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.
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
@@ -0,0 +1,134 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ class Base
6
+ attr_reader :name, :owner
7
+ delegate :type, :typecaster, :readonly, to: :reflection
8
+
9
+ def initialize(name, owner)
10
+ @name = name
11
+ @owner = owner
12
+ @origin = :default
13
+ end
14
+
15
+ def reflection
16
+ @owner.class._attributes[name]
17
+ end
18
+
19
+ def write_value(value, origin: :user)
20
+ reset
21
+ @origin = origin
22
+ @value_cache = value
23
+ end
24
+
25
+ def write(value)
26
+ return if readonly?
27
+ write_value value
28
+ end
29
+
30
+ def reset
31
+ remove_variable(:value, :value_before_type_cast)
32
+ end
33
+
34
+ def read
35
+ @value_cache
36
+ end
37
+
38
+ def read_before_type_cast
39
+ @value_cache
40
+ end
41
+
42
+ def came_from_user?
43
+ @origin == :user
44
+ end
45
+
46
+ def came_from_default?
47
+ @origin == :default
48
+ end
49
+
50
+ def value_present?
51
+ !read.nil? && !(read.respond_to?(:empty?) && read.empty?)
52
+ end
53
+
54
+ def query
55
+ !(read.respond_to?(:zero?) ? read.zero? : read.blank?)
56
+ end
57
+
58
+ def typecast(value)
59
+ if value.instance_of?(type)
60
+ value
61
+ else
62
+ typecaster.call(value, self) unless value.nil?
63
+ end
64
+ end
65
+
66
+ def readonly?
67
+ !!(readonly.is_a?(Proc) ? evaluate(&readonly) : readonly)
68
+ end
69
+
70
+ def inspect_attribute
71
+ value = case read
72
+ when Date, Time, DateTime
73
+ %("#{read.to_s(:db)}")
74
+ else
75
+ inspection = read.inspect
76
+ inspection.size > 100 ? inspection.truncate(50) : inspection
77
+ end
78
+ "#{name}: #{value}"
79
+ end
80
+
81
+ def pollute
82
+ pollute = owner.class.dirty? && !owner.send(:attribute_changed?, name)
83
+
84
+ if pollute
85
+ previous_value = owner.__send__(name)
86
+ owner.send("#{name}_will_change!")
87
+
88
+ result = yield
89
+
90
+ owner.__send__(:clear_attribute_changes, [name]) if owner.__send__(name) == previous_value
91
+
92
+ if previous_value != read || (
93
+ read.respond_to?(:changed?) &&
94
+ read.changed?
95
+ )
96
+ owner.send(:set_attribute_was, name, previous_value)
97
+ end
98
+ result
99
+ else
100
+ yield
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def evaluate(*args, &block)
107
+ if block.arity >= 0 && block.arity <= args.length
108
+ owner.instance_exec(*args.first(block.arity), &block)
109
+ else
110
+ args = block.arity < 0 ? args : args.first(block.arity)
111
+ yield(*args, owner)
112
+ end
113
+ end
114
+
115
+ def remove_variable(*names)
116
+ names.flatten.each do |name|
117
+ name = :"@#{name}"
118
+ remove_instance_variable(name) if instance_variable_defined?(name)
119
+ end
120
+ end
121
+
122
+ def variable_cache(name)
123
+ name = :"@#{name}"
124
+ if instance_variable_defined?(name)
125
+ instance_variable_get(name)
126
+ else
127
+ instance_variable_set(name, yield)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,19 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ class Collection < Attribute
6
+ def read
7
+ @value ||= normalize(read_before_type_cast.map do |value|
8
+ enumerize(typecast(value))
9
+ end)
10
+ end
11
+
12
+ def read_before_type_cast
13
+ @value_before_type_cast ||= Array.wrap(@value_cache).map { |value| defaultize(value) }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ class Dictionary < Attribute
6
+ delegate :keys, to: :reflection
7
+
8
+ def read
9
+ @value ||= begin
10
+ hash = read_before_type_cast
11
+ hash = hash.stringify_keys.slice(*keys) if keys.present?
12
+
13
+ normalize(Hash[hash.map do |key, value|
14
+ [key, enumerize(typecast(value))]
15
+ end].with_indifferent_access).with_indifferent_access
16
+ end
17
+ end
18
+
19
+ def read_before_type_cast
20
+ @value_before_type_cast ||= Hash[(@value_cache.presence || {}).map do |key, value|
21
+ [key, defaultize(value)]
22
+ end].with_indifferent_access
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ class Localized < Attribute
6
+ def read
7
+ @value ||= Hash[read_before_type_cast.map do |locale, value|
8
+ [locale.to_s, normalize(enumerize(typecast(value)))]
9
+ end]
10
+ end
11
+
12
+ def read_before_type_cast
13
+ @value_before_type_cast ||= Hash[(@value_cache.presence || {}).map do |locale, value|
14
+ [locale.to_s, defaultize(value)]
15
+ end]
16
+ end
17
+
18
+ def write_locale(value, locale)
19
+ pollute do
20
+ write(read.merge(locale.to_s => value))
21
+ end
22
+ end
23
+
24
+ def read_locale(locale)
25
+ read[owner.class.fallbacks(locale).detect do |fallback|
26
+ read[fallback.to_s]
27
+ end.to_s]
28
+ end
29
+
30
+ def read_locale_before_type_cast(locale)
31
+ read_before_type_cast[owner.class.fallbacks(locale).detect do |fallback|
32
+ read_before_type_cast[fallback.to_s]
33
+ end.to_s]
34
+ end
35
+
36
+ def locale_query(locale)
37
+ value = read_locale(locale)
38
+ !(value.respond_to?(:zero?) ? value.zero? : value.blank?)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ class ReferenceMany < ReferenceOne
6
+ def type_casted_value
7
+ variable_cache(:value) do
8
+ read_before_type_cast.map { |id| typecast(id) }
9
+ end
10
+ end
11
+
12
+ def read_before_type_cast
13
+ variable_cache(:value_before_type_cast) do
14
+ Array.wrap(@value_cache)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,52 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ class ReferenceOne < Base
6
+ def write(value)
7
+ pollute do
8
+ previous = type_casted_value
9
+ result = write_value value
10
+ changed = (!value.nil? && type_casted_value.nil?) || type_casted_value != previous
11
+
12
+ association.reset if changed
13
+ result
14
+ end
15
+ end
16
+
17
+ def read
18
+ if association.target
19
+ association.identify
20
+ else
21
+ type_casted_value
22
+ end
23
+ end
24
+
25
+ def type_casted_value
26
+ variable_cache(:value) do
27
+ typecast(read_before_type_cast)
28
+ end
29
+ end
30
+
31
+ def read_before_type_cast
32
+ @value_cache
33
+ end
34
+
35
+ def type
36
+ @type ||= association.reflection.persistence_adapter.primary_key_type
37
+ end
38
+
39
+ def typecaster
40
+ @typecaster ||= Granite::Form.typecaster(type.ancestors.grep(Class))
41
+ end
42
+
43
+ private
44
+
45
+ def association
46
+ @association ||= owner.association(reflection.association)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,61 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class Attribute < Base
7
+ def self.build(target, generated_methods, name, *args, &block)
8
+ attribute = super(target, generated_methods, name, *args, &block)
9
+ generate_methods name, generated_methods
10
+ attribute
11
+ end
12
+
13
+ def self.generate_methods(name, target)
14
+ target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
15
+ def #{name}
16
+ attribute('#{name}').read
17
+ end
18
+
19
+ def #{name}= value
20
+ attribute('#{name}').write(value)
21
+ end
22
+
23
+ def #{name}?
24
+ attribute('#{name}').query
25
+ end
26
+
27
+ def #{name}_before_type_cast
28
+ attribute('#{name}').read_before_type_cast
29
+ end
30
+
31
+ def #{name}_came_from_user?
32
+ attribute('#{name}').came_from_user?
33
+ end
34
+
35
+ def #{name}_default
36
+ attribute('#{name}').default
37
+ end
38
+
39
+ def #{name}_values
40
+ attribute('#{name}').enum.to_a
41
+ end
42
+ RUBY
43
+ end
44
+
45
+ def defaultizer
46
+ @defaultizer ||= options[:default]
47
+ end
48
+
49
+ def enumerizer
50
+ @enumerizer ||= options[:enum] || options[:in]
51
+ end
52
+
53
+ def normalizers
54
+ @normalizers ||= Array.wrap(options[:normalize] || options[:normalizer] || options[:normalizers])
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,62 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class Base
7
+ attr_reader :name, :options
8
+
9
+ class << self
10
+ def build(_target, _generated_methods, name, *args, &block)
11
+ options = args.extract_options!
12
+ options[:type] = args.first if args.first
13
+ options[:default] = block if block
14
+ new(name, options)
15
+ end
16
+
17
+ def generate_methods(name, target) end
18
+
19
+ def attribute_class
20
+ @attribute_class ||= "Granite::Form::Model::Attributes::#{name.demodulize}".constantize
21
+ end
22
+ end
23
+
24
+ def initialize(name, options = {})
25
+ @name = name.to_s
26
+ @options = options
27
+ end
28
+
29
+ def build_attribute(owner, raw_value = Granite::Form::UNDEFINED)
30
+ attribute = self.class.attribute_class.new(name, owner)
31
+ attribute.write_value(raw_value, origin: :persistence) unless raw_value == Granite::Form::UNDEFINED
32
+ attribute
33
+ end
34
+
35
+ def type
36
+ @type ||= case options[:type]
37
+ when Class, Module
38
+ options[:type]
39
+ when nil
40
+ raise "Type is not specified for `#{name}`"
41
+ else
42
+ options[:type].to_s.camelize.constantize
43
+ end
44
+ end
45
+
46
+ def typecaster
47
+ @typecaster ||= Granite::Form.typecaster(type.ancestors.grep(Class))
48
+ end
49
+
50
+ def readonly
51
+ @readonly ||= options[:readonly]
52
+ end
53
+
54
+ def inspect_reflection
55
+ "#{name}: #{type}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,12 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class Collection < Attribute
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class Dictionary < Attribute
7
+ def keys
8
+ @keys ||= Array.wrap(options[:keys]).map(&:to_s)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class Localized < Attribute
7
+ def self.build(target, generated_methods, name, *args, &block)
8
+ attribute = super(target, generated_methods, name, *args, &block)
9
+ generate_methods name, generated_methods
10
+ attribute
11
+ end
12
+
13
+ def self.generate_methods(name, target)
14
+ target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
15
+ def #{name}_translations
16
+ attribute('#{name}').read
17
+ end
18
+
19
+ def #{name}_translations= value
20
+ attribute('#{name}').write(value)
21
+ end
22
+
23
+ def #{name}
24
+ attribute('#{name}').read_locale(self.class.locale)
25
+ end
26
+
27
+ def #{name}= value
28
+ attribute('#{name}').write_locale(value, self.class.locale)
29
+ end
30
+
31
+ def #{name}?
32
+ attribute('#{name}').locale_query(self.class.locale)
33
+ end
34
+
35
+ def #{name}_before_type_cast
36
+ attribute('#{name}').read_locale_before_type_cast(self.class.locale)
37
+ end
38
+ RUBY
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class ReferenceMany < ReferenceOne
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class ReferenceOne < Base
7
+ def self.build(_target, generated_methods, name, *args)
8
+ options = args.extract_options!
9
+ generate_methods name, generated_methods
10
+ new(name, options)
11
+ end
12
+
13
+ def self.generate_methods(name, target)
14
+ target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
15
+ def #{name}
16
+ attribute('#{name}').read
17
+ end
18
+
19
+ def #{name}= value
20
+ attribute('#{name}').write(value)
21
+ end
22
+
23
+ def #{name}?
24
+ attribute('#{name}').query
25
+ end
26
+
27
+ def #{name}_before_type_cast
28
+ attribute('#{name}').read_before_type_cast
29
+ end
30
+ RUBY
31
+ end
32
+
33
+ def type
34
+ Object
35
+ end
36
+
37
+ def inspect_reflection
38
+ "#{name}: (reference)"
39
+ end
40
+
41
+ def association
42
+ @association ||= options[:association].to_s
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,56 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class Represents < Attribute
7
+ def self.build(target, generated_methods, name, *args, &block)
8
+ options = args.extract_options!
9
+
10
+ reference = target.reflect_on_association(options[:of]) if target.respond_to?(:reflect_on_association)
11
+ reference ||= target.reflect_on_attribute(options[:of]) if target.respond_to?(:reflect_on_attribute)
12
+ options[:of] = reference.name if reference
13
+ validates_nested = target.respond_to?(:validates_nested) && !target.validates_nested?(options[:of])
14
+ target.validates_nested(options[:of]) if validates_nested
15
+
16
+ super(target, generated_methods, name, *args, options, &block)
17
+ end
18
+
19
+ def initialize(name, options)
20
+ super
21
+ raise ArgumentError, "Undefined reference for `#{name}`" if reference.blank?
22
+ end
23
+
24
+ def type
25
+ Object
26
+ end
27
+
28
+ def reference
29
+ @reference ||= options[:of].to_s
30
+ end
31
+
32
+ def column
33
+ @column ||= options[:column].presence.try(:to_s) || name
34
+ end
35
+
36
+ def reader
37
+ @reader ||= options[:reader].presence.try(:to_s) || column
38
+ end
39
+
40
+ def reader_before_type_cast
41
+ @reader_before_type_cast ||= "#{reader}_before_type_cast"
42
+ end
43
+
44
+ def writer
45
+ @writer ||= "#{options[:writer].presence || column}="
46
+ end
47
+
48
+ def inspect_reflection
49
+ "#{name}: (represents)"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,67 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ class Represents < Attribute
6
+ delegate :reader, :reader_before_type_cast, :writer, to: :reflection
7
+
8
+ def write(value)
9
+ return if readonly?
10
+ pollute do
11
+ reset
12
+ reference.send(writer, value)
13
+ end
14
+ end
15
+
16
+ def reset
17
+ super
18
+ remove_variable(:cached_value, :cached_value_before_type_cast)
19
+ end
20
+
21
+ def read
22
+ reset if cached_value != read_value
23
+ variable_cache(:value) do
24
+ normalize(enumerize(defaultize(cached_value, read_before_type_cast)))
25
+ end
26
+ end
27
+
28
+ def read_before_type_cast
29
+ reset if cached_value_before_type_cast != read_value_before_type_cast
30
+ variable_cache(:value_before_type_cast) do
31
+ defaultize(cached_value_before_type_cast)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def reference
38
+ owner.send(reflection.reference)
39
+ end
40
+
41
+ def read_value
42
+ ref = reference
43
+ ref.public_send(reader) if ref
44
+ end
45
+
46
+ def cached_value
47
+ variable_cache(:cached_value) { read_value }
48
+ end
49
+
50
+ def read_value_before_type_cast
51
+ ref = reference
52
+ return unless ref
53
+ if ref.respond_to?(reader_before_type_cast)
54
+ ref.public_send(reader_before_type_cast)
55
+ else
56
+ ref.public_send(reader)
57
+ end
58
+ end
59
+
60
+ def cached_value_before_type_cast
61
+ variable_cache(:cached_value_before_type_cast) { read_value_before_type_cast }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end