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
@@ -0,0 +1,26 @@
1
+ require 'granite/form/model/attributes/reflections/localized'
2
+ require 'granite/form/model/attributes/localized'
3
+
4
+ module Granite
5
+ module Form
6
+ module Model
7
+ module Localization
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ def localized(*args, &block)
12
+ add_attribute(Granite::Form::Model::Attributes::Reflections::Localized, *args, &block)
13
+ end
14
+
15
+ def fallbacks(locale)
16
+ ::I18n.respond_to?(:fallbacks) ? ::I18n.fallbacks[locale] : [locale]
17
+ end
18
+
19
+ def locale
20
+ I18n.locale
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Persistence
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def instantiate(data)
9
+ data = data.stringify_keys
10
+ instance = allocate
11
+
12
+ instance.instance_variable_set(:@initial_attributes, data.slice(*attribute_names))
13
+ instance.send(:mark_persisted!)
14
+
15
+ instance
16
+ end
17
+
18
+ def instantiate_collection(data)
19
+ collection = Array.wrap(data).map { |attrs| instantiate attrs }
20
+ collection = scope(collection, true) if respond_to?(:scope)
21
+ collection
22
+ end
23
+ end
24
+
25
+ def persisted?
26
+ !!@persisted
27
+ end
28
+
29
+ def destroyed?
30
+ !!@destroyed
31
+ end
32
+
33
+ def marked_for_destruction?
34
+ @marked_for_destruction
35
+ end
36
+
37
+ def mark_for_destruction
38
+ @marked_for_destruction = true
39
+ end
40
+
41
+ def _destroy
42
+ marked_for_destruction?
43
+ end
44
+
45
+ private
46
+
47
+ def mark_persisted!
48
+ @persisted = true
49
+ @destroyed = false
50
+ end
51
+
52
+ def mark_destroyed!
53
+ @persisted = false
54
+ @destroyed = true
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,59 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Primary
5
+ extend ActiveSupport::Concern
6
+ DEFAULT_PRIMARY_ATTRIBUTE_OPTIONS = lambda do
7
+ {
8
+ type: Granite::Form::UUID,
9
+ default: -> { Granite::Form::UUID.random_create }
10
+ }
11
+ end
12
+
13
+ included do
14
+ class_attribute :_primary_name, instance_writer: false
15
+ delegate :has_primary_attribute?, to: 'self.class'
16
+
17
+ prepend PrependMethods
18
+ alias_method :eql?, :==
19
+ end
20
+
21
+ module ClassMethods
22
+ def primary(*args)
23
+ options = args.extract_options!
24
+ self._primary_name = (args.first.presence || Granite::Form.primary_attribute).to_s
25
+ unless has_attribute?(_primary_name)
26
+ options[:type] = args.second if args.second
27
+ attribute _primary_name, options.presence || DEFAULT_PRIMARY_ATTRIBUTE_OPTIONS.call
28
+ end
29
+ alias_attribute :primary_attribute, _primary_name
30
+ end
31
+
32
+ alias_method :primary_attribute, :primary
33
+
34
+ def has_primary_attribute? # rubocop:disable Naming/PredicateName
35
+ has_attribute? _primary_name
36
+ end
37
+
38
+ def primary_name
39
+ _primary_name
40
+ end
41
+ end
42
+
43
+ module PrependMethods
44
+ def ==(other)
45
+ if other.instance_of?(self.class) && has_primary_attribute?
46
+ if primary_attribute
47
+ primary_attribute == other.primary_attribute
48
+ else
49
+ object_id == other.object_id
50
+ end
51
+ else
52
+ super(other)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,101 @@
1
+ require 'active_model/version'
2
+ require 'granite/form/model/attributes/reflections/represents'
3
+ require 'granite/form/model/attributes/represents'
4
+
5
+ module Granite
6
+ module Form
7
+ module Model
8
+ module Representation
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ prepend PrependMethods
13
+ end
14
+
15
+ module PrependMethods
16
+ def assign_attributes(attrs)
17
+ if self.class.represented_attributes.present?
18
+ attrs = attrs.to_unsafe_hash if attrs.respond_to?(:to_unsafe_hash)
19
+ attrs = attrs.stringify_keys
20
+ represented_attrs = self.class.represented_names_and_aliases
21
+ .each_with_object({}) do |name, result|
22
+ result[name] = attrs.delete(name) if attrs.key?(name)
23
+ end
24
+
25
+ super(attrs.merge!(represented_attrs))
26
+ else
27
+ super(attrs)
28
+ end
29
+ end
30
+
31
+ alias_method :attributes=, :assign_attributes
32
+ end
33
+
34
+ module ClassMethods
35
+ def represents(*names, &block)
36
+ options = names.extract_options!
37
+ names.each do |name|
38
+ add_attribute(Granite::Form::Model::Attributes::Reflections::Represents, name, options, &block)
39
+ end
40
+ end
41
+
42
+ def represented_attributes
43
+ @represented_attributes ||= _attributes.values.select do |attribute|
44
+ attribute.is_a? Granite::Form::Model::Attributes::Reflections::Represents
45
+ end
46
+ end
47
+
48
+ def represented_names_and_aliases
49
+ @represented_names_and_aliases ||= represented_attributes.flat_map do |attribute|
50
+ [attribute.name, *inverted_attribute_aliases[attribute.name]]
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def run_validations! #:nodoc:
58
+ super
59
+ emerge_represented_attributes_errors!
60
+ errors.empty?
61
+ end
62
+
63
+ # Move represent attribute errors to the top level:
64
+ #
65
+ # {:'role.email' => ['Some error']}
66
+ #
67
+ # to:
68
+ #
69
+ # {email: ['Some error']}
70
+ #
71
+ def emerge_represented_attributes_errors!
72
+ self.class.represented_attributes.each do |attribute|
73
+ move_errors(:"#{attribute.reference}.#{attribute.column}", attribute.column)
74
+ end
75
+ end
76
+
77
+ if ActiveModel.version >= Gem::Version.new('6.1.0')
78
+ def move_errors(from, to)
79
+ errors.where(from).each do |error|
80
+ options = error.options
81
+ # If we generate message for built-in validation, we don't want to later escape it in our monkey-patch
82
+ options = options.merge(message: error.message.html_safe) unless options.key?(:message)
83
+
84
+ errors.add(to, error.type, **options)
85
+ end
86
+
87
+ errors.delete(from)
88
+ end
89
+ else
90
+ # up to 6.0.x
91
+ def move_errors(from, to)
92
+ return unless errors.messages.key?(from) && errors.messages[from].present?
93
+
94
+ errors[to].concat(errors.messages[from])
95
+ errors.delete(from)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,118 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Scopes
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :_scope_base
9
+ scopify
10
+ end
11
+
12
+ module ScopeProxy
13
+ extend ActiveSupport::Concern
14
+
15
+ def self.for(model)
16
+ klass = Class.new(model._scope_base) do
17
+ include Granite::Form::Model::Scopes::ScopeProxy
18
+ end
19
+ klass.define_singleton_method(:_scope_model) { model }
20
+ model.const_set('ScopeProxy', klass)
21
+ end
22
+
23
+ included do
24
+ def initialize(source = nil, trust = false)
25
+ source ||= self.class.superclass.new
26
+
27
+ unless trust && source.is_a?(self.class)
28
+ source.each do |entity|
29
+ raise AssociationTypeMismatch.new(self.class._scope_model, entity.class) unless entity.is_a?(self.class._scope_model)
30
+ end
31
+ end
32
+
33
+ super source
34
+ end
35
+ end
36
+
37
+ def respond_to_missing?(method, _)
38
+ super || self.class._scope_model.respond_to?(method)
39
+ end
40
+
41
+ # rubocop:disable Style/MethodMissing
42
+ # rubocop-0.52.1 doesn't understand that `#respond_to_missing?` is defined above
43
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0')
44
+ def method_missing(method, *args, **kwargs, &block)
45
+ with_scope do
46
+ model = self.class._scope_model
47
+ if model.respond_to?(method)
48
+ result = model.public_send(method, *args, **kwargs, &block)
49
+ result.is_a?(Granite::Form::Model::Scopes) ? result : model.scope_class.new(result)
50
+ else
51
+ super
52
+ end
53
+ end
54
+ end
55
+ elsif Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0')
56
+ def method_missing(method, *args, **kwargs, &block)
57
+ with_scope do
58
+ model = self.class._scope_model
59
+ if model.respond_to?(method)
60
+ model.public_send(method, *args, **kwargs, &block)
61
+ else
62
+ super
63
+ end
64
+ end
65
+ end
66
+ else
67
+ # up to 2.6.x
68
+ def method_missing(method, *args, &block)
69
+ with_scope do
70
+ model = self.class._scope_model
71
+ if model.respond_to?(method)
72
+ model.public_send(method, *args, &block)
73
+ else
74
+ super
75
+ end
76
+ end
77
+ end
78
+ end
79
+ # rubocop:enable Style/MethodMissing
80
+
81
+ def with_scope
82
+ previous_scope = self.class._scope_model.current_scope
83
+ self.class._scope_model.current_scope = self
84
+ result = yield
85
+ self.class._scope_model.current_scope = previous_scope
86
+ result
87
+ end
88
+ end
89
+
90
+ module ClassMethods
91
+ def scopify(scope_base = Array)
92
+ self._scope_base = scope_base
93
+ end
94
+
95
+ def scope_class
96
+ @scope_class ||= Granite::Form::Model::Scopes::ScopeProxy.for(self)
97
+ end
98
+
99
+ def scope(*args)
100
+ if args.empty?
101
+ current_scope
102
+ else
103
+ scope_class.new(*args)
104
+ end
105
+ end
106
+
107
+ def current_scope=(value)
108
+ @current_scope = value
109
+ end
110
+
111
+ def current_scope
112
+ @current_scope ||= scope_class.new
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,22 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Validations
5
+ class AssociatedValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ invalid_records = Array.wrap(value).reject do |r|
8
+ r.respond_to?(:valid?) && r.valid?(record.validation_context)
9
+ end
10
+ record.errors.add(attribute, :invalid, **options.merge(value: value)) if invalid_records.present?
11
+ end
12
+ end
13
+
14
+ module HelperMethods
15
+ def validates_associated(*attr_names)
16
+ validates_with AssociatedValidator, _merge_attributes(attr_names)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_model/version'
2
+
3
+ module Granite
4
+ module Form
5
+ module Model
6
+ module Validations
7
+ class NestedValidator < ActiveModel::EachValidator
8
+ def self.validate_nested(record, name, value)
9
+ if value.is_a?(Enumerable)
10
+ value.each.with_index do |object, i|
11
+ import_errors(object.errors, record.errors, "#{name}.#{i}") if yield object
12
+ end
13
+ elsif value
14
+ import_errors(value.errors, record.errors, name.to_s) if yield value
15
+ end
16
+ end
17
+
18
+ if ActiveModel.version >= Gem::Version.new('6.1.0')
19
+ def self.import_errors(from, to, prefix)
20
+ from.each do |error|
21
+ key = "#{prefix}.#{error.attribute}"
22
+ to.import(error, attribute: key) unless to.added?(key, error.type, error.options)
23
+ end
24
+ end
25
+ else
26
+ # up to 6.0.x
27
+ def self.import_errors(from, to, prefix)
28
+ from.each do |key, message|
29
+ key = "#{prefix}.#{key}"
30
+ to[key] << message
31
+ to[key].uniq!
32
+ end
33
+ end
34
+ end
35
+
36
+ def validate_each(record, attribute, value)
37
+ self.class.validate_nested(record, attribute, value) do |object|
38
+ object.invalid? && !(object.respond_to?(:marked_for_destruction?) && object.marked_for_destruction?)
39
+ end
40
+ end
41
+ end
42
+
43
+ module HelperMethods
44
+ def validates_nested(*attr_names)
45
+ validates_with NestedValidator, _merge_attributes(attr_names)
46
+ end
47
+
48
+ def validates_nested?(attr)
49
+ _validators[attr.to_sym]
50
+ .grep(Granite::Form::Model::Validations::NestedValidator).present?
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,29 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Validations
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::Validations
7
+
8
+ included do
9
+ extend HelperMethods
10
+ include HelperMethods
11
+
12
+ alias_method :validate, :valid?
13
+ end
14
+
15
+ def validate!(context = nil)
16
+ valid?(context) || raise_validation_error
17
+ end
18
+
19
+ protected
20
+
21
+ def raise_validation_error
22
+ raise Granite::Form::ValidationError, self
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ Dir[File.dirname(__FILE__) + '/validations/*.rb'].each { |file| require file }
@@ -0,0 +1,33 @@
1
+ require 'granite/form/model/conventions'
2
+ require 'granite/form/model/attributes'
3
+ require 'granite/form/model/validations'
4
+ require 'granite/form/model/scopes'
5
+ require 'granite/form/model/primary'
6
+ require 'granite/form/model/lifecycle'
7
+ require 'granite/form/model/persistence'
8
+ require 'granite/form/model/callbacks'
9
+ require 'granite/form/model/associations'
10
+ require 'granite/form/model/localization'
11
+ require 'granite/form/model/representation'
12
+ require 'granite/form/model/dirty'
13
+
14
+ module Granite
15
+ module Form
16
+ module Model
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ extend ActiveModel::Naming
21
+ extend ActiveModel::Translation
22
+
23
+ include ActiveModel::Conversion
24
+ include ActiveModel::Serialization
25
+ include ActiveModel::Serializers::JSON
26
+
27
+ include Conventions
28
+ include Attributes
29
+ include Validations
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Granite
2
+ module Form
3
+ class Railtie < Rails::Railtie
4
+ initializer 'granite.logger', after: 'active_record.logger' do
5
+ ActiveSupport.on_load(:active_record) { Granite::Form.logger ||= ActiveRecord::Base.logger }
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require 'singleton'
2
+
3
+ module Granite
4
+ module Form
5
+ class UndefinedClass
6
+ include Singleton
7
+ end
8
+
9
+ UNDEFINED = UndefinedClass.instance.freeze
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Granite
2
+ module Form
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,163 @@
1
+ require 'tzinfo'
2
+ require 'active_support'
3
+ require 'active_support/deprecation'
4
+ require 'active_support/core_ext'
5
+ require 'active_support/concern'
6
+ require 'singleton'
7
+
8
+ require 'active_model'
9
+
10
+ require 'granite/form/version'
11
+ require 'granite/form/errors'
12
+ require 'granite/form/extensions'
13
+ require 'granite/form/undefined_class'
14
+ require 'granite/form/config'
15
+ require 'granite/form/railtie' if defined? Rails
16
+ require 'granite/form/model'
17
+ require 'granite/form/model/associations/persistence_adapters/base'
18
+ require 'granite/form/model/associations/persistence_adapters/active_record'
19
+
20
+ module Granite
21
+ module Form
22
+ BOOLEAN_MAPPING = {
23
+ 1 => true,
24
+ 0 => false,
25
+ '1' => true,
26
+ '0' => false,
27
+ 't' => true,
28
+ 'f' => false,
29
+ 'T' => true,
30
+ 'F' => false,
31
+ true => true,
32
+ false => false,
33
+ 'true' => true,
34
+ 'false' => false,
35
+ 'TRUE' => true,
36
+ 'FALSE' => false,
37
+ 'y' => true,
38
+ 'n' => false,
39
+ 'yes' => true,
40
+ 'no' => false
41
+ }.freeze
42
+
43
+ def self.config
44
+ Granite::Form::Config.instance
45
+ end
46
+
47
+ singleton_class.delegate(*Granite::Form::Config.delegated, to: :config)
48
+
49
+ typecaster('Object') { |value, attribute| value if value.class < attribute.type }
50
+ typecaster('String') { |value, _| value.to_s }
51
+ typecaster('Array') do |value|
52
+ case value
53
+ when ::Array then
54
+ value
55
+ when ::String then
56
+ value.split(',').map(&:strip)
57
+ end
58
+ end
59
+ typecaster('Hash') do |value|
60
+ case value
61
+ when ::Hash then
62
+ value
63
+ end
64
+ end
65
+ ActiveSupport.on_load :action_controller do
66
+ Granite::Form.typecaster('Hash') do |value|
67
+ case value
68
+ when ActionController::Parameters
69
+ value.to_h if value.permitted?
70
+ when ::Hash then
71
+ value
72
+ end
73
+ end
74
+ end
75
+ typecaster('Date') do |value|
76
+ begin
77
+ value.to_date
78
+ rescue ArgumentError, NoMethodError
79
+ nil
80
+ end
81
+ end
82
+ typecaster('DateTime') do |value|
83
+ begin
84
+ value.to_datetime
85
+ rescue ArgumentError
86
+ nil
87
+ end
88
+ end
89
+ typecaster('Time') do |value|
90
+ begin
91
+ value.is_a?(String) && ::Time.zone ? ::Time.zone.parse(value) : value.to_time
92
+ rescue ArgumentError
93
+ nil
94
+ end
95
+ end
96
+ typecaster('ActiveSupport::TimeZone') do |value|
97
+ case value
98
+ when ActiveSupport::TimeZone
99
+ value
100
+ when ::TZInfo::Timezone
101
+ ActiveSupport::TimeZone[value.name]
102
+ when String, Numeric, ActiveSupport::Duration
103
+ value = begin
104
+ Float(value)
105
+ rescue ArgumentError, TypeError
106
+ value
107
+ end
108
+ ActiveSupport::TimeZone[value]
109
+ end
110
+ end
111
+ typecaster('BigDecimal') do |value|
112
+ next unless value
113
+ begin
114
+ BigDecimal(Float(value).to_s)
115
+ rescue ArgumentError, TypeError
116
+ nil
117
+ end
118
+ end
119
+ typecaster('Float') do |value|
120
+ begin
121
+ Float(value)
122
+ rescue ArgumentError, TypeError
123
+ nil
124
+ end
125
+ end
126
+ typecaster('Integer') do |value|
127
+ begin
128
+ Float(value).to_i
129
+ rescue ArgumentError, TypeError
130
+ nil
131
+ end
132
+ end
133
+ typecaster('Boolean') { |value| BOOLEAN_MAPPING[value] }
134
+ typecaster('Granite::Form::UUID') do |value|
135
+ case value
136
+ when UUIDTools::UUID
137
+ Granite::Form::UUID.parse_raw value.raw
138
+ when Granite::Form::UUID
139
+ value
140
+ when String
141
+ Granite::Form::UUID.parse_string value
142
+ when Integer
143
+ Granite::Form::UUID.parse_int value
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ require 'granite/form/base'
150
+
151
+ Granite::Form.base_class = Granite::Form::Base
152
+
153
+ ActiveSupport.on_load :active_record do
154
+ require 'granite/form/active_record/associations'
155
+ require 'granite/form/active_record/nested_attributes'
156
+
157
+ include Granite::Form::ActiveRecord::Associations
158
+ singleton_class.prepend Granite::Form::ActiveRecord::NestedAttributes
159
+
160
+ def self.granite_persistence_adapter
161
+ Granite::Form::Model::Associations::PersistenceAdapters::ActiveRecord
162
+ end
163
+ end