granite-form 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +13 -0
  3. data/.github/workflows/ci.yml +35 -0
  4. data/.github/workflows/main.yml +29 -0
  5. data/.gitignore +21 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +64 -0
  8. data/.rubocop_todo.yml +48 -0
  9. data/Appraisals +8 -0
  10. data/CHANGELOG.md +73 -0
  11. data/Gemfile +8 -0
  12. data/Guardfile +77 -0
  13. data/LICENSE +22 -0
  14. data/README.md +429 -0
  15. data/Rakefile +6 -0
  16. data/gemfiles/rails.4.2.gemfile +15 -0
  17. data/gemfiles/rails.5.0.gemfile +15 -0
  18. data/gemfiles/rails.5.1.gemfile +15 -0
  19. data/gemfiles/rails.5.2.gemfile +15 -0
  20. data/gemfiles/rails.6.0.gemfile +14 -0
  21. data/gemfiles/rails.6.1.gemfile +14 -0
  22. data/gemfiles/rails.7.0.gemfile +14 -0
  23. data/granite-form.gemspec +31 -0
  24. data/lib/granite/form/active_record/associations.rb +57 -0
  25. data/lib/granite/form/active_record/nested_attributes.rb +20 -0
  26. data/lib/granite/form/base.rb +15 -0
  27. data/lib/granite/form/config.rb +42 -0
  28. data/lib/granite/form/errors.rb +111 -0
  29. data/lib/granite/form/extensions.rb +36 -0
  30. data/lib/granite/form/model/associations/base.rb +97 -0
  31. data/lib/granite/form/model/associations/collection/embedded.rb +14 -0
  32. data/lib/granite/form/model/associations/collection/proxy.rb +35 -0
  33. data/lib/granite/form/model/associations/embeds_any.rb +19 -0
  34. data/lib/granite/form/model/associations/embeds_many.rb +152 -0
  35. data/lib/granite/form/model/associations/embeds_one.rb +112 -0
  36. data/lib/granite/form/model/associations/nested_attributes.rb +215 -0
  37. data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb +33 -0
  38. data/lib/granite/form/model/associations/persistence_adapters/active_record.rb +68 -0
  39. data/lib/granite/form/model/associations/persistence_adapters/base.rb +55 -0
  40. data/lib/granite/form/model/associations/references_any.rb +43 -0
  41. data/lib/granite/form/model/associations/references_many.rb +113 -0
  42. data/lib/granite/form/model/associations/references_one.rb +88 -0
  43. data/lib/granite/form/model/associations/reflections/base.rb +92 -0
  44. data/lib/granite/form/model/associations/reflections/embeds_any.rb +52 -0
  45. data/lib/granite/form/model/associations/reflections/embeds_many.rb +17 -0
  46. data/lib/granite/form/model/associations/reflections/embeds_one.rb +19 -0
  47. data/lib/granite/form/model/associations/reflections/references_any.rb +65 -0
  48. data/lib/granite/form/model/associations/reflections/references_many.rb +30 -0
  49. data/lib/granite/form/model/associations/reflections/references_one.rb +32 -0
  50. data/lib/granite/form/model/associations/reflections/singular.rb +37 -0
  51. data/lib/granite/form/model/associations/validations.rb +41 -0
  52. data/lib/granite/form/model/associations.rb +120 -0
  53. data/lib/granite/form/model/attributes/attribute.rb +75 -0
  54. data/lib/granite/form/model/attributes/base.rb +134 -0
  55. data/lib/granite/form/model/attributes/collection.rb +19 -0
  56. data/lib/granite/form/model/attributes/dictionary.rb +28 -0
  57. data/lib/granite/form/model/attributes/localized.rb +44 -0
  58. data/lib/granite/form/model/attributes/reference_many.rb +21 -0
  59. data/lib/granite/form/model/attributes/reference_one.rb +52 -0
  60. data/lib/granite/form/model/attributes/reflections/attribute.rb +61 -0
  61. data/lib/granite/form/model/attributes/reflections/base.rb +62 -0
  62. data/lib/granite/form/model/attributes/reflections/collection.rb +12 -0
  63. data/lib/granite/form/model/attributes/reflections/dictionary.rb +15 -0
  64. data/lib/granite/form/model/attributes/reflections/localized.rb +45 -0
  65. data/lib/granite/form/model/attributes/reflections/reference_many.rb +12 -0
  66. data/lib/granite/form/model/attributes/reflections/reference_one.rb +49 -0
  67. data/lib/granite/form/model/attributes/reflections/represents.rb +56 -0
  68. data/lib/granite/form/model/attributes/represents.rb +67 -0
  69. data/lib/granite/form/model/attributes.rb +204 -0
  70. data/lib/granite/form/model/callbacks.rb +72 -0
  71. data/lib/granite/form/model/conventions.rb +40 -0
  72. data/lib/granite/form/model/dirty.rb +84 -0
  73. data/lib/granite/form/model/lifecycle.rb +309 -0
  74. data/lib/granite/form/model/localization.rb +26 -0
  75. data/lib/granite/form/model/persistence.rb +59 -0
  76. data/lib/granite/form/model/primary.rb +59 -0
  77. data/lib/granite/form/model/representation.rb +101 -0
  78. data/lib/granite/form/model/scopes.rb +118 -0
  79. data/lib/granite/form/model/validations/associated.rb +22 -0
  80. data/lib/granite/form/model/validations/nested.rb +56 -0
  81. data/lib/granite/form/model/validations.rb +29 -0
  82. data/lib/granite/form/model.rb +33 -0
  83. data/lib/granite/form/railtie.rb +9 -0
  84. data/lib/granite/form/undefined_class.rb +11 -0
  85. data/lib/granite/form/version.rb +5 -0
  86. data/lib/granite/form.rb +163 -0
  87. data/spec/lib/granite/form/active_record/associations_spec.rb +211 -0
  88. data/spec/lib/granite/form/active_record/nested_attributes_spec.rb +15 -0
  89. data/spec/lib/granite/form/config_spec.rb +66 -0
  90. data/spec/lib/granite/form/model/associations/embeds_many_spec.rb +706 -0
  91. data/spec/lib/granite/form/model/associations/embeds_one_spec.rb +533 -0
  92. data/spec/lib/granite/form/model/associations/nested_attributes_spec.rb +119 -0
  93. data/spec/lib/granite/form/model/associations/persistence_adapters/active_record_spec.rb +58 -0
  94. data/spec/lib/granite/form/model/associations/references_many_spec.rb +572 -0
  95. data/spec/lib/granite/form/model/associations/references_one_spec.rb +445 -0
  96. data/spec/lib/granite/form/model/associations/reflections/embeds_any_spec.rb +42 -0
  97. data/spec/lib/granite/form/model/associations/reflections/embeds_many_spec.rb +145 -0
  98. data/spec/lib/granite/form/model/associations/reflections/embeds_one_spec.rb +117 -0
  99. data/spec/lib/granite/form/model/associations/reflections/references_many_spec.rb +303 -0
  100. data/spec/lib/granite/form/model/associations/reflections/references_one_spec.rb +287 -0
  101. data/spec/lib/granite/form/model/associations/validations_spec.rb +137 -0
  102. data/spec/lib/granite/form/model/associations_spec.rb +198 -0
  103. data/spec/lib/granite/form/model/attributes/attribute_spec.rb +186 -0
  104. data/spec/lib/granite/form/model/attributes/base_spec.rb +97 -0
  105. data/spec/lib/granite/form/model/attributes/collection_spec.rb +72 -0
  106. data/spec/lib/granite/form/model/attributes/dictionary_spec.rb +100 -0
  107. data/spec/lib/granite/form/model/attributes/localized_spec.rb +103 -0
  108. data/spec/lib/granite/form/model/attributes/reflections/attribute_spec.rb +72 -0
  109. data/spec/lib/granite/form/model/attributes/reflections/base_spec.rb +56 -0
  110. data/spec/lib/granite/form/model/attributes/reflections/collection_spec.rb +37 -0
  111. data/spec/lib/granite/form/model/attributes/reflections/dictionary_spec.rb +43 -0
  112. data/spec/lib/granite/form/model/attributes/reflections/localized_spec.rb +37 -0
  113. data/spec/lib/granite/form/model/attributes/reflections/represents_spec.rb +70 -0
  114. data/spec/lib/granite/form/model/attributes/represents_spec.rb +85 -0
  115. data/spec/lib/granite/form/model/attributes_spec.rb +350 -0
  116. data/spec/lib/granite/form/model/callbacks_spec.rb +337 -0
  117. data/spec/lib/granite/form/model/conventions_spec.rb +11 -0
  118. data/spec/lib/granite/form/model/dirty_spec.rb +84 -0
  119. data/spec/lib/granite/form/model/lifecycle_spec.rb +356 -0
  120. data/spec/lib/granite/form/model/persistence_spec.rb +46 -0
  121. data/spec/lib/granite/form/model/primary_spec.rb +84 -0
  122. data/spec/lib/granite/form/model/representation_spec.rb +139 -0
  123. data/spec/lib/granite/form/model/scopes_spec.rb +86 -0
  124. data/spec/lib/granite/form/model/typecasting_spec.rb +193 -0
  125. data/spec/lib/granite/form/model/validations/associated_spec.rb +102 -0
  126. data/spec/lib/granite/form/model/validations/nested_spec.rb +164 -0
  127. data/spec/lib/granite/form/model/validations_spec.rb +31 -0
  128. data/spec/lib/granite/form/model_spec.rb +10 -0
  129. data/spec/lib/granite/form_spec.rb +11 -0
  130. data/spec/shared/nested_attribute_examples.rb +332 -0
  131. data/spec/spec_helper.rb +50 -0
  132. data/spec/support/model_helpers.rb +10 -0
  133. data/spec/support/muffle_helper.rb +7 -0
  134. metadata +403 -0
data/README.md ADDED
@@ -0,0 +1,429 @@
1
+ [![Build Status](https://travis-ci.org/toptal/granite-form.png?branch=master)](https://travis-ci.org/toptal/granite-form)
2
+ [![Code Climate](https://codeclimate.com/github/toptal/granite-form.png)](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,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -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