formality 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/formality.rb +284 -0
  2. metadata +11 -10
@@ -0,0 +1,284 @@
1
+ require "set"
2
+
3
+ require "active_model"
4
+ require "active_support"
5
+ require "active_support/hash_with_indifferent_access"
6
+
7
+ module Formality
8
+ VERSION = "0.0.2"
9
+
10
+ extend ActiveSupport::Concern
11
+
12
+ #
13
+ # ActiveModel Compliance
14
+ # ======================
15
+ #
16
+
17
+ # Gives classes including Formality the standard validations
18
+ # framework plus the :valid? and :invalid? methods
19
+ include ActiveModel::Validations
20
+
21
+ module ClassMethods
22
+ # :model_name must be defined on the class and return a
23
+ # String with various convenience methods. ActiveModel::Name
24
+ # gives us that.
25
+ #
26
+ # By default, :model_name uses the name of the Form class.
27
+ def model_name
28
+ @__model_name ||= ActiveModel::Name.new(self)
29
+ end
30
+ end
31
+
32
+ # More ActiveModel compliance shenanigans.
33
+ def to_key; end
34
+ def to_param; end
35
+ def to_partial_path; "" end
36
+
37
+ # When Formality is included into a class, it defines an
38
+ # attr_accessor for :id. This in combination with the
39
+ # definition of :persisted? helps Formality forms work
40
+ # cleanly with :form_for.
41
+ included do
42
+ attr_accessor :id
43
+ end
44
+
45
+ # :form_for calls :persisted? on the object it receives
46
+ # to determine whether to :post or :put.
47
+ #
48
+ # We assume we're persisted (i.e. editing an object) if
49
+ # we have an id.
50
+ def persisted?
51
+ id.present?
52
+ end
53
+
54
+ #
55
+ # Attributes
56
+ # ==========
57
+ #
58
+
59
+ module ClassMethods
60
+ # Declare an attribute.
61
+ #
62
+ # Defines a reader and writer. Accepts a :default options
63
+ # for the default value of the attribute.
64
+ def attribute(name, options={})
65
+ attributes << name.to_s
66
+ define_reader(name, options[:default])
67
+ attr_writer name
68
+ end
69
+
70
+ # A Set of attribute names, stored on the form class.
71
+ def attributes
72
+ @__attributes ||= Set.new
73
+ end
74
+
75
+ # A convenience class method for creating and assigning
76
+ # a Hash to a form.
77
+ def assign(attrs)
78
+ new.assign(attrs)
79
+ end
80
+
81
+ private
82
+
83
+ # Defines an attribute reader with an
84
+ # optional default value.
85
+ def define_reader(name, default=nil)
86
+ class_eval <<-reader
87
+ def #{name}
88
+ @#{name} ||= #{default.inspect}
89
+ end
90
+ reader
91
+ end
92
+ end
93
+
94
+ # Returns an Array of attribute names (Strings).
95
+ def attribute_names
96
+ self.class.attributes.to_a
97
+ end
98
+
99
+ # Returns a HashWithIndifferentAccess of all the
100
+ # defined attributes and their values.
101
+ def attributes
102
+ hash = ActiveSupport::HashWithIndifferentAccess.new
103
+ attribute_names.each_with_object({}) do |name|
104
+ hash[name] = send(name)
105
+ end
106
+ hash
107
+ end
108
+
109
+ # Assigns a hash of attributes to the form. Only assigns
110
+ # values if the key for that value is a declared
111
+ # attribute. It silently ignores non-declared keys.
112
+ def assign(new_attributes)
113
+ new_attributes.each do |name, value|
114
+ next unless attribute?(name)
115
+ send("#{name}=", value)
116
+ end
117
+ self.id = new_attributes[:id]
118
+ self
119
+ end
120
+
121
+ # Returns a Boolean that answers the question: Is this `name`
122
+ # a declared attribute?
123
+ def attribute?(name)
124
+ attribute_names.include?(name.to_s)
125
+ end
126
+
127
+ #
128
+ # Working with models
129
+ # ===================
130
+ #
131
+
132
+ module ClassMethods
133
+ # Declare the model that this form object represents.
134
+ #
135
+ # Purely a convenience so that you don't have to
136
+ # specify the :url parameter in your :form_for calls.
137
+ def model(name_sym)
138
+ model_klass = name_sym.to_s.capitalize.constantize
139
+ @__model_name = ActiveModel::Name.new(model_klass)
140
+ end
141
+
142
+ # Build a form object from an existing model.
143
+ #
144
+ # If nested forms were declared with the
145
+ # :from_model_attribute option, it will also
146
+ # build the nested form object(s).
147
+ def from_model(model)
148
+ new.tap do |form|
149
+ form.id = model.id
150
+ form.assign(model.attributes)
151
+ nested_forms.each do |nested|
152
+ form.send("#{nested}=", model)
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ #
159
+ # Some Controller Sugar
160
+ # =====================
161
+ #
162
+ # class FooController < ApplicationController
163
+ # def create
164
+ # form = FooForm.new.assign(params[:foo_form])
165
+ #
166
+ # form.valid do
167
+ # current_user.foos.create(@form.attributes)
168
+ # end
169
+ #
170
+ # form.invalid do
171
+ # @form = form
172
+ # render :new
173
+ # end
174
+ # end
175
+ # end
176
+ #
177
+
178
+ # Yields to its block if the form is valid.
179
+ def valid; yield if valid? end
180
+
181
+ # Same as :valid, but in reverse: only yields to
182
+ # the block if the form is invalid.
183
+ def invalid; yield if invalid? end
184
+
185
+ #
186
+ # Nesting
187
+ # =======
188
+ #
189
+ # Allows forms to have forms nested within them that
190
+ # work nicely with Rails' :fields_for method.
191
+ #
192
+ # Validations are called on nested forms, so that if
193
+ # any nested form is invalid, so is the parent.
194
+ #
195
+
196
+ module ClassMethods
197
+ # Singular Nesting.
198
+ def nest_one(child, options={})
199
+ add_nested_form(child)
200
+ define_nested_form_one(child, options)
201
+ end
202
+
203
+ # Plural nesting.
204
+ #
205
+ # Works just like :nest_one, except it works for
206
+ # an Array of nested forms.
207
+ def nest_many(children, options={})
208
+ add_nested_form(children)
209
+ define_nested_form_many(children, options)
210
+ end
211
+
212
+ # Keep track of what forms we've nested.
213
+ def nested_forms
214
+ @__nested_forms ||= Set.new
215
+ end
216
+
217
+ private
218
+
219
+ def add_nested_form(nested)
220
+ attributes << "#{nested}_attributes"
221
+ nested_forms << nested
222
+ end
223
+
224
+ # Define the accessors for a singular nested form.
225
+ def define_nested_form_one(name, options={})
226
+ define_reader(name)
227
+ from_model_attribute = options[:from_model_attribute]
228
+ class_eval <<-one
229
+ def #{name}_attributes
230
+ self.#{name} ? @#{name}.attributes : nil
231
+ end
232
+
233
+ def #{name}_attributes=(attrs)
234
+ form_klass = "#{name}".classify.constantize
235
+ @#{name} = form_klass.new.assign(attrs)
236
+ end
237
+
238
+ def #{name}=(model)
239
+ return unless #{from_model_attribute.inspect}
240
+ nested_model = model.send(#{from_model_attribute.inspect})
241
+ self.#{name}_attributes = nested_model.attributes
242
+ end
243
+ one
244
+ end
245
+
246
+ # Define the accessors for a plural nested form.
247
+ def define_nested_form_many(name, options={})
248
+ define_reader(name, [])
249
+ from_model_attribute = options[:from_model_attribute]
250
+ class_eval <<-many
251
+ def #{name}_attributes
252
+ self.#{name}.map { |form| form.attributes }
253
+ end
254
+
255
+ def #{name}_attributes=(attrs_array)
256
+ form_klass = "#{name}".classify.constantize
257
+ @#{name} = attrs_array.map do |attrs|
258
+ form_klass.new.assign(attrs)
259
+ end
260
+ end
261
+
262
+ def #{name}=(model)
263
+ return unless #{from_model_attribute.inspect}
264
+ nested_models = model.send(#{from_model_attribute.inspect})
265
+ self.#{name}_attributes = nested_models.map { |m| m.attributes }
266
+ end
267
+ many
268
+ end
269
+ end
270
+
271
+ # A Formality form object is valid if its attributes
272
+ # validate and all of its children are valid.
273
+ def valid?(context=nil)
274
+ nested_forms_valid?(context) && super(context)
275
+ end
276
+
277
+ # If there are nested forms, call :valid? on them.
278
+ def nested_forms_valid?(context)
279
+ self.class.nested_forms.all? do |name|
280
+ nested = Array(send(name))
281
+ nested.all? { |form| form.valid?(context) }
282
+ end
283
+ end
284
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: formality
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-04 00:00:00.000000000 Z
12
+ date: 2012-09-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activemodel
16
- requirement: &70175687157900 !ruby/object:Gem::Requirement
16
+ requirement: &70323137369080 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 3.0.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70175687157900
24
+ version_requirements: *70323137369080
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: activesupport
27
- requirement: &70175687157400 !ruby/object:Gem::Requirement
27
+ requirement: &70323137368580 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 3.0.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70175687157400
35
+ version_requirements: *70323137368580
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: actionpack
38
- requirement: &70175687156940 !ruby/object:Gem::Requirement
38
+ requirement: &70323137368120 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 3.0.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70175687156940
46
+ version_requirements: *70323137368120
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: tst
49
- requirement: &70175687156560 !ruby/object:Gem::Requirement
49
+ requirement: &70323137367740 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70175687156560
57
+ version_requirements: *70323137367740
58
58
  description: ! 'ActiveModel-compliant form objects for rails app.
59
59
 
60
60
 
@@ -74,6 +74,7 @@ files:
74
74
  - LICENSE
75
75
  - Rakefile
76
76
  - README.md
77
+ - lib/formality.rb
77
78
  - test/attributes.rb
78
79
  - test/compliance.rb
79
80
  - test/lint.rb