formality 0.0.1 → 0.0.2

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.
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