granite-form 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -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
|
data/lib/granite/form.rb
ADDED
@@ -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
|