reform 1.2.6 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -1
- data/CHANGES.md +14 -0
- data/Gemfile +3 -2
- data/README.md +225 -283
- data/Rakefile +27 -0
- data/TODO.md +12 -0
- data/database.sqlite3 +0 -0
- data/gemfiles/Gemfile.rails-3.0 +1 -0
- data/gemfiles/Gemfile.rails-3.1 +1 -0
- data/gemfiles/Gemfile.rails-3.2 +1 -0
- data/gemfiles/Gemfile.rails-4.0 +1 -0
- data/lib/reform.rb +0 -1
- data/lib/reform/contract.rb +64 -170
- data/lib/reform/contract/validate.rb +10 -13
- data/lib/reform/form.rb +74 -19
- data/lib/reform/form/active_model.rb +19 -14
- data/lib/reform/form/coercion.rb +1 -13
- data/lib/reform/form/composition.rb +2 -24
- data/lib/reform/form/multi_parameter_attributes.rb +43 -62
- data/lib/reform/form/populator.rb +85 -0
- data/lib/reform/form/prepopulate.rb +13 -43
- data/lib/reform/form/validate.rb +29 -90
- data/lib/reform/form/validation/unique_validator.rb +13 -0
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +7 -7
- data/test/active_model_test.rb +43 -0
- data/test/changed_test.rb +23 -51
- data/test/coercion_test.rb +1 -7
- data/test/composition_test.rb +128 -34
- data/test/contract_test.rb +27 -86
- data/test/feature_test.rb +43 -6
- data/test/fields_test.rb +2 -12
- data/test/form_builder_test.rb +28 -25
- data/test/form_option_test.rb +19 -0
- data/test/from_test.rb +0 -75
- data/test/inherit_test.rb +178 -117
- data/test/model_reflections_test.rb +1 -1
- data/test/populate_test.rb +226 -0
- data/test/prepopulator_test.rb +112 -0
- data/test/readable_test.rb +2 -4
- data/test/save_test.rb +56 -112
- data/test/setup_test.rb +48 -0
- data/test/skip_if_test.rb +5 -2
- data/test/skip_setter_and_getter_test.rb +54 -0
- data/test/test_helper.rb +3 -1
- data/test/uniqueness_test.rb +41 -0
- data/test/validate_test.rb +325 -289
- data/test/virtual_test.rb +1 -3
- data/test/writeable_test.rb +3 -4
- metadata +35 -39
- data/lib/reform/composition.rb +0 -63
- data/lib/reform/contract/setup.rb +0 -50
- data/lib/reform/form/changed.rb +0 -9
- data/lib/reform/form/sync.rb +0 -116
- data/lib/reform/representer.rb +0 -84
- data/test/empty_test.rb +0 -58
- data/test/form_composition_test.rb +0 -145
- data/test/nested_form_test.rb +0 -197
- data/test/prepopulate_test.rb +0 -85
- data/test/sync_option_test.rb +0 -83
- data/test/sync_test.rb +0 -56
data/Rakefile
CHANGED
@@ -5,6 +5,33 @@ task :default => [:test]
|
|
5
5
|
Rake::TestTask.new(:test) do |test|
|
6
6
|
test.libs << 'test'
|
7
7
|
test.test_files = FileList['test/*_test.rb']
|
8
|
+
|
9
|
+
test.test_files = ["test/changed_test.rb",
|
10
|
+
"test/coercion_test.rb",
|
11
|
+
"test/feature_test.rb",
|
12
|
+
|
13
|
+
"test/contract_test.rb",
|
14
|
+
|
15
|
+
"test/populate_test.rb", "test/prepopulator_test.rb",
|
16
|
+
|
17
|
+
"test/readable_test.rb","test/setup_test.rb","test/skip_if_test.rb",
|
18
|
+
|
19
|
+
"test/validate_test.rb", "test/save_test.rb",
|
20
|
+
|
21
|
+
"test/writeable_test.rb","test/virtual_test.rb",
|
22
|
+
|
23
|
+
"test/form_builder_test.rb", "test/active_model_test.rb",
|
24
|
+
|
25
|
+
"test/readonly_test.rb",
|
26
|
+
"test/inherit_test.rb",
|
27
|
+
"test/uniqueness_test.rb",
|
28
|
+
"test/from_test.rb",
|
29
|
+
"test/composition_test.rb",
|
30
|
+
"test/form_option_test.rb"
|
31
|
+
]
|
32
|
+
|
33
|
+
|
34
|
+
|
8
35
|
test.verbose = true
|
9
36
|
end
|
10
37
|
|
data/TODO.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# 2.0
|
2
|
+
|
3
|
+
* make Coercible optional (include it to activate)
|
4
|
+
* all options Uber:::Value with :method support
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
# NOTES
|
9
|
+
* use the same test setup everywhere (album -> songs -> composer)
|
10
|
+
* copy things in tests
|
11
|
+
* one test file per "feature": sync_test, sync_option_test.
|
12
|
+
|
1
13
|
* fields is a Twin and sorts out all the changed? stuff.
|
2
14
|
* virtual: don't read dont write
|
3
15
|
* empty dont read, but write
|
data/database.sqlite3
CHANGED
Binary file
|
data/gemfiles/Gemfile.rails-3.0
CHANGED
data/gemfiles/Gemfile.rails-3.1
CHANGED
data/gemfiles/Gemfile.rails-3.2
CHANGED
data/gemfiles/Gemfile.rails-4.0
CHANGED
data/lib/reform.rb
CHANGED
data/lib/reform/contract.rb
CHANGED
@@ -1,172 +1,106 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'uber/inheritable_attr'
|
3
3
|
require 'uber/delegates'
|
4
|
-
require 'ostruct'
|
5
|
-
|
6
|
-
require 'reform/representer'
|
7
4
|
|
8
5
|
module Reform
|
9
6
|
# Gives you a DSL for defining the object structure and its validations.
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
inheritable_attr :representer_class
|
16
|
-
self.representer_class = Reform::Representer.for(:form_class => self) # only happens in Contract/Form.
|
17
|
-
# this should be the only mechanism to inherit, features should be stored in this as well.
|
18
|
-
|
19
|
-
|
20
|
-
# each contract keeps track of its features and passes them onto its local representer_class.
|
21
|
-
# gets inherited, features get automatically included into inline representer.
|
22
|
-
# TODO: the representer class should handle that, e.g. in options (deep-clone when inheriting.)
|
23
|
-
inheritable_attr :features
|
24
|
-
self.features = {}
|
25
|
-
|
26
|
-
|
27
|
-
RESERVED_METHODS = [:model, :aliased_model, :fields, :mapper] # TODO: refactor that so we don't need that.
|
28
|
-
|
29
|
-
|
30
|
-
module PropertyMethods
|
31
|
-
def property(name, options={}, &block)
|
32
|
-
deprecate_as!(options)
|
33
|
-
options[:private_name] = options.delete(:from)
|
34
|
-
options[:coercion_type] = options.delete(:type)
|
35
|
-
options[:features] ||= []
|
36
|
-
options[:features] += features.keys if block_given?
|
37
|
-
options[:pass_options] = true
|
38
|
-
|
39
|
-
# readable and writeable is true as it's not == false
|
7
|
+
require "disposable/twin"
|
8
|
+
require "disposable/twin/setup"
|
9
|
+
class Contract < Disposable::Twin
|
10
|
+
require "disposable/twin/composition" # Expose.
|
11
|
+
include Expose
|
40
12
|
|
41
|
-
|
42
|
-
|
43
|
-
options[:_readable] = false
|
44
|
-
options[:_writeable] = false
|
45
|
-
else
|
46
|
-
options[:_readable] = options.delete(:readable)
|
47
|
-
options[:_writeable] = options.delete(:writeable)
|
48
|
-
end
|
49
|
-
|
50
|
-
else # TODO: remove me in 2.0.
|
51
|
-
deprecate_virtual_and_empty!(options)
|
52
|
-
end
|
53
|
-
|
54
|
-
validates(name, options.delete(:validates).dup) if options[:validates]
|
55
|
-
|
56
|
-
definition = representer_class.property(name, options, &block)
|
57
|
-
setup_form_definition(definition) if block_given? or options[:form]
|
58
|
-
|
59
|
-
create_accessor(name)
|
60
|
-
definition
|
61
|
-
end
|
62
|
-
|
63
|
-
def properties(*args)
|
64
|
-
options = args.extract_options!
|
65
|
-
|
66
|
-
if args.first.is_a? Array # TODO: REMOVE in 2.0.
|
67
|
-
warn "[Reform] Deprecation: Please pass a list of names instead of array to ::properties, like `properties :title, :id`."
|
68
|
-
args = args.first
|
69
|
-
end
|
70
|
-
args.each { |name| property(name, options.dup) }
|
71
|
-
end
|
13
|
+
feature Setup
|
14
|
+
feature Setup::SkipSetter
|
72
15
|
|
73
|
-
|
74
|
-
options[:collection] = true
|
16
|
+
extend Uber::Delegates
|
75
17
|
|
76
|
-
|
18
|
+
representer_class.instance_eval do
|
19
|
+
def default_inline_class
|
20
|
+
Contract
|
77
21
|
end
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
:pass_options => true, # new style of passing args
|
84
|
-
:prepare => lambda { |form, args| form }, # always just return the form without decorating.
|
85
|
-
:representable => true, # form: Class must be treated as a typed property.
|
86
|
-
}
|
87
|
-
|
88
|
-
definition.merge!(options)
|
22
|
+
end
|
23
|
+
# FIXME: THIS sucks because we're building two representers.
|
24
|
+
representer_class.instance_eval do
|
25
|
+
def default_inline_class
|
26
|
+
Contract
|
89
27
|
end
|
28
|
+
end
|
90
29
|
|
91
|
-
|
92
|
-
|
93
|
-
def create_accessor(name)
|
94
|
-
handle_reserved_names(name)
|
30
|
+
def self.property(name, options={}, &block)
|
31
|
+
options.merge!(pass_options: true)
|
95
32
|
|
96
|
-
|
33
|
+
if twin = options.delete(:form)
|
34
|
+
options[:twin] = twin
|
97
35
|
end
|
98
36
|
|
99
|
-
|
100
|
-
raise "[Reform] the property name '#{name}' is reserved, please consider something else using :as." if RESERVED_METHODS.include?(name)
|
101
|
-
end
|
37
|
+
super
|
102
38
|
end
|
103
|
-
extend PropertyMethods
|
104
39
|
|
40
|
+
# FIXME: test me.
|
41
|
+
def self.properties(*args)
|
42
|
+
options = args.extract_options!
|
43
|
+
args.each { |name| property(name, options.dup) }
|
44
|
+
end
|
105
45
|
|
106
46
|
# FIXME: make AM optional.
|
107
47
|
require 'active_model'
|
108
48
|
include ActiveModel::Validations
|
109
49
|
|
110
|
-
|
111
|
-
|
112
|
-
attr_accessor :model
|
113
|
-
|
114
|
-
require 'reform/contract/setup'
|
115
|
-
include Setup
|
116
|
-
|
117
|
-
def self.representers # keeps all transformation representers for one class.
|
118
|
-
@representers ||= {}
|
119
|
-
end
|
120
|
-
|
121
|
-
def self.representer(name=nil, options={}, &block)
|
122
|
-
return representer_class.each(&block) if name == nil
|
123
|
-
return representers[name] if representers[name] # don't run block as this representer is already setup for this form class.
|
124
|
-
|
125
|
-
only_forms = options[:all] ? false : true
|
126
|
-
base = options[:superclass] || representer_class
|
127
|
-
|
128
|
-
representers[name] = Class.new(base).each(only_forms, &block) # let user modify representer.
|
129
|
-
end
|
130
|
-
|
131
50
|
require 'reform/contract/validate'
|
132
|
-
include Validate
|
51
|
+
include Reform::Contract::Validate
|
133
52
|
|
134
53
|
def errors # FIXME: this is needed for Rails 3.0 compatibility.
|
135
54
|
@errors ||= Errors.new(self)
|
136
55
|
end
|
137
56
|
|
138
|
-
|
139
57
|
private
|
140
|
-
attr_accessor :fields
|
141
58
|
attr_writer :errors # only used in top form. (is that true?)
|
142
59
|
|
143
|
-
|
144
|
-
|
60
|
+
# DISCUSS: can we achieve that somehow via features in build_inline?
|
61
|
+
# TODO: check out if that is needed with Lotus::Validations and make it a AM feature.
|
62
|
+
def self.process_inline!(mod, definition)
|
63
|
+
_name = definition.name
|
64
|
+
mod.instance_eval do
|
65
|
+
@_name = _name.singularize.camelize
|
66
|
+
def name # this adds Form::name for AM::Validations and I18N. i know it's retarded.
|
67
|
+
# something weird happens here: somewhere in Rails, this creates a constant (e.g. User). if this name doesn't represent a valid
|
68
|
+
# constant, the reloading in dev will fail with weird messages. i'm not sure if we should just get rid of Rails validations etc.
|
69
|
+
# or if i should look into this?
|
70
|
+
@_name
|
71
|
+
end
|
72
|
+
end
|
145
73
|
end
|
146
74
|
|
147
|
-
def self.deprecate_as!(options) # TODO: remove me in 2.0.
|
148
|
-
return unless as = options.delete(:as)
|
149
|
-
options[:from] = as
|
150
|
-
warn "[Reform] The :as options got renamed to :from. See https://github.com/apotonick/reform/wiki/Migration-Guide and have a nice day."
|
151
|
-
end
|
152
75
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
options[:_writeable] = false
|
76
|
+
# DISCUSS: separate file?
|
77
|
+
module Readonly
|
78
|
+
def readonly?(name)
|
79
|
+
options_for(name)[:writeable] == false
|
158
80
|
end
|
159
|
-
|
160
|
-
|
161
|
-
warn "[Reform] The :empty option has changed! Check https://github.com/apotonick/reform/wiki/Migration-Guide and have a good day."
|
162
|
-
options[:_readable] = false
|
163
|
-
options[:_writeable] = false
|
81
|
+
def options_for(name)
|
82
|
+
self.class.options_for(name)
|
164
83
|
end
|
165
84
|
end
|
85
|
+
def self.options_for(name)
|
86
|
+
representer_class.representable_attrs.get(name)
|
87
|
+
end
|
88
|
+
include Readonly
|
89
|
+
|
166
90
|
|
167
|
-
def self.
|
168
|
-
|
91
|
+
def self.clone # TODO: test. THIS IS ONLY FOR Trailblazer when contract gets cloned in suboperation.
|
92
|
+
Class.new(self)
|
169
93
|
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class Contract_ # DISCUSS: make class?
|
97
|
+
extend Uber::InheritableAttr
|
98
|
+
|
99
|
+
RESERVED_METHODS = [:model] # TODO: refactor that so we don't need that.
|
100
|
+
def handle_reserved_names(name)
|
101
|
+
raise "[Reform] the property name '#{name}' is reserved, please consider something else using :as." if RESERVED_METHODS.include?(name)
|
102
|
+
end
|
103
|
+
|
170
104
|
|
171
105
|
# allows including representers from Representable, Roar or disposable.
|
172
106
|
def self.inherit_module!(representer) # called from Representable::included.
|
@@ -182,48 +116,8 @@ module Reform
|
|
182
116
|
end
|
183
117
|
end
|
184
118
|
|
185
|
-
def self.clone
|
186
|
-
Class.new(self)
|
187
|
-
end
|
188
|
-
|
189
119
|
require 'reform/schema'
|
190
120
|
extend Schema
|
191
|
-
|
192
|
-
alias_method :aliased_model, :model
|
193
|
-
|
194
|
-
module Readonly
|
195
|
-
def readonly?(name)
|
196
|
-
options_for(name)[:writeable] == false
|
197
|
-
end
|
198
|
-
|
199
|
-
def options_for(name)
|
200
|
-
self.class.representer_class.representable_attrs.get(name)
|
201
|
-
end
|
202
|
-
end
|
203
|
-
include Readonly
|
204
|
-
|
205
|
-
# TODO: remove me in 2.0.
|
206
|
-
module Reform20Switch
|
207
|
-
def self.included(base)
|
208
|
-
base.register_feature(Reform20Switch)
|
209
|
-
end
|
210
|
-
end
|
211
|
-
def self.reform_2_0!
|
212
|
-
include Reform20Switch
|
213
|
-
end
|
214
|
-
def self.reform_2_0
|
215
|
-
features[Reform20Switch]
|
216
|
-
end
|
217
|
-
|
218
|
-
|
219
|
-
# Keeps values of the form fields. What's in here is to be displayed in the browser!
|
220
|
-
# we need this intermediate object to display both "original values" and new input from the form after submitting.
|
221
|
-
class Fields < OpenStruct
|
222
|
-
def initialize(properties, values={})
|
223
|
-
fields = properties.inject({}) { |hsh, attr| hsh.merge!(attr => nil) }
|
224
|
-
super(fields.merge!(values)) # TODO: stringify value keys!
|
225
|
-
end
|
226
|
-
end # Fields
|
227
121
|
end
|
228
122
|
end
|
229
123
|
|
@@ -12,8 +12,7 @@ module Reform::Contract::Validate
|
|
12
12
|
def validate!(options)
|
13
13
|
prefix = options[:prefix]
|
14
14
|
|
15
|
-
# call valid? recursively and collect nested errors.
|
16
|
-
valid_representer.new(fields).to_hash(options) # TODO: only include nested forms here.
|
15
|
+
validate_nested!(options) # call valid? recursively and collect nested errors.
|
17
16
|
|
18
17
|
valid? # this validates on <Fields> using AM::Validations, currently.
|
19
18
|
|
@@ -23,17 +22,15 @@ module Reform::Contract::Validate
|
|
23
22
|
private
|
24
23
|
|
25
24
|
# runs form.validate! on all nested forms
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
}
|
36
|
-
)
|
25
|
+
def validate_nested!(options)
|
26
|
+
schema.each(twin: true) do |dfn|
|
27
|
+
property_options = options.dup
|
28
|
+
|
29
|
+
property_options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
|
30
|
+
property_options[:prefix] << dfn.name
|
31
|
+
|
32
|
+
# recursively call valid? on nested form.
|
33
|
+
Disposable::Twin::PropertyProcessor.new(dfn, self).() { |form| form.validate!(property_options) }
|
37
34
|
end
|
38
35
|
end
|
39
36
|
end
|
data/lib/reform/form.rb
CHANGED
@@ -1,33 +1,88 @@
|
|
1
1
|
module Reform
|
2
2
|
class Form < Contract
|
3
|
-
|
3
|
+
representer_class.instance_eval do
|
4
|
+
def default_inline_class
|
5
|
+
Form
|
6
|
+
end
|
7
|
+
end
|
4
8
|
|
5
9
|
require "reform/form/validate"
|
6
10
|
include Validate # extend Contract#validate with additional behaviour.
|
7
|
-
require "reform/form/sync"
|
8
|
-
include Sync
|
9
|
-
require "reform/form/save"
|
10
|
-
include Save
|
11
|
-
require "reform/form/prepopulate"
|
12
|
-
include Prepopulate
|
13
11
|
|
14
|
-
require "reform/form/
|
15
|
-
|
12
|
+
require "reform/form/populator"
|
13
|
+
|
14
|
+
module Property
|
15
|
+
# add macro logic, e.g. for :populator.
|
16
|
+
def property(name, options={}, &block)
|
17
|
+
if options.delete(:virtual)
|
18
|
+
options[:writeable] = options[:readable] = false # DISCUSS: isn't that like an #option in Twin?
|
19
|
+
end
|
20
|
+
|
21
|
+
if deserializer = options[:deserializer] # this means someone is explicitly specifying :deserializer.
|
22
|
+
options[:deserializer] = Representable::Cloneable::Hash[deserializer]
|
23
|
+
end
|
24
|
+
|
25
|
+
definition = super # let representable sort out inheriting of properties, and so on.
|
26
|
+
definition.merge!(deserializer: Representable::Cloneable::Hash.new) unless definition[:deserializer] # always keep :deserializer per property.
|
27
|
+
|
28
|
+
|
29
|
+
deserializer_options = definition[:deserializer]
|
30
|
+
|
31
|
+
# TODO: make this pluggable.
|
32
|
+
# DISCUSS: Populators should be a representable concept?
|
33
|
+
|
34
|
+
# Populators
|
35
|
+
# * they assign created data, no :setter (hence the name).
|
36
|
+
# * they (ab)use :instance, this is why they need to return a twin form.
|
37
|
+
# * they are only used in the deserializer.
|
38
|
+
|
16
39
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
40
|
+
|
41
|
+
if populator = options.delete(:populate_if_empty)
|
42
|
+
deserializer_options.merge!({instance: Populator::IfEmpty.new(populator)})
|
43
|
+
deserializer_options.merge!({setter: nil})
|
44
|
+
elsif populator = options.delete(:populator)
|
45
|
+
deserializer_options.merge!({instance: Populator.new(populator)})
|
46
|
+
deserializer_options.merge!({setter: nil}) #if options[:collection] # collections don't need to get re-assigned, they don't change.
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# TODO: shouldn't that go into validate?
|
51
|
+
if proc = options.delete(:skip_if)
|
52
|
+
proc = Reform::Form::Validate::Skip::AllBlank.new if proc == :all_blank
|
53
|
+
|
54
|
+
deserializer_options.merge!(skip_parse: proc)
|
55
|
+
end
|
56
|
+
|
57
|
+
# default:
|
58
|
+
# add Sync populator to nested forms.
|
59
|
+
# FIXME: this is, of course, ridiculous and needs a better structuring.
|
60
|
+
if (deserializer_options == {} || deserializer_options.keys == [:skip_parse]) && block_given? && !options[:inherit] # FIXME: hmm. not a fan of this: only add when no other option given?
|
61
|
+
deserializer_options.merge!(instance: Populator::Sync.new(nil), setter: nil)
|
62
|
+
end
|
63
|
+
|
64
|
+
# per default, everything should be writeable for the deserializer (we're only writing on the form). however, allow turning it off.
|
65
|
+
deserializer_options.merge!(writeable: true) unless deserializer_options.has_key?(:writeable)
|
66
|
+
|
67
|
+
definition
|
68
|
+
end
|
21
69
|
end
|
70
|
+
extend Property
|
71
|
+
|
72
|
+
|
73
|
+
require "reform/form/multi_parameter_attributes"
|
22
74
|
|
75
|
+
require "disposable/twin/changed"
|
76
|
+
feature Disposable::Twin::Changed
|
23
77
|
|
24
|
-
require "
|
25
|
-
|
78
|
+
require "disposable/twin/sync"
|
79
|
+
feature Disposable::Twin::Sync
|
80
|
+
feature Disposable::Twin::Sync::SkipGetter
|
26
81
|
|
82
|
+
require "disposable/twin/save"
|
83
|
+
feature Disposable::Twin::Save
|
27
84
|
|
28
|
-
|
29
|
-
|
30
|
-
register_feature Changed
|
31
|
-
include Changed
|
85
|
+
require "reform/form/prepopulate"
|
86
|
+
include Prepopulate
|
32
87
|
end
|
33
88
|
end
|