granite-form 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codeclimate.yml +13 -0
- data/.github/workflows/ci.yml +35 -0
- data/.github/workflows/main.yml +29 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.rubocop.yml +64 -0
- data/.rubocop_todo.yml +48 -0
- data/Appraisals +8 -0
- data/CHANGELOG.md +73 -0
- data/Gemfile +8 -0
- data/Guardfile +77 -0
- data/LICENSE +22 -0
- data/README.md +429 -0
- data/Rakefile +6 -0
- data/gemfiles/rails.4.2.gemfile +15 -0
- data/gemfiles/rails.5.0.gemfile +15 -0
- data/gemfiles/rails.5.1.gemfile +15 -0
- data/gemfiles/rails.5.2.gemfile +15 -0
- data/gemfiles/rails.6.0.gemfile +14 -0
- data/gemfiles/rails.6.1.gemfile +14 -0
- data/gemfiles/rails.7.0.gemfile +14 -0
- data/granite-form.gemspec +31 -0
- data/lib/granite/form/active_record/associations.rb +57 -0
- data/lib/granite/form/active_record/nested_attributes.rb +20 -0
- data/lib/granite/form/base.rb +15 -0
- data/lib/granite/form/config.rb +42 -0
- data/lib/granite/form/errors.rb +111 -0
- data/lib/granite/form/extensions.rb +36 -0
- data/lib/granite/form/model/associations/base.rb +97 -0
- data/lib/granite/form/model/associations/collection/embedded.rb +14 -0
- data/lib/granite/form/model/associations/collection/proxy.rb +35 -0
- data/lib/granite/form/model/associations/embeds_any.rb +19 -0
- data/lib/granite/form/model/associations/embeds_many.rb +152 -0
- data/lib/granite/form/model/associations/embeds_one.rb +112 -0
- data/lib/granite/form/model/associations/nested_attributes.rb +215 -0
- data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb +33 -0
- data/lib/granite/form/model/associations/persistence_adapters/active_record.rb +68 -0
- data/lib/granite/form/model/associations/persistence_adapters/base.rb +55 -0
- data/lib/granite/form/model/associations/references_any.rb +43 -0
- data/lib/granite/form/model/associations/references_many.rb +113 -0
- data/lib/granite/form/model/associations/references_one.rb +88 -0
- data/lib/granite/form/model/associations/reflections/base.rb +92 -0
- data/lib/granite/form/model/associations/reflections/embeds_any.rb +52 -0
- data/lib/granite/form/model/associations/reflections/embeds_many.rb +17 -0
- data/lib/granite/form/model/associations/reflections/embeds_one.rb +19 -0
- data/lib/granite/form/model/associations/reflections/references_any.rb +65 -0
- data/lib/granite/form/model/associations/reflections/references_many.rb +30 -0
- data/lib/granite/form/model/associations/reflections/references_one.rb +32 -0
- data/lib/granite/form/model/associations/reflections/singular.rb +37 -0
- data/lib/granite/form/model/associations/validations.rb +41 -0
- data/lib/granite/form/model/associations.rb +120 -0
- data/lib/granite/form/model/attributes/attribute.rb +75 -0
- data/lib/granite/form/model/attributes/base.rb +134 -0
- data/lib/granite/form/model/attributes/collection.rb +19 -0
- data/lib/granite/form/model/attributes/dictionary.rb +28 -0
- data/lib/granite/form/model/attributes/localized.rb +44 -0
- data/lib/granite/form/model/attributes/reference_many.rb +21 -0
- data/lib/granite/form/model/attributes/reference_one.rb +52 -0
- data/lib/granite/form/model/attributes/reflections/attribute.rb +61 -0
- data/lib/granite/form/model/attributes/reflections/base.rb +62 -0
- data/lib/granite/form/model/attributes/reflections/collection.rb +12 -0
- data/lib/granite/form/model/attributes/reflections/dictionary.rb +15 -0
- data/lib/granite/form/model/attributes/reflections/localized.rb +45 -0
- data/lib/granite/form/model/attributes/reflections/reference_many.rb +12 -0
- data/lib/granite/form/model/attributes/reflections/reference_one.rb +49 -0
- data/lib/granite/form/model/attributes/reflections/represents.rb +56 -0
- data/lib/granite/form/model/attributes/represents.rb +67 -0
- data/lib/granite/form/model/attributes.rb +204 -0
- data/lib/granite/form/model/callbacks.rb +72 -0
- data/lib/granite/form/model/conventions.rb +40 -0
- data/lib/granite/form/model/dirty.rb +84 -0
- data/lib/granite/form/model/lifecycle.rb +309 -0
- data/lib/granite/form/model/localization.rb +26 -0
- data/lib/granite/form/model/persistence.rb +59 -0
- data/lib/granite/form/model/primary.rb +59 -0
- data/lib/granite/form/model/representation.rb +101 -0
- data/lib/granite/form/model/scopes.rb +118 -0
- data/lib/granite/form/model/validations/associated.rb +22 -0
- data/lib/granite/form/model/validations/nested.rb +56 -0
- data/lib/granite/form/model/validations.rb +29 -0
- data/lib/granite/form/model.rb +33 -0
- data/lib/granite/form/railtie.rb +9 -0
- data/lib/granite/form/undefined_class.rb +11 -0
- data/lib/granite/form/version.rb +5 -0
- data/lib/granite/form.rb +163 -0
- data/spec/lib/granite/form/active_record/associations_spec.rb +211 -0
- data/spec/lib/granite/form/active_record/nested_attributes_spec.rb +15 -0
- data/spec/lib/granite/form/config_spec.rb +66 -0
- data/spec/lib/granite/form/model/associations/embeds_many_spec.rb +706 -0
- data/spec/lib/granite/form/model/associations/embeds_one_spec.rb +533 -0
- data/spec/lib/granite/form/model/associations/nested_attributes_spec.rb +119 -0
- data/spec/lib/granite/form/model/associations/persistence_adapters/active_record_spec.rb +58 -0
- data/spec/lib/granite/form/model/associations/references_many_spec.rb +572 -0
- data/spec/lib/granite/form/model/associations/references_one_spec.rb +445 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_any_spec.rb +42 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_many_spec.rb +145 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_one_spec.rb +117 -0
- data/spec/lib/granite/form/model/associations/reflections/references_many_spec.rb +303 -0
- data/spec/lib/granite/form/model/associations/reflections/references_one_spec.rb +287 -0
- data/spec/lib/granite/form/model/associations/validations_spec.rb +137 -0
- data/spec/lib/granite/form/model/associations_spec.rb +198 -0
- data/spec/lib/granite/form/model/attributes/attribute_spec.rb +186 -0
- data/spec/lib/granite/form/model/attributes/base_spec.rb +97 -0
- data/spec/lib/granite/form/model/attributes/collection_spec.rb +72 -0
- data/spec/lib/granite/form/model/attributes/dictionary_spec.rb +100 -0
- data/spec/lib/granite/form/model/attributes/localized_spec.rb +103 -0
- data/spec/lib/granite/form/model/attributes/reflections/attribute_spec.rb +72 -0
- data/spec/lib/granite/form/model/attributes/reflections/base_spec.rb +56 -0
- data/spec/lib/granite/form/model/attributes/reflections/collection_spec.rb +37 -0
- data/spec/lib/granite/form/model/attributes/reflections/dictionary_spec.rb +43 -0
- data/spec/lib/granite/form/model/attributes/reflections/localized_spec.rb +37 -0
- data/spec/lib/granite/form/model/attributes/reflections/represents_spec.rb +70 -0
- data/spec/lib/granite/form/model/attributes/represents_spec.rb +85 -0
- data/spec/lib/granite/form/model/attributes_spec.rb +350 -0
- data/spec/lib/granite/form/model/callbacks_spec.rb +337 -0
- data/spec/lib/granite/form/model/conventions_spec.rb +11 -0
- data/spec/lib/granite/form/model/dirty_spec.rb +84 -0
- data/spec/lib/granite/form/model/lifecycle_spec.rb +356 -0
- data/spec/lib/granite/form/model/persistence_spec.rb +46 -0
- data/spec/lib/granite/form/model/primary_spec.rb +84 -0
- data/spec/lib/granite/form/model/representation_spec.rb +139 -0
- data/spec/lib/granite/form/model/scopes_spec.rb +86 -0
- data/spec/lib/granite/form/model/typecasting_spec.rb +193 -0
- data/spec/lib/granite/form/model/validations/associated_spec.rb +102 -0
- data/spec/lib/granite/form/model/validations/nested_spec.rb +164 -0
- data/spec/lib/granite/form/model/validations_spec.rb +31 -0
- data/spec/lib/granite/form/model_spec.rb +10 -0
- data/spec/lib/granite/form_spec.rb +11 -0
- data/spec/shared/nested_attribute_examples.rb +332 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/support/model_helpers.rb +10 -0
- data/spec/support/muffle_helper.rb +7 -0
- metadata +403 -0
|
@@ -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
|