Formalism
Ruby gem for forms with validations and nesting.
Why
I need for service-like objects.
I've explored these projects:
But nothing of them supports all features I need for:
- nesting (into unlimited levels) of themselves;
- simple syntax;
- custom validations and coercions;
- unified output.
So, I've tried to combine these all into one library and got Formalism.
Why here are forms and what about service objects?
I've discovered that form object, only with validations, are useless without service objects. So, I've combined them: service objects include validations.
If these are service objects, why they called forms?
Because if we're combining them — it's more like forms with logic inside for me
than service objects built-in forms. Even in HTML we're writing <form>
.
So, Formalism can accept all data from any-difficult <form>
and process it,
also with nested forms (for example, if you have some request form
with contact data and want to pass contacts into something like user form).
And if I need for simple service object without validation?
You can use Formalism::Action
, a parent of Formalism::Form
.
Installation
Add this line to your application's Gemfile:
gem 'formalism'
And then execute:
bundle install
Or install it yourself as:
gem install formalism
Usage
Basic example
class FindArtistForm < Formalism::Form
field :name
private
def validate
if name.to_s.empty?
errors.add 'Name is not provided'
end
end
def execute
Artist.first(fields_and_nested_forms)
end
end
class CreateAlbumForm < Formalism::Form
field :name, String
fiels :tags, Array, of: String
nested :artist, FindArtistForm
private
def validate
if name.to_s.empty?
errors.add 'Name is not provided'
end
end
def execute
Album.create(fields_and_nested_forms)
end
end
form = CreateAlbumForm.new(
name: 'Hits', tags: %w[Indie Rock Hits], artist: { name: 'Alex' }
)
form.run
Running
Usually you need to initialize a form and execute #run
method.
Internally, it runs #valid?
(public) and #execute
(private) methods.
#valid?
runs #validate
(private) of a form itself and nested forms.
#run
can be redefined for database transaction, for example.
Also you can call .run
with arguments for #initialize
,
it's the alias for #initialize
+ #run
.
Form outcome
Any call of run
returns Form::Outcome
instance which has #success?
,
#result
and #errors
methods. Result is a result of #execute
method.
Be careful: calling #result
for failed outcome will raise ValidationError
.
Field type
Field receives type as the second argument. It's not required. It can be a constant, String or Symbol. If specified — there is a coercion to specified type, if not — data remains unchanged.
Nested forms — their class, as constant.
Type or :initialize
block is required.
Formalism also supports Array
type with the optional :of
option
(type of elements).
Coercion will be applied to a data itself and to its elements.
Coercion
There is built-in coercion into some types, if you try to coerce
to undefined type — you'll get Formalism::Form::NoCoercionError
.
You can define a coercion to some type via definition of such class:
# frozen_string_literal: true
module Formalism
class Form < Action
class Coercion
## Class for coercion to String
class String < Base
private
def execute
@value&.to_s
end
end
end
end
end
Default value
field
and nested
accepts :default
option.
It can be any value, if it's an instance of Proc
— it'll be executed
in the form instance scope.
Different keys
field
supports :key
option (Symbol) to receive data by a different key,
not as a field name.
Custom initialization of nested forms
By default, nested forms initialized with data by key as their name
in parent data. So, if a parent receive { foo: 1, bar: { baz: 2 } }
,
it's nested form :bar
will receive { baz: 2 }
.
If you want to prevent initialization at all, or pass custom arguments —
you should use :initialize
option which accepts a proc
with a form class argument.
If you want to just refine incoming data (add or remove) — you should define
#params_for_nested_*
private method, where *
is a nested form name.
You can use super
inside.
Order of filling with data
Fields and nested forms are filling in order of their definition.
But sometimes you want to change this order, for example,
if you have a nested forms in ancestors which depends on data in children forms.
For such cases you can use :depends_on
option, which accepts fields
and nested forms names as Symbol or Array of symbols. They will be filled
(and initialized) before dependent.
Merging into final data
There is Form#fields_and_nested_forms
as final data
(after coercion, defaults, etc). But you may want to not include some fields
or nested forms into this data. You can do it via :merge
option,
which can be true
, false
or Proc
(executed in form's instance scope).
For example:
field :bar, merge: true
nested :only_valid, nested_form_class, merge: ->(form) { form.valid? }
Runnable
You can disable #valid?
and #run
of forms (including nested ones)
by setting form.runnable = false
.
It can be helpful for some cases, for example, with policies (permissions):
def initialize_nested_form(name, options)
return unless (form = super)
form.runnable = allowed_to_change?(name)
form
end
Inheritance
Any class ChildForm < ParentForm
will have all fields and nested forms
from ParentForm
.
Removing (inherited) field
But you're able to remove (usually inherited) fields by:
class ChildForm < ParentForm
remove_field :field_from_parent
end
Modules
You can define modules and use them later like this:
module CommonFields
include Formalism::Form::Fields
field :base_field
nested :base_nested
end
class SomeForm < Formalism::Form
include CommonFields
field :another_field
end
Convert to params
You can convert a Form back to (processed) params, for example, for view render:
form = CreateAlbumForm.new(
name: 'Hits', tags: %w[Indie Rock Hits], artist: { name: 'Alex' }
)
form.to_params
# {
# name: 'Hits',
# tags: %w[Indie Rock Hits],
# artist: { name: 'Alex' }
# }
Actions
For actions without fields, nesting and validation you can use
Formalism::Action
(the parent of Formalism::Form
).
Plugins
There is a few plugins which I personally need for:
-
formalism-model_forms
Default CRUD forms for Sequel DB models. Can be renamed! -
formalism-sequel_transactions
Sequel transactions inside forms. -
formalism-r18n_errors
R18n errors inside forms, including validation helpers. Can be separated!
Development
After checking out the repo, run bundle install
to install dependencies.
Then, run toys rspec
to run the tests.
To install this gem onto your local machine, run toys gem install
.
To release a new version, run toys gem release %version%
.
See how it works here.
Contributing
Bug reports and pull requests are welcome on GitHub.
License
The gem is available as open source under the terms of the MIT License.