reform 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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