Skip to content

Commit

Permalink
Merge pull request #1 from imdrasil/add_form_nesting
Browse files Browse the repository at this point in the history
Add form nesting
  • Loading branch information
imdrasil authored Jan 8, 2019
2 parents 9d4b9bd + 72d7e3b commit e3b51f2
Show file tree
Hide file tree
Showing 24 changed files with 1,409 additions and 247 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*.dwarf

/spec/support/structure.sql
/spec/support/database.yml

# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
Expand Down
6 changes: 4 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ env:
global:
- DB_USER=postgres
- DB_PASSWORD=""
before_script: make sam db:setup
script: crystal spec
before_script:
- cp ./spec/support/database.yml.example ./spec/support/database.yml
- make sam db:setup
script: crystal spec
103 changes: 103 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ require "form_object"
require "form_object/coercer/pg" # if you are going to use PG::Numeric
```

Also it is important to notice that `form_object` modifies `HTTP::Request` core class to store body in private variable `@cached_body : IO::Memory?` of maximum size 1 GB. This is done because to allow request body multiple reading.

### Defining Form

Forms are defined in the separate classes. Often (but not necessary) these classes are pretty similar to related models:
Expand Down Expand Up @@ -54,6 +56,107 @@ f.errors # Jennifer::Model::Errors

Resource model translation messages are used for the form.

#### Nesting

To define nested object use `.object` macro:

```crystal
class AddressForm < FormObject::Base(Address)
attr :street, Address
end
class ContactForm < FormObject::Base(Contact)
object :address, Address
end
```

For collection use `.collection` macro.

##### Populators

In `#verify`, nested hash is passed. Form object by default will try to match nested hashes to the nested forms. But sometimes the incoming hash and the existing object graph are not matching 1-to-1. That's where populators will help you.

You have to declare a populator when the form has to deserialize nested input. ATM populator may be only a method name.

Populator is called only if an incoming part for particular object is present.

```crystal
# request with { addresses: [{ :street => "Some street" }]} payload
form.verify(request) # will call populator once
# request with { addresses: [] of String} payload
form.verify(request) # will not call populator
```

Populator for collection is executed for every collection part in the incoming hash.

```crystal
class ContactForm < FormObject::Base(Contact)
collection :addresses, Address, populator: :address_populator
def address_populator(collection, index, **opts)
if item = collection[index]?
item
else
item = AddressForm.new(Address.new({contact_id: resource.id}))
collection << item
item
end
end
```

This populator checks if a nested form is already existing by using `collection[index]?`. While the `index` argument represents where we are in the incoming array traversal, `collection` is identical to `self.addresses`.

It is very important that each populator invocation returns the *form* not the model.

##### Delete

Populators can not only create, but also destroy. Let's say the following input is passed in.

```crystal
# request with the { addresses: [{:street => "Street", :id => 2, :_delete => "1" }] } payload
form.verify(request)
```

You can implement your own deletion:

```crystal
class ContactForm < FormObject::Base(Contact)
collection :addresses, Address, populator: :address_populator
property ids_to_destroy : Array(Int32)
def address_populator(context, **opts)
item = addresses.find { |address| address.id == context["id"] }
if context["_delete"]
addresses.delete(item)
ids_to_destroy << item.id
skip
end
if item
item
else
item = AddressForm.new(Address.new)
collection << item
item
end
end
def persist
super.tap do |result|
next unless result
ids = ids_to_destroy
Address.where { _id.in(ids) }.destroy
end
end
end
```

##### Skip

Populators can skip processing of a part by invoking `#skip`. This method raises `FormObject::SkipException` which makes form object to ignore particular part.

#### Reusability

To reuse common attributes or functionality you can use modules inclusion and inheritance:
Expand Down
6 changes: 3 additions & 3 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: form_object
version: 0.1.0
version: 0.2.0

authors:
- Roman Kalnytskyi <[email protected]>

crystal: 0.26.1
crystal: 0.27.0

license: MIT

Expand All @@ -20,5 +20,5 @@ development_dependencies:
version: "~> 0.5"
jennifer:
github: imdrasil/jennifer.cr
branch: master
branch: release/0.7.0
# version: "~> 0.6.1"
Loading

0 comments on commit e3b51f2

Please sign in to comment.