granite-form 0.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 +7 -0
- data/.codeclimate.yml +13 -0
- data/.github/workflows/ci.yml +35 -0
- data/.github/workflows/main.yml +29 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.rubocop.yml +64 -0
- data/.rubocop_todo.yml +48 -0
- data/Appraisals +8 -0
- data/CHANGELOG.md +73 -0
- data/Gemfile +8 -0
- data/Guardfile +77 -0
- data/LICENSE +22 -0
- data/README.md +429 -0
- data/Rakefile +6 -0
- data/gemfiles/rails.4.2.gemfile +15 -0
- data/gemfiles/rails.5.0.gemfile +15 -0
- data/gemfiles/rails.5.1.gemfile +15 -0
- data/gemfiles/rails.5.2.gemfile +15 -0
- data/gemfiles/rails.6.0.gemfile +14 -0
- data/gemfiles/rails.6.1.gemfile +14 -0
- data/gemfiles/rails.7.0.gemfile +14 -0
- data/granite-form.gemspec +31 -0
- data/lib/granite/form/active_record/associations.rb +57 -0
- data/lib/granite/form/active_record/nested_attributes.rb +20 -0
- data/lib/granite/form/base.rb +15 -0
- data/lib/granite/form/config.rb +42 -0
- data/lib/granite/form/errors.rb +111 -0
- data/lib/granite/form/extensions.rb +36 -0
- data/lib/granite/form/model/associations/base.rb +97 -0
- data/lib/granite/form/model/associations/collection/embedded.rb +14 -0
- data/lib/granite/form/model/associations/collection/proxy.rb +35 -0
- data/lib/granite/form/model/associations/embeds_any.rb +19 -0
- data/lib/granite/form/model/associations/embeds_many.rb +152 -0
- data/lib/granite/form/model/associations/embeds_one.rb +112 -0
- data/lib/granite/form/model/associations/nested_attributes.rb +215 -0
- data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb +33 -0
- data/lib/granite/form/model/associations/persistence_adapters/active_record.rb +68 -0
- data/lib/granite/form/model/associations/persistence_adapters/base.rb +55 -0
- data/lib/granite/form/model/associations/references_any.rb +43 -0
- data/lib/granite/form/model/associations/references_many.rb +113 -0
- data/lib/granite/form/model/associations/references_one.rb +88 -0
- data/lib/granite/form/model/associations/reflections/base.rb +92 -0
- data/lib/granite/form/model/associations/reflections/embeds_any.rb +52 -0
- data/lib/granite/form/model/associations/reflections/embeds_many.rb +17 -0
- data/lib/granite/form/model/associations/reflections/embeds_one.rb +19 -0
- data/lib/granite/form/model/associations/reflections/references_any.rb +65 -0
- data/lib/granite/form/model/associations/reflections/references_many.rb +30 -0
- data/lib/granite/form/model/associations/reflections/references_one.rb +32 -0
- data/lib/granite/form/model/associations/reflections/singular.rb +37 -0
- data/lib/granite/form/model/associations/validations.rb +41 -0
- data/lib/granite/form/model/associations.rb +120 -0
- data/lib/granite/form/model/attributes/attribute.rb +75 -0
- data/lib/granite/form/model/attributes/base.rb +134 -0
- data/lib/granite/form/model/attributes/collection.rb +19 -0
- data/lib/granite/form/model/attributes/dictionary.rb +28 -0
- data/lib/granite/form/model/attributes/localized.rb +44 -0
- data/lib/granite/form/model/attributes/reference_many.rb +21 -0
- data/lib/granite/form/model/attributes/reference_one.rb +52 -0
- data/lib/granite/form/model/attributes/reflections/attribute.rb +61 -0
- data/lib/granite/form/model/attributes/reflections/base.rb +62 -0
- data/lib/granite/form/model/attributes/reflections/collection.rb +12 -0
- data/lib/granite/form/model/attributes/reflections/dictionary.rb +15 -0
- data/lib/granite/form/model/attributes/reflections/localized.rb +45 -0
- data/lib/granite/form/model/attributes/reflections/reference_many.rb +12 -0
- data/lib/granite/form/model/attributes/reflections/reference_one.rb +49 -0
- data/lib/granite/form/model/attributes/reflections/represents.rb +56 -0
- data/lib/granite/form/model/attributes/represents.rb +67 -0
- data/lib/granite/form/model/attributes.rb +204 -0
- data/lib/granite/form/model/callbacks.rb +72 -0
- data/lib/granite/form/model/conventions.rb +40 -0
- data/lib/granite/form/model/dirty.rb +84 -0
- data/lib/granite/form/model/lifecycle.rb +309 -0
- data/lib/granite/form/model/localization.rb +26 -0
- data/lib/granite/form/model/persistence.rb +59 -0
- data/lib/granite/form/model/primary.rb +59 -0
- data/lib/granite/form/model/representation.rb +101 -0
- data/lib/granite/form/model/scopes.rb +118 -0
- data/lib/granite/form/model/validations/associated.rb +22 -0
- data/lib/granite/form/model/validations/nested.rb +56 -0
- data/lib/granite/form/model/validations.rb +29 -0
- data/lib/granite/form/model.rb +33 -0
- data/lib/granite/form/railtie.rb +9 -0
- data/lib/granite/form/undefined_class.rb +11 -0
- data/lib/granite/form/version.rb +5 -0
- data/lib/granite/form.rb +163 -0
- data/spec/lib/granite/form/active_record/associations_spec.rb +211 -0
- data/spec/lib/granite/form/active_record/nested_attributes_spec.rb +15 -0
- data/spec/lib/granite/form/config_spec.rb +66 -0
- data/spec/lib/granite/form/model/associations/embeds_many_spec.rb +706 -0
- data/spec/lib/granite/form/model/associations/embeds_one_spec.rb +533 -0
- data/spec/lib/granite/form/model/associations/nested_attributes_spec.rb +119 -0
- data/spec/lib/granite/form/model/associations/persistence_adapters/active_record_spec.rb +58 -0
- data/spec/lib/granite/form/model/associations/references_many_spec.rb +572 -0
- data/spec/lib/granite/form/model/associations/references_one_spec.rb +445 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_any_spec.rb +42 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_many_spec.rb +145 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_one_spec.rb +117 -0
- data/spec/lib/granite/form/model/associations/reflections/references_many_spec.rb +303 -0
- data/spec/lib/granite/form/model/associations/reflections/references_one_spec.rb +287 -0
- data/spec/lib/granite/form/model/associations/validations_spec.rb +137 -0
- data/spec/lib/granite/form/model/associations_spec.rb +198 -0
- data/spec/lib/granite/form/model/attributes/attribute_spec.rb +186 -0
- data/spec/lib/granite/form/model/attributes/base_spec.rb +97 -0
- data/spec/lib/granite/form/model/attributes/collection_spec.rb +72 -0
- data/spec/lib/granite/form/model/attributes/dictionary_spec.rb +100 -0
- data/spec/lib/granite/form/model/attributes/localized_spec.rb +103 -0
- data/spec/lib/granite/form/model/attributes/reflections/attribute_spec.rb +72 -0
- data/spec/lib/granite/form/model/attributes/reflections/base_spec.rb +56 -0
- data/spec/lib/granite/form/model/attributes/reflections/collection_spec.rb +37 -0
- data/spec/lib/granite/form/model/attributes/reflections/dictionary_spec.rb +43 -0
- data/spec/lib/granite/form/model/attributes/reflections/localized_spec.rb +37 -0
- data/spec/lib/granite/form/model/attributes/reflections/represents_spec.rb +70 -0
- data/spec/lib/granite/form/model/attributes/represents_spec.rb +85 -0
- data/spec/lib/granite/form/model/attributes_spec.rb +350 -0
- data/spec/lib/granite/form/model/callbacks_spec.rb +337 -0
- data/spec/lib/granite/form/model/conventions_spec.rb +11 -0
- data/spec/lib/granite/form/model/dirty_spec.rb +84 -0
- data/spec/lib/granite/form/model/lifecycle_spec.rb +356 -0
- data/spec/lib/granite/form/model/persistence_spec.rb +46 -0
- data/spec/lib/granite/form/model/primary_spec.rb +84 -0
- data/spec/lib/granite/form/model/representation_spec.rb +139 -0
- data/spec/lib/granite/form/model/scopes_spec.rb +86 -0
- data/spec/lib/granite/form/model/typecasting_spec.rb +193 -0
- data/spec/lib/granite/form/model/validations/associated_spec.rb +102 -0
- data/spec/lib/granite/form/model/validations/nested_spec.rb +164 -0
- data/spec/lib/granite/form/model/validations_spec.rb +31 -0
- data/spec/lib/granite/form/model_spec.rb +10 -0
- data/spec/lib/granite/form_spec.rb +11 -0
- data/spec/shared/nested_attribute_examples.rb +332 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/support/model_helpers.rb +10 -0
- data/spec/support/muffle_helper.rb +7 -0
- metadata +403 -0
data/README.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
[](https://travis-ci.org/toptal/granite-form)
|
|
2
|
+
[](https://codeclimate.com/github/toptal/granite-form)
|
|
3
|
+
|
|
4
|
+
# Granite::Form
|
|
5
|
+
|
|
6
|
+
Granite::Form is a ActiveModel-based front-end for your data. You might need to use it in the following cases:
|
|
7
|
+
|
|
8
|
+
* When you need a form objects pattern.
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
class ProfileForm
|
|
12
|
+
include Granite::Form::Model
|
|
13
|
+
|
|
14
|
+
attribute 'first_name', String
|
|
15
|
+
attribute 'last_name', String
|
|
16
|
+
attribute 'birth_date', Date
|
|
17
|
+
|
|
18
|
+
def full_name
|
|
19
|
+
[first_name, last_name].reject(&:blank).join(' ')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def full_name= value
|
|
23
|
+
self.first_name, self.last_name = value.split(' ', 2).map(&:strip)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class ProfileController < ApplicationController
|
|
28
|
+
def edit
|
|
29
|
+
@form = ProfileForm.new current_user.attributes
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update
|
|
33
|
+
result = ProfileForm.new(params[:profile_form]).save do |form|
|
|
34
|
+
current_user.update_attributes(form.attributes)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if result
|
|
38
|
+
redirect_to ...
|
|
39
|
+
else
|
|
40
|
+
render 'edit'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
* When you need to work with data-storage in ActiveRecord style with
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class Flight
|
|
50
|
+
include Granite::Form::Model
|
|
51
|
+
|
|
52
|
+
attribute :airline, String
|
|
53
|
+
attribute :number, String
|
|
54
|
+
attribute :departure, Time
|
|
55
|
+
attribute :arrival, Time
|
|
56
|
+
|
|
57
|
+
validates :airline, :number, presence: true
|
|
58
|
+
|
|
59
|
+
def id
|
|
60
|
+
[airline, number].join('-')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.find id
|
|
64
|
+
source = REDIS.get(id)
|
|
65
|
+
instantiate(JSON.parse(source)) if source.present?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
define_save do
|
|
69
|
+
REDIS.set(id, attributes.to_json)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
define_destroy do
|
|
73
|
+
REDIS.del(id)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
* When you need to implement embedded objects for ActiveRecord models
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
class Answer
|
|
82
|
+
include Granite::Form::Model
|
|
83
|
+
|
|
84
|
+
attribute :question_id, Integer
|
|
85
|
+
attribute :content, String
|
|
86
|
+
|
|
87
|
+
validates :question_id, :content, presence: true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Quiz < ActiveRecord::Base
|
|
91
|
+
embeds_many :answers
|
|
92
|
+
|
|
93
|
+
validates :user_id, presence: true
|
|
94
|
+
validates :answers, associated: true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
q = Quiz.new
|
|
98
|
+
q.answers.build(question_id: 42, content: 'blabla')
|
|
99
|
+
q.save
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Why?
|
|
103
|
+
|
|
104
|
+
Granite::Form is an ActiveModel-based library that provides the following abilities:
|
|
105
|
+
|
|
106
|
+
* Standard form objects building toolkit: attributes with typecasting, validations, etc.
|
|
107
|
+
* High-level universal ORM/ODM library using any data source (DB, http, redis, text files).
|
|
108
|
+
* Embedding objects into your ActiveRecord entities. Quite useful with PG JSON capabilities.
|
|
109
|
+
|
|
110
|
+
Key features:
|
|
111
|
+
|
|
112
|
+
* Complete objects lifecycle support: saving, updating, destroying.
|
|
113
|
+
* Embedded and referenced associations.
|
|
114
|
+
* Backend-agnostic named scopes functionality.
|
|
115
|
+
* Callbacks, validations and dirty attributes inside.
|
|
116
|
+
|
|
117
|
+
## Installation
|
|
118
|
+
|
|
119
|
+
Add this line to your application's Gemfile:
|
|
120
|
+
|
|
121
|
+
gem 'granite-form'
|
|
122
|
+
|
|
123
|
+
And then execute:
|
|
124
|
+
|
|
125
|
+
$ bundle
|
|
126
|
+
|
|
127
|
+
Or install it yourself as:
|
|
128
|
+
|
|
129
|
+
$ gem install granite-form
|
|
130
|
+
|
|
131
|
+
## Usage
|
|
132
|
+
|
|
133
|
+
Granite::Form has modular architecture, so it is required to include modules to obtain additional features. By default Granite::Form supports attributes definition and validations.
|
|
134
|
+
|
|
135
|
+
### Attributes
|
|
136
|
+
|
|
137
|
+
Granite::Form provides several types of attributes and typecasts each attribute to its defined type upon initialization.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
class Book
|
|
141
|
+
include Granite::Form::Model
|
|
142
|
+
|
|
143
|
+
attribute :title, String
|
|
144
|
+
collection :author_ids, Integer
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Attribute
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
attribute :full_name, String, default: 'John Talbot'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
By default, if type for attribute is not set, it is defined with `Object` type, so it would be a great idea to specify type for every attribute explicitly.
|
|
155
|
+
|
|
156
|
+
Type is necessary for attribute typecasting. Here is the list of pre-defined basic typecasters:
|
|
157
|
+
|
|
158
|
+
```irb
|
|
159
|
+
[1] pry(main)> Granite::Form._typecasters.keys
|
|
160
|
+
=> ["Object", "String", "Array", "Hash", "Date", "DateTime", "Time", "ActiveSupport::TimeZone", "BigDecimal", "Float", "Integer", "Boolean", "Granite::Form::UUID"]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
In addition, you can provide any class type when defining the attribute, but in that case you will be able to only assign instances of that specific class or value nil:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
attribute :template, MyCustomTemplateType
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
##### Defaults
|
|
170
|
+
|
|
171
|
+
It is possible to provide default values for attributes and they will act in the same way as AR or Mongoid default values:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
attribute :check, Boolean, default: false # Simply false by default
|
|
175
|
+
attribute :today, Date, default: ->{ Time.zone.now.to_date } # Dynamic default value
|
|
176
|
+
attribute :today_wday, Integer, default: ->{ today.wday } # Default is evaluated in instance context
|
|
177
|
+
attribute :today_wday, Integer, default: ->(instance) { instance.today.wday } # The same as previous, but instance provided explicitly
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
##### Enums
|
|
181
|
+
|
|
182
|
+
Enums restrict the scope of possible values for attribute. If assigned value is not included in provided list - then it turns to nil:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
attribute :direction, String, enum: %w[north south east west]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
##### Normalizers
|
|
189
|
+
|
|
190
|
+
Normalizers are applied last, modifying typecast value. It is possible to provide a list of normalizers, they will be applied in the order. It is possible to pre-define normalizers to DRY code:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
Granite::Form.normalizer(:trim) do |value, options, _attribute|
|
|
194
|
+
value.first(options[:length] || 2)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
attribute :title, String, normalizers: [->(value) { value.strip }, trim: {length: 80}]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
##### Readonly
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
attribute :name, String, readonly: true # Readonly forever
|
|
204
|
+
attribute :name, String, readonly: ->{ true } # Conditionally readonly
|
|
205
|
+
attribute :name, String, readonly: ->(instance) { instance.subject.present? } # Explicit instance
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### Collection
|
|
209
|
+
|
|
210
|
+
Collection is simply an array of equally-typed values:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
class Panda
|
|
214
|
+
include Granite::Form::Model
|
|
215
|
+
|
|
216
|
+
collection :ids, Integer
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Collection typecasts each value to specified type and also no matter what are you going to pass - it will be an array.
|
|
221
|
+
|
|
222
|
+
```irb
|
|
223
|
+
[1] pry(main)> Panda.new
|
|
224
|
+
=> #<Panda ids: []>
|
|
225
|
+
[2] pry(main)> Panda.new(ids: 42)
|
|
226
|
+
=> #<Panda ids: [42]>
|
|
227
|
+
[3] pry(main)> Panda.new(ids: [42, '33'])
|
|
228
|
+
=> #<Panda ids: [42, 33]>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Default and enum modifiers are applied to every value, normalizer will be applied to the whole array.
|
|
232
|
+
|
|
233
|
+
#### Dictionary
|
|
234
|
+
|
|
235
|
+
Dictionary field is a hash of specified type values with string keys:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class Foo
|
|
239
|
+
include Granite::Form::Model
|
|
240
|
+
|
|
241
|
+
dictionary :ordering, String
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```irb
|
|
246
|
+
[1] pry(main)> Foo.new
|
|
247
|
+
=> #<Foo ordering: {}>
|
|
248
|
+
[2] pry(main)> Foo.new(ordering: {name: :desc})
|
|
249
|
+
=> #<Foo ordering: {"name"=>"desc"}>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Keys list might be restricted with `:keys` option, defaults and enums are applied to every value, normalizers are applied to the whole hash.
|
|
253
|
+
|
|
254
|
+
#### Localized
|
|
255
|
+
|
|
256
|
+
Localized is similar to how Globalize 3 attributes work.
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
localized :title, String
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### Represents
|
|
263
|
+
|
|
264
|
+
Represents provides an easy way to expose model attributes through an interface.
|
|
265
|
+
It will automatically set passed value to the represented object **before validation**.
|
|
266
|
+
You can use any ActiveRecord, ActiveModel or Granite::Form object as a target of representation.
|
|
267
|
+
A type of an attribute will be taken from it.
|
|
268
|
+
If there is no type, it will be `Object` by default. You can set the type explicitly by passing the `type: TypeClass` option.
|
|
269
|
+
Represents will also add automatic validation of the target object.
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
class Person
|
|
273
|
+
include Granite::Form::Model
|
|
274
|
+
|
|
275
|
+
attribute :name, String
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
class Doctor
|
|
279
|
+
include Granite::Form::Model
|
|
280
|
+
include Granite::Form::Model::Representation
|
|
281
|
+
|
|
282
|
+
attribute :person, Object
|
|
283
|
+
represents :name, of: :person
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
person = Person.new(name: 'Walter Bishop')
|
|
287
|
+
# => #<Person name: "Walter Bishop">
|
|
288
|
+
Doctor.new(person: person).name
|
|
289
|
+
# => "Walter Bishop"
|
|
290
|
+
Doctor.new(person: person, name: 'Dr. Walter Bishop').name
|
|
291
|
+
# => "Dr. Walter Bishop"
|
|
292
|
+
person.name
|
|
293
|
+
# => "Dr. Walter Bishop"
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Associations
|
|
297
|
+
|
|
298
|
+
Granite::Form provides a set of associations. There are two types of them: referenced and embedded. The closest example of referenced association is AR `belongs_to` and as for embedded ones - Mongoid's embedded. Also these associations support `accepts_nested_attributes` call.
|
|
299
|
+
|
|
300
|
+
#### EmbedsOne
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
embeds_one :profile
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Defines singular embedded object. Might be defined inline:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
embeds_one :profile do
|
|
310
|
+
attribute :first_name, String
|
|
311
|
+
attribute :last_name, String
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Possible options:
|
|
316
|
+
|
|
317
|
+
* `:class_name` - association class name
|
|
318
|
+
* `:validate` - true or false
|
|
319
|
+
* `:default` - default value for association: attributes hash or instance of defined class
|
|
320
|
+
|
|
321
|
+
#### EmbedsMany
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
embeds_many :tags
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Defines collection of embedded objects. Might be defined inline:
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
embeds_many :tags do
|
|
331
|
+
attribute :identifier, String
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
* `:class_name` - association class name
|
|
336
|
+
* `:validate` - true or false
|
|
337
|
+
* `:default` - default value for association: attributes hash collection or instances of defined class
|
|
338
|
+
|
|
339
|
+
#### ReferencesOne
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
references_one :user
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
This will provide several methods to the object: `#user`, `#user=`, `#user_id` and `#user_id=`, just as would occur with an ActiveRecord association.
|
|
346
|
+
|
|
347
|
+
Possible options:
|
|
348
|
+
|
|
349
|
+
* `:class_name` - association class name
|
|
350
|
+
* `:primary_key` - associated object primary key (`:id` by default):
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
references_one :user, primary_key: :name
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
This will create the following methods: `#user`, `#user=`, `#user_name` and `#user_name=`
|
|
357
|
+
|
|
358
|
+
* `:reference_key` - redefines `#user_id` and `#user_id=` method names completely.
|
|
359
|
+
* `:validate` - true or false
|
|
360
|
+
* `:default` - default value for association: reference or object itself
|
|
361
|
+
|
|
362
|
+
#### ReferencesMany
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
references_many :users
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
This will provide several methods to the object: `#users`, `#users=`, `#user_ids` and `#user_ids=` just as an ActiveRecord relation does.
|
|
369
|
+
|
|
370
|
+
Possible options:
|
|
371
|
+
|
|
372
|
+
* `:class_name` - association class name
|
|
373
|
+
* `:primary_key` - associated object primary key (`:id` by default):
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
references_many :users, primary_key: :name
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
This will create the following methods: `#users`, `#users=`, `#user_names` and `#user_names=`
|
|
380
|
+
|
|
381
|
+
* `:reference_key` - redefines `#user_ids` and `#user_ids=` method names completely.
|
|
382
|
+
* `:validate` - true or false
|
|
383
|
+
* `:default` - default value for association: reference collection or objects themselves
|
|
384
|
+
|
|
385
|
+
#### Interacting with ActiveRecord
|
|
386
|
+
|
|
387
|
+
### Persistence Adapters
|
|
388
|
+
|
|
389
|
+
Adapter definition syntax:
|
|
390
|
+
```ruby
|
|
391
|
+
class Mongoid::Document
|
|
392
|
+
# anything that have similar interface to
|
|
393
|
+
# Granite::Form::Model::Associations::PersistenceAdapters::Base
|
|
394
|
+
def self.granite_persistence_adapter
|
|
395
|
+
MongoidAdapter
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
Where
|
|
400
|
+
`ClassName` - name of model class or one of ancestors
|
|
401
|
+
`data_source` - name of data source class
|
|
402
|
+
`primary_key` - key to search data
|
|
403
|
+
`scope_proc` - additional proc for filtering
|
|
404
|
+
|
|
405
|
+
All required interface for adapters described in `Granite::Form::Model::Associations::PersistenceAdapters::Base`.
|
|
406
|
+
|
|
407
|
+
Adapter for ActiveRecord is `Granite::Form::Model::Associations::PersistenceAdapters::ActiveRecord`. So, all AR models will use `PersistenceAdapters::ActiveRecord` by default.
|
|
408
|
+
|
|
409
|
+
### Primary
|
|
410
|
+
|
|
411
|
+
### Persistence
|
|
412
|
+
|
|
413
|
+
### Lifecycle
|
|
414
|
+
|
|
415
|
+
### Callbacks
|
|
416
|
+
|
|
417
|
+
### Dirty
|
|
418
|
+
|
|
419
|
+
### Validations
|
|
420
|
+
|
|
421
|
+
### Scopes
|
|
422
|
+
|
|
423
|
+
## Contributing
|
|
424
|
+
|
|
425
|
+
1. Fork it
|
|
426
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
427
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
428
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
429
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "activesupport", "~> 4.2.0"
|
|
6
|
+
gem "activemodel", "~> 4.2.0"
|
|
7
|
+
gem "activerecord", "~> 4.2.0"
|
|
8
|
+
gem "sqlite3", "~> 1.3.6"
|
|
9
|
+
|
|
10
|
+
group :test do
|
|
11
|
+
gem "guard"
|
|
12
|
+
gem "guard-rspec"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "activesupport", "~> 5.0.0"
|
|
6
|
+
gem "activemodel", "~> 5.0.0"
|
|
7
|
+
gem "activerecord", "~> 5.0.0"
|
|
8
|
+
gem "sqlite3", "~> 1.3.6"
|
|
9
|
+
|
|
10
|
+
group :test do
|
|
11
|
+
gem "guard"
|
|
12
|
+
gem "guard-rspec"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "activesupport", "~> 5.1.0"
|
|
6
|
+
gem "activemodel", "~> 5.1.0"
|
|
7
|
+
gem "activerecord", "~> 5.1.0"
|
|
8
|
+
gem "sqlite3", "~> 1.3.6"
|
|
9
|
+
|
|
10
|
+
group :test do
|
|
11
|
+
gem "guard"
|
|
12
|
+
gem "guard-rspec"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "activesupport", "~> 5.2.0"
|
|
6
|
+
gem "activemodel", "~> 5.2.0"
|
|
7
|
+
gem "activerecord", "~> 5.2.0"
|
|
8
|
+
gem "sqlite3", "~> 1.3.6"
|
|
9
|
+
|
|
10
|
+
group :test do
|
|
11
|
+
gem "guard"
|
|
12
|
+
gem "guard-rspec"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "activesupport", "~> 6.0.0"
|
|
6
|
+
gem "activemodel", "~> 6.0.0"
|
|
7
|
+
gem "activerecord", "~> 6.0.0"
|
|
8
|
+
|
|
9
|
+
group :test do
|
|
10
|
+
gem "guard"
|
|
11
|
+
gem "guard-rspec"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "activesupport", "~> 6.1.0"
|
|
6
|
+
gem "activemodel", "~> 6.1.0"
|
|
7
|
+
gem "activerecord", "~> 6.1.0"
|
|
8
|
+
|
|
9
|
+
group :test do
|
|
10
|
+
gem "guard"
|
|
11
|
+
gem "guard-rspec"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "activesupport", "~> 7.0.0"
|
|
6
|
+
gem "activemodel", "~> 7.0.0"
|
|
7
|
+
gem "activerecord", "~> 7.0.0"
|
|
8
|
+
|
|
9
|
+
group :test do
|
|
10
|
+
gem "guard"
|
|
11
|
+
gem "guard-rspec"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require File.expand_path('../lib/granite/form/version', __FILE__)
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |gem|
|
|
4
|
+
gem.authors = ['pyromaniac']
|
|
5
|
+
gem.email = ['kinwizard@gmail.com']
|
|
6
|
+
gem.description = 'Making object from any hash or hash array'
|
|
7
|
+
gem.summary = 'Working with hashes in AR style'
|
|
8
|
+
gem.homepage = ''
|
|
9
|
+
|
|
10
|
+
gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
|
|
11
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
13
|
+
gem.name = 'granite-form'
|
|
14
|
+
gem.require_paths = ['lib']
|
|
15
|
+
gem.version = Granite::Form::VERSION
|
|
16
|
+
|
|
17
|
+
gem.add_development_dependency 'actionpack', '>= 4.0'
|
|
18
|
+
gem.add_development_dependency 'activerecord', '>= 4.0'
|
|
19
|
+
gem.add_development_dependency 'appraisal'
|
|
20
|
+
gem.add_development_dependency 'database_cleaner'
|
|
21
|
+
gem.add_development_dependency 'rake'
|
|
22
|
+
gem.add_development_dependency 'rspec', '~> 3.7.0'
|
|
23
|
+
gem.add_development_dependency 'rspec-its'
|
|
24
|
+
gem.add_development_dependency 'rubocop', '0.52.1'
|
|
25
|
+
gem.add_development_dependency 'sqlite3'
|
|
26
|
+
gem.add_development_dependency 'uuidtools'
|
|
27
|
+
|
|
28
|
+
gem.add_runtime_dependency 'activemodel', '>= 4.0'
|
|
29
|
+
gem.add_runtime_dependency 'activesupport', '>= 4.0'
|
|
30
|
+
gem.add_runtime_dependency 'tzinfo'
|
|
31
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Associations
|
|
5
|
+
READER = lambda do |ref, object|
|
|
6
|
+
value = object.read_attribute(ref.name)
|
|
7
|
+
if value.present?
|
|
8
|
+
value.is_a?(String) ? JSON.parse(value) : value
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
WRITER = lambda do |ref, object, value|
|
|
13
|
+
object.send(:write_attribute, ref.name, value ? value.to_json : nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Reflections
|
|
17
|
+
class EmbedsOne < Granite::Form::Model::Associations::Reflections::EmbedsOne
|
|
18
|
+
def is_a?(klass)
|
|
19
|
+
super || klass == ::ActiveRecord::Reflection::AssociationReflection
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class EmbedsMany < Granite::Form::Model::Associations::Reflections::EmbedsMany
|
|
24
|
+
def is_a?(klass)
|
|
25
|
+
super || klass == ::ActiveRecord::Reflection::AssociationReflection
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
extend ActiveSupport::Concern
|
|
31
|
+
|
|
32
|
+
included do
|
|
33
|
+
{embeds_many: Reflections::EmbedsMany, embeds_one: Reflections::EmbedsOne}.each do |(method, reflection_class)|
|
|
34
|
+
define_singleton_method method do |name, options = {}, &block|
|
|
35
|
+
reflection = reflection_class.build(self, self, name,
|
|
36
|
+
options.reverse_merge(read: READER, write: WRITER),
|
|
37
|
+
&block)
|
|
38
|
+
if ::ActiveRecord::Reflection.respond_to? :add_reflection
|
|
39
|
+
::ActiveRecord::Reflection.add_reflection self, reflection.name, reflection
|
|
40
|
+
else
|
|
41
|
+
self.reflections = reflections.merge(reflection.name => reflection)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
callback_name = :"update_#{reflection.name}_association"
|
|
45
|
+
before_save callback_name
|
|
46
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
|
47
|
+
def #{callback_name}
|
|
48
|
+
association(:#{reflection.name}).apply_changes!
|
|
49
|
+
end
|
|
50
|
+
METHOD
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module NestedAttributes
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
def accepts_nested_attributes_for(*attr_names)
|
|
8
|
+
options = attr_names.extract_options!
|
|
9
|
+
granite_associations, active_record_association = attr_names.partition do |association_name|
|
|
10
|
+
reflect_on_association(association_name).is_a?(Granite::Form::Model::Associations::Reflections::Base)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
Granite::Form::Model::Associations::NestedAttributes::NestedAttributesMethods
|
|
14
|
+
.accepts_nested_attributes_for(self, *granite_associations, options.dup)
|
|
15
|
+
super(*active_record_association, options.dup)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'granite/form/model'
|
|
2
|
+
require 'granite/form/model/primary'
|
|
3
|
+
require 'granite/form/model/lifecycle'
|
|
4
|
+
require 'granite/form/model/associations'
|
|
5
|
+
|
|
6
|
+
module Granite
|
|
7
|
+
module Form
|
|
8
|
+
class Base
|
|
9
|
+
include Granite::Form::Model
|
|
10
|
+
include Granite::Form::Model::Primary
|
|
11
|
+
include Granite::Form::Model::Lifecycle
|
|
12
|
+
include Granite::Form::Model::Associations
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|