reform 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +0 -1
  3. data/CHANGES.md +13 -0
  4. data/Gemfile +0 -2
  5. data/README.md +276 -94
  6. data/Rakefile +6 -0
  7. data/TODO.md +10 -1
  8. data/database.sqlite3 +0 -0
  9. data/lib/reform/active_record.rb +2 -0
  10. data/lib/reform/composition.rb +55 -0
  11. data/lib/reform/form/active_model.rb +60 -15
  12. data/lib/reform/form/active_record.rb +3 -3
  13. data/lib/reform/form/composition.rb +69 -0
  14. data/lib/reform/form.rb +183 -80
  15. data/lib/reform/rails.rb +8 -1
  16. data/lib/reform/representer.rb +38 -0
  17. data/lib/reform/version.rb +1 -1
  18. data/lib/reform.rb +5 -2
  19. data/reform.gemspec +3 -2
  20. data/test/active_model_test.rb +83 -9
  21. data/test/coercion_test.rb +26 -0
  22. data/test/composition_test.rb +57 -0
  23. data/test/dummy/Rakefile +7 -0
  24. data/test/dummy/app/controllers/albums_controller.rb +18 -0
  25. data/test/dummy/app/controllers/application_controller.rb +4 -0
  26. data/test/dummy/app/controllers/musician_controller.rb +5 -0
  27. data/test/dummy/app/forms/album_form.rb +18 -0
  28. data/test/dummy/app/helpers/application_helper.rb +2 -0
  29. data/test/dummy/app/models/album.rb +4 -0
  30. data/test/dummy/app/models/song.rb +3 -0
  31. data/test/dummy/app/views/albums/new.html.erb +28 -0
  32. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  33. data/test/dummy/config/application.rb +20 -0
  34. data/test/dummy/config/boot.rb +10 -0
  35. data/test/dummy/config/database.yml +22 -0
  36. data/test/dummy/config/environment.rb +5 -0
  37. data/test/dummy/config/environments/development.rb +16 -0
  38. data/test/dummy/config/environments/production.rb +46 -0
  39. data/test/dummy/config/environments/test.rb +33 -0
  40. data/test/dummy/config/locales/en.yml +5 -0
  41. data/test/dummy/config/routes.rb +4 -0
  42. data/test/dummy/config.ru +4 -0
  43. data/test/dummy/db/test.sqlite3 +0 -0
  44. data/test/dummy/log/production.log +0 -0
  45. data/test/dummy/log/server.log +0 -0
  46. data/test/errors_test.rb +95 -0
  47. data/test/form_composition_test.rb +60 -0
  48. data/test/nested_form_test.rb +129 -0
  49. data/test/rails/integration_test.rb +54 -0
  50. data/test/reform_test.rb +80 -114
  51. data/test/test_helper.rb +14 -1
  52. metadata +86 -11
  53. data/lib/reform/form/dsl.rb +0 -38
  54. data/test/dsl_test.rb +0 -43
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ spec/reports
16
16
  test/tmp
17
17
  test/version_tmp
18
18
  tmp
19
+ test/dummy/log/*.log
data/.travis.yml CHANGED
@@ -2,7 +2,6 @@ language: ruby
2
2
  rvm:
3
3
  - 2.0.0
4
4
  - 1.9.3
5
- - 1.8.7
6
5
  gemfile:
7
6
  - Gemfile
8
7
  - gemfiles/Gemfile.rails-3.0
data/CHANGES.md CHANGED
@@ -1,3 +1,16 @@
1
+ h3. 0.2.0
2
+
3
+ * Added nested property and collection for `has_one` and `has_many` relationships. . Note that this currently works only 1-level deep.
4
+ * Renamed `Reform::Form::DSL` to `Reform::Form::Composition` and deprecated `DSL`.
5
+ * `require 'reform'` now automatically requires Rails stuff in a Rails environment. Mainly, this is the FormBuilder compatibility layer that is injected into `Form`. If you don't want that, only require 'reform/form'.
6
+ * Composition now totally optional
7
+ * `Form.new` now accepts one argument, only: the model/composition. If you want to create your own representer, inject it by overriding `Form#mapper`. Note that this won't create property accessors for you.
8
+ * `Form::ActiveModel` no longer creates accessors to your represented models, e.g. having `property :title, on: :song` doesn't allow `form.song` anymore. This is because the actual model and the form's state might differ, so please use `form.title` directly.
9
+
10
+ h3. 0.1.3
11
+
12
+ * Altered `reform/rails` to conditionally load `ActiveRecord` code and created `reform/active_record`.
13
+
1
14
  h3. 0.1.2
2
15
 
3
16
  * `Form#to_model` is now delegated to model.
data/Gemfile CHANGED
@@ -1,7 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in reform.gemspec
4
3
  gemspec
5
4
 
6
- gem 'rake', "10.1.0.beta3"
7
5
  #gem 'representable', path: "../representable"
data/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Decouple your models from forms. Reform gives you a form object with validations and nested setup of models. It is completely framework-agnostic and doesn't care about your database.
4
4
 
5
+ Although reform can be used in any Ruby framework, it comes with [Rails support](#rails-integration), works with [simple_form and other form gems](#formbuilder-support), allows nesting forms to implement [has_one](#nesting-forms-1-1-relations) and [has_many](#nesting-forms-1-n-relations) relationships, can [compose a form](#compositions) from multiple objects and gives you [coercion](#coercion).
6
+
7
+
5
8
  ## Installation
6
9
 
7
10
  Add this line to your Gemfile:
@@ -12,47 +15,47 @@ gem 'reform'
12
15
 
13
16
  ## Defining Forms
14
17
 
15
- Say you need a form for song requests on a radio station. Internally, this would imply associating `songs` table and the `artists` table. You don't wanna reflect that in your web form, do you?
18
+ You're working at a famous record label and your job is archiving all the songs, albums and artists. You start with a form to populate your `songs` table.
16
19
 
17
20
  ```ruby
18
- class SongRequestForm < Reform::Form
19
- include DSL
20
-
21
- property :title, on: :song
22
- property :name, on: :artist
21
+ class SongForm < Reform::Form
22
+ property :title
23
+ property :length
23
24
 
24
- validates :name, :title, presence: true
25
+ validates :title, presence: true
26
+ validates :length, numericality: true
25
27
  end
26
28
  ```
27
29
 
28
- The `::property` method allows defining the fields of the form. Using `:on` delegates this field to a nested object in your form.
30
+ To add fields to the form use the `::property` method. Also, validations no longer go into the model but sit in the form.
29
31
 
30
- __Note__: There is a convenience method `::properties` that allows you to pass an array of fields at one time.
31
32
 
32
33
  ## Using Forms
33
34
 
34
35
  In your controller you'd create a form instance and pass in the models you wanna work on.
35
36
 
36
37
  ```ruby
37
- def new
38
- @form = SongRequestForm.new(song: Song.new, artist: Artist.new)
39
- end
38
+ class SongsController
39
+ def new
40
+ @form = SongForm.new(Song.new)
41
+ end
40
42
  ```
41
43
 
42
44
  You can also setup the form for editing existing items.
43
45
 
44
46
  ```ruby
45
- def edit
46
- @form = SongRequestForm.new(song: Song.find(1), artist: Artist.find(2))
47
- end
47
+ class SongsController
48
+ def edit
49
+ @form = SongForm.new(Song.find(1))
50
+ end
48
51
  ```
49
52
 
50
53
  ## Rendering Forms
51
54
 
52
- Your `@form` is now ready to be rendered, either do it yourself or use something like `simple_form`.
55
+ Your `@form` is now ready to be rendered, either do it yourself or use something like Rails' `#form_for`, `simple_form` or `formtastic`.
53
56
 
54
57
  ```haml
55
- = simple_form_for @form do |f|
58
+ = form_for @form do |f|
56
59
 
57
60
  = f.input :name
58
61
  = f.input :title
@@ -63,15 +66,16 @@ Your `@form` is now ready to be rendered, either do it yourself or use something
63
66
  After a form submission, you wanna validate the input.
64
67
 
65
68
  ```ruby
66
- def create
67
- @form = SongRequestForm.new(song: Song.new, artist: Artist.new)
69
+ class SongsController
70
+ def create
71
+ @form = SongForm.new(Song.new)
68
72
 
69
- #=> params: {song_request: {title: "Rio", name: "Duran Duran"}}
73
+ #=> params: {song: {title: "Rio", length: "366"}}
70
74
 
71
- if @form.validate(params[:song_request])
75
+ if @form.validate(params[:song])
72
76
  ```
73
77
 
74
- `Reform` uses the validations you provided in the form - and nothing else.
78
+ Reform uses the validations you provided in the form - and nothing else.
75
79
 
76
80
 
77
81
  ## Saving Forms
@@ -79,143 +83,320 @@ def create
79
83
  We provide a bullet-proof way to save your form data: by letting _you_ do it!
80
84
 
81
85
  ```ruby
82
- if @form.validate(params[:song_request])
86
+ if @form.validate(params[:song])
83
87
 
84
88
  @form.save do |data, nested|
85
- #=> data: <title: "Rio", name: "Duran Duran">
86
- #
87
- # nested: {song: {title: "Rio"},
88
- # artist: {name: "Duran Duran"}}
89
+ data.title #=> "Rio"
90
+ data.length #=> "366"
91
+
92
+ nested #=> {title: "Rio"}
89
93
 
90
- SongRequest.new(nested[:song][:title])
94
+ Song.create(nested)
91
95
  end
92
96
  ```
93
97
 
94
- While `data` gives you an object exposing the form property readers, `nested` already reflects the nesting you defined in your form earlier.
98
+ While `data` gives you an object exposing the form property readers, `nested` is a hash reflecting the nesting structure of your form. Note how you can use arbitrary code to create/update models - in this example, we used `Song::create`.
95
99
 
96
100
  To push the incoming data to the models directly, call `#save` without the block.
97
101
 
98
102
  ```ruby
99
- @form.save #=> populates song and artist with incoming data
100
- # by calling @form.song.name= and @form.artist.title=.
103
+ @form.save #=> populates song with incoming data
104
+ # by calling @form.song.title= and @form.song.length=.
101
105
  ```
102
106
 
103
- ## Coercion
104
107
 
105
- Often you want incoming form data to be converted to a type, like timestamps. Reform uses [virtus](https://github.com/solnic/virtus) for coercion, the DSL is seamlessly integrated into Reform with the `:type` option.
108
+ ## Nesting Forms: 1-1 Relations
106
109
 
107
- Be sure to add `virtus` to your Gemfile.
110
+ Songs have artists to compose them. Let's say your `Song` model would implement that as follows.
108
111
 
109
112
  ```ruby
110
- require 'reform/form/coercion'
113
+ class Song < ActiveRecord::Base
114
+ has_one :artist
115
+ end
116
+ ```
117
+
118
+ The edit form should allow changing data for artist and song.
119
+
120
+ ```ruby
121
+ class SongForm < Reform::Form
122
+ property :title
123
+ property :length
111
124
 
112
- class SongRequestForm < Reform::Form
113
- include DSL
114
- include Reform::Form::Coercion
125
+ property :artist do
126
+ property :name
115
127
 
116
- property :written_at, on: :song, type: DateTime
128
+ validates :name, presence: true
129
+ end
130
+
131
+ #validates :title, ...
117
132
  end
133
+ ```
134
+
135
+ See how simple nesting forms is? By passing a block to `::property` you can define another form nested into your main form.
136
+
137
+
138
+ ### has_one: Setup
139
+
140
+ This setup's only requirement is having a working `Song#artist` reader.
141
+
142
+ ```ruby
143
+ class SongsController
144
+ def edit
145
+ song = Song.find(1)
146
+ song.artist #=> <0x999#Artist title="Duran Duran">
147
+
148
+ @form = SongForm.new(song)
149
+ end
150
+ ```
151
+
152
+ ### has_one: Rendering
153
+
154
+ When rendering this form you could use the form's accessors manually.
155
+
156
+ ```haml
157
+ = text_field :title, @form.title
158
+ = text_field "artist[name]", @form.artist.name
159
+ ```
118
160
 
161
+ Or use something like `#fields_for` in a Rails environment.
162
+
163
+ ```haml
164
+ = form_for @form |f|
165
+ = f.text_field :title
166
+ = f.text_field :length
167
+
168
+ = fields_for :artist do |a|
169
+ = a.text_field :name
170
+ ```
171
+
172
+ ### has_one: Processing
173
+
174
+ The block form of `#save` would give you the following data.
175
+
176
+ ```ruby
119
177
  @form.save do |data, nested|
120
- data.written_at #=> <DateTime XXX>
178
+ data.title #=> "Hungry Like The Wolf"
179
+ data.artist.name #=> "Duran Duran"
180
+
181
+ nested #=> {title: "Hungry Like The Wolf",
182
+ # artist: {name: "Duran Duran"}}
183
+ end
121
184
  ```
122
185
 
123
- ## Rails Integration
186
+ Supposed you use reform's automatic save without a block, the following assignments would be made.
187
+
188
+ ```ruby
189
+ form.song.title = "Hungry Like The Wolf"
190
+ form.song.artist.name = "Duran Duran"
191
+ ```
192
+
193
+ ## Nesting Forms: 1-n Relations
194
+
195
+ Reform also gives you nested collections.
196
+
197
+ Let's have Albums with songs!
198
+
199
+ ```ruby
200
+ class Album < ActiveRecord::Base
201
+ has_many :songs
202
+ end
203
+ ```
204
+
205
+ The form might look like this.
206
+
207
+ ```ruby
208
+ class AlbumForm < Reform::Form
209
+ property :title
210
+
211
+ collection :songs do
212
+ property :title
213
+
214
+ validates :title, presence: true
215
+ end
216
+ end
217
+ ```
218
+
219
+ This basically works like a nested `property` that iterates over a collection of songs.
220
+
221
+ ### has_many: Rendering
222
+
223
+ Reform will expose the collection using the `#songs` method.
224
+
225
+ ```haml
226
+ = text_field :title, @form.title
227
+ = text_field "songs[0][title]", @form.songs[0].title
228
+ ```
124
229
 
125
- [A sample Rails app using Reform.](https://github.com/gogogarrett/reform_example)
230
+ However, `#fields_for` works just fine, again.
126
231
 
127
- Reform offers ActiveRecord support to easily make this accessible in Rails based projects. You simply `include Reform::Form::ActiveRecord` in your form object and the Rails specific code will be handled for you. This happens by adding behaviour to make the form ActiveModel-compliant. Note that this module will also work with other ORMs like Datamapper.
232
+ ```haml
233
+ = form_for @form |f|
234
+ = f.text_field :title
235
+
236
+ = fields_for :songs do |s|
237
+ = s.text_field :title
238
+ ```
128
239
 
129
- ### Simple Integration
130
- #### Form Class
240
+ ### has_many: Processing
131
241
 
132
- You have to include a call to `model` to specify which is the main object of the form.
242
+ The block form of `#save` will expose the data structures already discussed.
133
243
 
134
244
  ```ruby
135
- require 'reform/rails'
245
+ @form.save do |data, nested|
246
+ data.title #=> "Rio"
247
+ data.songs.first.title #=> "Hungry Like The Wolf"
248
+
249
+ nested #=> {title: "Rio"
250
+ songs: [{title: "Hungry Like The Wolf"},
251
+ {title: "Last Chance On The Stairways"}]
252
+ end
253
+ ```
136
254
 
137
- class UserProfileForm < Reform::Form
138
- include DSL
139
- include Reform::Form::ActiveRecord
140
255
 
141
- property :email, on: :user
142
- properties [:gender, :age], on: :profile
256
+ ## Compositions
143
257
 
144
- model :user
258
+ Sometimes you might want to embrace two (or more) unrelated objects with a single form. While you could write a simple delegating composition yourself, reform comes with it built-in.
145
259
 
146
- validates :email, :gender, presence: true
147
- validates :age, numericality: true
148
- validates_uniqueness_of :email
260
+ Say we were to edit a song and the label data the record was released from. Internally, this would imply working on the `songs` table and the `labels` table.
261
+
262
+ ```ruby
263
+ class SongWithLabelForm < Reform::Form
264
+ include Composition
265
+
266
+ property :title, on: :song
267
+ property :city, on: :label
268
+
269
+ validates :title, :city, presence: true
149
270
  end
150
271
  ```
151
272
 
152
- Basically, `model :user` tells Reform to use the `:user` object in the composition as the form main object while using `"user"` as the form name (needed for URL computation). If you want to change the form name let Reform know.
273
+ Note that reform needs to know about the owner objects of properties. You can do so by using the `on:` option.
274
+
275
+ ### Composition: Setup
276
+
277
+ The constructor slightly differs.
153
278
 
154
279
  ```ruby
155
- model :singer, :on => :user # form name is "singer" whereas main object is `:user` in composition.
280
+ @form = SongWithLabelForm.new(song: Song.new, label: Label.new)
156
281
  ```
157
282
 
283
+ ### Composition: Rendering
284
+
285
+ After you configured your composition in the form, reform hides the fact that you're actually showing two different objects.
158
286
 
159
- #### View Form
287
+ ```haml
288
+ = form_for @form do |f|
160
289
 
161
- The form becomes __very__ dumb as it knows nothing about the backend assocations or data binding to the database layer. This simply takes input and passes it along to the controller as it should.
290
+ Song: = f.input :title
162
291
 
163
- ```erb
164
- <%= form_for @form do |f| %>
165
- <%= f.email_field :email %>
166
- <%= f.input :gender %>
167
- <%= f.number_field :age %>
168
- <%= f.submit %>
169
- <% end %>
292
+ Label in: = f.input :city
170
293
  ```
171
294
 
172
- #### Controller
295
+ ### Composition: Processing
296
+
297
+ When using `#save' without a block reform will use writer methods on the different objects to push validated data to the properties.
173
298
 
174
- In the controller you can easily create helpers to build these form objects for you. In the create and update actions Reform allows you total control of what to do with the data being passed via the form. How you interact with the data is entirely up to you.
299
+ Here's how the block parameters look like.
175
300
 
176
301
  ```ruby
177
- class UsersController < ApplicationController
302
+ @form.save do |data, nested|
303
+ data.title #=> "Rio"
304
+ data.city #=> "London"
178
305
 
179
- def create
180
- @form = create_new_form
181
- if @form.validate(params[:user])
182
- @form.save do |data, map|
183
- new_user = User.new(map[:user])
184
- new_user.build_user_profile(map[:profile])
185
- new_user.save!
186
- end
187
- end
188
- end
306
+ nested #=> {
307
+ song: {title: "Rio"}
308
+ label: {city: "London"}
309
+ }
310
+ end
311
+ ```
189
312
 
313
+ ## Coercion
190
314
 
191
- private
192
- def create_new_form
193
- UserProfileForm.new(user: User.new, profile: UserProfile.new)
194
- end
315
+ Often you want incoming form data to be converted to a type, like timestamps. Reform uses [virtus](https://github.com/solnic/virtus) for coercion, the DSL is seamlessly integrated into Reform with the `:type` option.
316
+
317
+ Be sure to add `virtus` to your Gemfile.
318
+
319
+ ```ruby
320
+ require 'reform/form/coercion'
321
+
322
+ class SongForm < Reform::Form
323
+ include Coercion
324
+
325
+ property :written_at, type: DateTime
195
326
  end
327
+
328
+ @form.save do |data, nested|
329
+ data.written_at #=> <DateTime XXX>
196
330
  ```
197
331
 
198
- __Note__: this can also be used for the update action as well.
199
332
 
200
- ## Using Your Models In Validations
333
+ ## Agnosticism: Mapping Data
334
+
335
+ Reform doesn't really know whether it's working with a PORO, an `ActiveRecord` instance or a `Sequel` row.
336
+
337
+ When rendering the form, reform calls readers on the decorated model to retrieve the field data (`Song#title`, `Song#length`).
338
+
339
+ When saving a submitted form, the same happens using writers. Reform simply calls `Song#title=(value)`. No knowledge is required about the underlying database layer.
340
+
341
+ Nesting forms only requires readers for the nested properties as `Album#songs`.
342
+
343
+
344
+ ## Rails Integration
201
345
 
202
- Sometimes you want to access your database in a validation. You can access the models using the `#model` accessor in the form.
346
+ Check out [@gogogarret](https://twitter.com/GoGoGarrett/)'s [sample Rails app](https://github.com/gogogarrett/reform_example) using Reform.
347
+
348
+ Rails and Reform work out-of-the-box. If you're using Rails but for some reason wanna use the pure reform, `require reform/form`, only.
349
+
350
+
351
+ ## ActiveModel compliance
352
+
353
+ Forms in Reform can easily be made ActiveModel-compliant.
354
+
355
+ Note that this step is _not_ necessary in a Rails environment.
203
356
 
204
357
  ```ruby
205
- class ArtistForm < Reform::Form
206
- property :name
358
+ class SongForm < Reform::Form
359
+ include Reform::Form::ActiveModel
360
+ end
361
+ ```
207
362
 
208
- validate "name_correct?"
363
+ If you're not happy with the `model_name` result, configure it manually.
209
364
 
210
- def name_correct?
211
- errors.add :name, "#{name} is stupid!" if model.artist.stupid_name?(name)
212
- end
365
+ ```ruby
366
+ class CoverSongForm < Reform::Form
367
+ include Reform::Form::ActiveModel
368
+
369
+ model :hit
213
370
  end
214
371
  ```
215
372
 
373
+ This is especially helpful when your framework tries to render `cover_song_path` although you wanna go with `song_path`.
374
+
375
+
376
+ ## FormBuilder Support
377
+
378
+ To make your forms work with all the form gems like `simple_form` or Rails `form_for` you need to include another module.
379
+
380
+ Again, this step is implicit in Rails and you don't need to do it manually.
381
+
382
+ ```ruby
383
+ class SongForm < Reform::Form
384
+ include Reform::Form::ActiveModel
385
+ include Reform::Form::ActiveModel::FormBuilderMethods
386
+ end
387
+ ```
388
+
389
+
216
390
  ## Security
217
391
 
218
- By explicitely defining the form layout using `::property` there is no more need for protecting from unwanted input. `strong_parameter` or `attr_accessible` become obsolete. Reform will simply ignore undefined incoming parameters.
392
+ By explicitely defining the form layout using `::property` there is no more need for protecting from unwanted input. `strong_parameter` or `
393
+ attr_accessible` become obsolete. Reform will simply ignore undefined incoming parameters.
394
+
395
+
396
+ ## Support
397
+
398
+ If you run into any trouble chat with us on irc.freenode.org#trailblazer.
399
+
219
400
 
220
401
  ## Maintainers
221
402
 
@@ -223,6 +404,7 @@ By explicitely defining the form layout using `::property` there is no more need
223
404
 
224
405
  [Garrett Heinlen](https://github.com/gogogarrett)
225
406
 
407
+
226
408
  ### Attributions!!!
227
409
 
228
410
  Great thanks to [Blake Education](https://github.com/blake-education) for giving us the freedom and time to develop this project while working on their project.
data/Rakefile CHANGED
@@ -7,3 +7,9 @@ Rake::TestTask.new(:test) do |test|
7
7
  test.test_files = FileList['test/*_test.rb']
8
8
  test.verbose = true
9
9
  end
10
+
11
+ Rake::TestTask.new(:test_rails) do |test|
12
+ test.libs << 'test'
13
+ test.test_files = FileList['test/rails/*_test.rb']
14
+ test.verbose = true
15
+ end
data/TODO.md CHANGED
@@ -1,4 +1,13 @@
1
1
  * `validates :title, :presence => true`
2
2
  with @model.title == "Little Green Car" and validate({}) the form is still valid (as we "have" a valid title). is that what we want?
3
3
 
4
- * document Form#to_hash and Form#to_nested_hash (e.g. with OpenStruct composition to make it a very simple form)
4
+ * document Form#to_hash and Form#to_nested_hash (e.g. with OpenStruct composition to make it a very simple form)
5
+
6
+ * allow :as to rename nested forms
7
+ * make #nested_forms easier
8
+
9
+ vererben in inline representern (module zum einmixen, attrs löschen)
10
+
11
+ # TODO: remove the concept of Errors#messages and just iterate over Errors.
12
+ # each form contains its local field errors in Errors
13
+ # form.messages should then go through them and compile a "summary" instead of adding them to the parents #errors in #validate.
data/database.sqlite3 CHANGED
Binary file
@@ -0,0 +1,2 @@
1
+ require 'reform/form/active_model'
2
+ require 'reform/form/active_record'
@@ -0,0 +1,55 @@
1
+ module Reform
2
+ # Keeps composition of models and knows how to transform a plain hash into a nested hash.
3
+ class Composition
4
+ class << self
5
+ def map(options)
6
+ @attr2obj = {} # {song: ["title", "track"], artist: ["name"]}
7
+
8
+ options.each do |mdl, meths|
9
+ create_accessors(mdl, meths)
10
+ attr_reader mdl
11
+
12
+ meths.each { |m| @attr2obj[m.to_s] = mdl }
13
+ end
14
+ end
15
+
16
+ # Specific to representable.
17
+ def map_from(representer)
18
+ options = {}
19
+ representer.representable_attrs.each do |cfg|
20
+ options[cfg.options[:on]] ||= []
21
+ options[cfg.options[:on]] << cfg.name
22
+ end
23
+
24
+ map options
25
+ end
26
+
27
+ def model_for_property(name)
28
+ @attr2obj.fetch(name.to_s)
29
+ end
30
+
31
+ private
32
+ def create_accessors(model, methods)
33
+ accessors = methods.collect { |m| [m, "#{m}="] }.flatten
34
+ delegate *accessors << {:to => :"#{model}"}
35
+ end
36
+ end
37
+
38
+ # TODO: make class method?
39
+ def nested_hash_for(attrs)
40
+ {}.tap do |hsh|
41
+ attrs.each do |name, val|
42
+ obj = self.class.model_for_property(name)
43
+ hsh[obj] ||= {}
44
+ hsh[obj][name.to_sym] = val
45
+ end
46
+ end
47
+ end
48
+
49
+ def initialize(models)
50
+ models.each do |name, obj|
51
+ instance_variable_set(:"@#{name}", obj)
52
+ end
53
+ end
54
+ end
55
+ end