reform 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: be845dd1b0e8a941df46ef6ed8d582b440b44acf
4
- data.tar.gz: 4a4d7e81be355c4164fb884060eb07194bad0af6
3
+ metadata.gz: b95fac93b5186e174e6e2aef263bff4bf5305210
4
+ data.tar.gz: bafdc2c15f78fa1fe27e9b554c32d3aafcfb4fbd
5
5
  SHA512:
6
- metadata.gz: 0c3c10fbedfb3cf9df5afdfbd98088a1ef948b9aa7b8730d294673b624309786658a450dcdafdd4571ef35ffbbfd3e3e724accd1bc2e250f9345c323f57640c6
7
- data.tar.gz: 3c24007806a3469ce08ecc1ff13c40bf7e271b46d8fdaa8e098e2da0ed1a62571c51fcd7e0feca928c56c01c49bda1d632cb47c8aa7f2fdf9c9255274fbb2c5d
6
+ metadata.gz: a55abac9d331f20d75fa66ed8950c35e5cd16460fc054a182c6d0cfb0c4d9e1524349553b9aeeeafa33b75453d35ce3c8476ad8f0adce7775c41d99d4176338a
7
+ data.tar.gz: 61d436b3f4cb696e2cffa02f7e3ff1179741209da8a4f2073e36da45d12971983ff19be9ffdcb0b515ed601f933240685287fa208287a63f521d3d4dbcb2e99c
data/CHANGES.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 1.1.0
2
+
3
+ * Deprecate first block argument in save. It's new signature is `save { |hash| }`. You already got the form instance when calling `form.save` so there's no need to pass it into the block.
4
+ * `#validate` does **not** touch any model anymore. Both single values and collections are written to the model after `#sync` or `#save`.
5
+ * Coercion now happens in `#validate`, only.
6
+ * You can now define forms in modules including `Reform::Form::Module` to improve reusability.
7
+ * Inheriting from forms and then overriding/extending properties with `:inherit` now works properly.
8
+ * You can now define methods in inline forms.
9
+ * Forms can now also deserialize other formats, e.g. JSON. This allows them to be used as a contract for API endpoints and in Operations in Trailblazer.
10
+ * Composition forms no longer expose readers to the composition members. The composition is available via `Form#model`, members via `Form#model[:member_name]`.
11
+ * ActiveRecord support is now included correctly and passed on to nested forms.
12
+ * Undocumented/Experimental: Scalar forms. This is still WIP.
13
+
1
14
  ## 1.0.4
2
15
 
3
16
  Reverting what I did in 1.0.3. Leave your code as it is. You may override a writers like `#title=` to sanitize or filter incoming data, as in
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  # gem 'representable', path: "../representable"
6
- # gem "disposable", path: "../disposable"
6
+ # gem "disposable", path: "../disposable"
data/README.md CHANGED
@@ -147,24 +147,29 @@ Sometimes, you need to do stuff manually.
147
147
 
148
148
  ## Saving Forms Manually
149
149
 
150
- This is where you call `#save` with a block. This won't touch the models at all but give you a nice hash, so you can do it yourself.
150
+ Calling `#save` with a block doesn't do anything but providing you a nested hash with all the validated input. This allows you to implement the saving yourself.
151
151
 
152
- Note that you can call `#sync` and _then_ call `#save` with the block to save models yourself.
152
+ The block parameter is a nested hash of the form input.
153
153
 
154
154
  ```ruby
155
- if @form.validate(params[:song])
155
+ @form.save do |hash|
156
+ hash #=> {title: "Rio", length: "366"}
156
157
 
157
- @form.save do |data, nested|
158
- data.title #=> "Rio"
159
- data.length #=> "366"
158
+ Song.create(hash)
159
+ end
160
+ ```
160
161
 
161
- nested #=> {title: "Rio", length: "366"}
162
+ You can always access the form's model. This is helpful when you were using populators to set up objects when validating.
162
163
 
163
- Song.create(nested)
164
- end
164
+ ```ruby
165
+ @form.save do |nested|
166
+ album = @form.album.model
167
+
168
+ album.update_attributes(nested[:album])
169
+ end
165
170
  ```
166
171
 
167
- 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`.
172
+ Note that you can call `#sync` and _then_ call `#save { |hsh| }` to save models yourself.
168
173
 
169
174
 
170
175
  ## Contracts
@@ -200,12 +205,14 @@ In future versions and with the upcoming [Trailblazer framework](https://github.
200
205
  Applying a contract is simple, all you need is a populated object (e.g. an album after `#update_attributes`).
201
206
 
202
207
  ```ruby
203
- album.update_attributes(..)
208
+ album.assign_attributes(..)
204
209
 
205
- if AlbumContract.new(album).validate
210
+ contract = AlbumContract.new(album)
211
+
212
+ if contract.validate
206
213
  album.save
207
214
  else
208
- raise album.errors.messages.inspect
215
+ raise contract.errors.messages.inspect
209
216
  end
210
217
  ```
211
218
 
@@ -486,6 +493,47 @@ Here's how the block parameters look like.
486
493
  end
487
494
  ```
488
495
 
496
+ ## Inheritance
497
+
498
+ Forms can be derived from other forms and will inherit all properties and validations.
499
+
500
+ ```ruby
501
+ class AlbumForm < Reform::Form
502
+ property :title
503
+
504
+ collection :songs do
505
+ property :title
506
+
507
+ validates :title, presence: true
508
+ end
509
+ end
510
+ ```
511
+
512
+ Now, a simple inheritance can add fields.
513
+
514
+ ```ruby
515
+ class CompilationForm < AlbumForm
516
+ property :composers do
517
+ property :name
518
+ end
519
+ end
520
+ ```
521
+
522
+ This will _add_ `composers` to the existing fields.
523
+
524
+ You can also partially override fields using `:inherit`.
525
+
526
+ ```ruby
527
+ class CompilationForm < AlbumForm
528
+ property :songs, inherit: true do
529
+ property :band_id
530
+ validates :band_id, presence: true
531
+ end
532
+ end
533
+ ```
534
+
535
+ Using `inherit:` here will extend the existing `songs` form with the `band_id` field. Note that this simply uses [representable's inheritance mechanism](https://github.com/apotonick/representable/#partly-overriding-properties).
536
+
489
537
  ## Coercion
490
538
 
491
539
  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.
Binary file
@@ -1,5 +1,6 @@
1
1
  require 'disposable/composition'
2
2
 
3
+ # TODO: replace that with lazy Twin and Composition from Disposable.
3
4
  module Reform
4
5
  class Expose
5
6
  include Disposable::Composition
@@ -1,31 +1,40 @@
1
1
  require 'forwardable'
2
2
  require 'uber/inheritable_attr'
3
+ require 'uber/delegates'
3
4
 
4
5
  require 'reform/representer'
5
6
 
6
7
  module Reform
7
8
  # Gives you a DSL for defining the object structure and its validations.
8
9
  class Contract # DISCUSS: make class?
9
- extend Forwardable
10
+ extend Uber::Delegates
10
11
 
11
12
  extend Uber::InheritableAttr
13
+ # representer_class gets inherited (cloned) to subclasses.
12
14
  inheritable_attr :representer_class
13
- self.representer_class = Reform::Representer.for(:form_class => self)
15
+ self.representer_class = Reform::Representer.for(:form_class => self) # only happens in Contract/Form.
16
+ # this should be the only mechanism to inherit, features should be stored in this as well.
14
17
 
18
+
19
+ # each contract keeps track of its features and passes them onto its local representer_class.
20
+ # gets inherited, features get automatically included into inline representer.
21
+ # TODO: the representer class should handle that, e.g. in options (deep-clone when inheriting.)
15
22
  inheritable_attr :features
16
- self.features = []
23
+ self.features = {}
24
+
17
25
 
18
26
  RESERVED_METHODS = [:model, :aliased_model, :fields, :mapper] # TODO: refactor that so we don't need that.
19
27
 
20
28
 
21
29
  module PropertyMethods
22
- extend Forwardable
23
-
24
30
  def property(name, options={}, &block)
25
31
  options[:private_name] = options.delete(:as)
26
32
 
27
- # at this point, :extend is a Form class.
28
- options[:features] = features if block_given?
33
+ options[:coercion_type] = options.delete(:type)
34
+
35
+ options[:features] ||= []
36
+ options[:features] += features.keys if block_given?
37
+
29
38
  definition = representer_class.property(name, options, &block)
30
39
  setup_form_definition(definition) if block_given? or options[:form]
31
40
 
@@ -45,7 +54,8 @@ module Reform
45
54
 
46
55
  def setup_form_definition(definition)
47
56
  options = {
48
- :form => definition[:form] || definition[:extend].evaluate(nil), # :form is always just a Form class name.
57
+ # TODO: make this a bit nicer. why do we need :form at all?
58
+ :form => (definition[:extend] and definition[:extend].evaluate(nil)) || definition[:form], # :form is always just a Form class name.
49
59
  :pass_options => true, # new style of passing args
50
60
  :prepare => lambda { |form, args| form }, # always just return the form without decorating.
51
61
  :representable => true, # form: Class must be treated as a typed property.
@@ -58,13 +68,7 @@ module Reform
58
68
  def create_accessor(name)
59
69
  handle_reserved_names(name)
60
70
 
61
- # Make a module that contains these very accessors, then include it
62
- # so they can be overridden but still are callable with super.
63
- accessors = Module.new do
64
- extend Forwardable # DISCUSS: do we really need Forwardable here?
65
- delegate [name, "#{name}="] => :fields
66
- end
67
- include accessors
71
+ delegates :fields, name, "#{name}=" # Uber::Delegates
68
72
  end
69
73
 
70
74
  def handle_reserved_names(name)
@@ -79,6 +83,7 @@ module Reform
79
83
  include ActiveModel::Validations
80
84
 
81
85
 
86
+
82
87
  attr_accessor :model
83
88
 
84
89
  require 'reform/contract/setup'
@@ -100,6 +105,10 @@ module Reform
100
105
  self.class.representer_class
101
106
  end
102
107
 
108
+ def self.register_feature(mod)
109
+ features[mod] = true
110
+ end
111
+
103
112
  alias_method :aliased_model, :model
104
113
 
105
114
 
@@ -2,6 +2,7 @@ require 'ostruct'
2
2
 
3
3
  require 'reform/contract'
4
4
  require 'reform/composition'
5
+ require 'reform/form/module'
5
6
 
6
7
  module Reform
7
8
  class Form < Contract
@@ -24,5 +25,9 @@ module Reform
24
25
  # TODO: cache the Expose.from class!
25
26
  Reform::Expose.from(mapper).new(:model => model)
26
27
  end
28
+
29
+
30
+ require 'reform/form/scalar'
31
+ extend Scalar::Property # experimental feature!
27
32
  end
28
33
  end
@@ -1,9 +1,11 @@
1
+ require 'reform/form/active_model/model_validations'
2
+
1
3
  module Reform::Form::ActiveModel
2
4
  module FormBuilderMethods # TODO: rename to FormBuilderCompat.
3
5
  def self.included(base)
4
6
  base.class_eval do
5
7
  extend ClassMethods # ::model_name
6
- features << FormBuilderMethods
8
+ register_feature FormBuilderMethods
7
9
  end
8
10
  end
9
11
 
@@ -48,9 +50,9 @@ module Reform::Form::ActiveModel
48
50
  def self.included(base)
49
51
  base.class_eval do
50
52
  extend ClassMethods
51
- features << ActiveModel
53
+ register_feature ActiveModel
52
54
 
53
- delegate [:persisted?, :to_key, :to_param, :id] => :model
55
+ delegates :model, *[:persisted?, :to_key, :to_param, :id] # Uber::Delegates
54
56
 
55
57
  def to_model # this is called somewhere in FormBuilder and ActionController.
56
58
  self
@@ -0,0 +1,98 @@
1
+ module Reform::Form::ActiveModel
2
+ module ModelValidations
3
+ # TODO: extract Composition behaviour.
4
+ # reduce code in Mapping.
5
+
6
+ class ValidationCopier
7
+
8
+ def self.copy(form_class, mapping, models)
9
+ if models.is_a?(Hash)
10
+ models.each do |model_name, model|
11
+ new(form_class, mapping, model, model_name).copy
12
+ end
13
+ else
14
+ new(form_class, mapping, models).copy
15
+ end
16
+ end
17
+
18
+ def initialize(form_class, mapping, model, model_name=nil)
19
+ @form_class = form_class
20
+ @mapping = mapping
21
+ @model = model
22
+ @model_name = model_name
23
+ end
24
+
25
+ def copy
26
+ @model.validators.each(&method(:add_validator))
27
+ end
28
+
29
+ private
30
+
31
+ def add_validator(validator)
32
+ attributes = inverse_map_attributes(validator.attributes)
33
+ if attributes.any?
34
+ @form_class.validates(*attributes, {validator.kind => validator.options})
35
+ end
36
+ end
37
+
38
+ def inverse_map_attributes(attributes)
39
+ @mapping.inverse_image(create_attributes(attributes))
40
+ end
41
+
42
+ def create_attributes(attributes)
43
+ attributes.map do |attribute|
44
+ [@model_name, attribute].compact
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+ class Mapping
51
+ def self.from_representable_attrs(attrs)
52
+ new.tap do |mapping|
53
+ attrs.each do |dfn|
54
+ from = dfn.name.to_sym
55
+ to = [dfn[:on], (dfn[:private_name] || dfn.name)].compact.map(&:to_sym)
56
+ mapping.add(from, to)
57
+ end
58
+ end
59
+ end
60
+
61
+ def initialize
62
+ @forward_map = {}
63
+ @inverse_map = {}
64
+ end
65
+
66
+ # from is a symbol attribute
67
+ # to is an 1 or 2 element array, depending on whether the attribute is 'namespaced', as it is with composite forms.
68
+ # eg, add(:phone_number, [:person, :phone])
69
+ def add(from, to)
70
+ raise 'Mapping is not one-to-one' if @forward_map.has_key?(from) || @inverse_map.has_key?(to)
71
+ @forward_map[from] = to
72
+ @inverse_map[to] = from
73
+ end
74
+
75
+ def forward_image(attrs)
76
+ @forward_map.values_at(*attrs).compact
77
+ end
78
+
79
+ def forward(attr)
80
+ @forward_map[attr]
81
+ end
82
+
83
+ def inverse_image(attrs)
84
+ @inverse_map.values_at(*attrs).compact
85
+ end
86
+
87
+ def inverse(attr)
88
+ @inverse_map[attr]
89
+ end
90
+
91
+ end
92
+
93
+ def copy_validations_from(models)
94
+ ValidationCopier.copy(self, Mapping.from_representable_attrs(representer_class.representable_attrs), models)
95
+ end
96
+
97
+ end
98
+ end
@@ -1,6 +1,7 @@
1
1
  module Reform::Form::ActiveRecord
2
2
  def self.included(base)
3
3
  base.class_eval do
4
+ register_feature Reform::Form::ActiveRecord
4
5
  include Reform::Form::ActiveModel
5
6
  extend ClassMethods
6
7
  end
@@ -37,7 +38,13 @@ module Reform::Form::ActiveRecord
37
38
  def model_for_property(name)
38
39
  return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.
39
40
 
40
- model_name = mapper.representable_attrs[name][:on]
41
+ model_name = mapper.representable_attrs.get(name)[:on]
41
42
  model[model_name]
42
43
  end
44
+
45
+ # Delegate column for attribute to the model to support simple_form's
46
+ # attribute type interrogation.
47
+ def column_for_attribute(name)
48
+ model_for_property(name).column_for_attribute(name)
49
+ end
43
50
  end
@@ -1,15 +1,15 @@
1
- require 'representable/decorator/coercion'
1
+ require 'representable/coercion'
2
2
 
3
3
  module Reform::Form::Coercion
4
4
  def self.included(base)
5
5
  base.extend(ClassMethods)
6
- base.features << self
6
+ base.send(:register_feature, self)
7
7
  end
8
8
 
9
9
  module ClassMethods
10
- def representer_class
10
+ def representer_class # TODO: check out how we can utilise Config#features.
11
11
  super.class_eval do
12
- include Representable::Decorator::Coercion unless self < Representable::Decorator::Coercion # DISCUSS: include it once. why do we have to check this?
12
+ include Representable::Coercion
13
13
  self
14
14
  end
15
15
  end
@@ -16,12 +16,6 @@ module Reform::Form::Composition
16
16
  @model_class ||= Reform::Composition.from(representer_class)
17
17
  end
18
18
 
19
- def property(name, options={})
20
- super.tap do |definition|
21
- handle_deprecated_model_accessor(options[:on]) unless options[:skip_accessors] # TODO: remove in 1.2.
22
- end
23
- end
24
-
25
19
  # Same as ActiveModel::model but allows you to define the main model in the composition
26
20
  # using +:on+.
27
21
  #
@@ -32,8 +26,6 @@ module Reform::Form::Composition
32
26
 
33
27
  composition_model = options[:on] || main_model
34
28
 
35
- handle_deprecated_model_accessor(composition_model) unless options[:skip_accessors] # TODO: remove in 1.2.
36
-
37
29
  # FIXME: this should just delegate to :model as in FB, and the comp would take care of it internally.
38
30
  [:persisted?, :to_key, :to_param].each do |method|
39
31
  define_method method do
@@ -41,16 +33,7 @@ module Reform::Form::Composition
41
33
  end
42
34
  end
43
35
 
44
- alias_method main_model, composition_model # #hit => model.song. # TODO: remove in 1.2.
45
- end
46
-
47
- private
48
- def handle_deprecated_model_accessor(name, aliased=name)
49
- define_method name do # form.band -> composition.band
50
- warn %{[Reform] Deprecation WARNING: When using Composition, you may not call Form##{name} anymore to access the contained model. Please use Form#model[:#{name}] and have a lovely day!}
51
-
52
- @model[name]
53
- end
36
+ self
54
37
  end
55
38
  end
56
39
 
@@ -64,7 +47,7 @@ module Reform::Form::Composition
64
47
  end
65
48
 
66
49
  def to_hash(*args)
67
- mapper.new(fields).to_hash(*args)
50
+ mapper.new(fields).to_hash(*args) # do not map names, yet. this happens in #to_nested_hash
68
51
  end
69
52
 
70
53
  private