nueca_rails_interfaces 1.1.2 → 1.2.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbcdb703d90c83096aa868e52808ce08c4801267cca85927ae1299cf70f0ed32
4
- data.tar.gz: ea0af8ccfca4f1561382d5e25ee97ff4ca4b95d5702d4ad5957bf80ab80284b4
3
+ metadata.gz: 4666de0cb174c7bfac30b5efbade4c91b77e974747e15ff18e5b7159f3e557fc
4
+ data.tar.gz: 4b487712a93e076adf1431c5fcaa5ed535cdb189b1c87917779b0b8c668d83e3
5
5
  SHA512:
6
- metadata.gz: 66f4e97c4d687815f825d0fc4c22d529f716846b4f845415b0f5f35784140cef97cd159b30617e009484f84b9c3ddec80bbdc8e1b8e502ee82eae1e52d49a5e8
7
- data.tar.gz: 859e241733b214d7c168f0ab0a020e906eab24179a60d78bf777146cb30f617b62c247f710797a45936528c8795731a453091d62c69d2d049933dd1fbba76fed
6
+ metadata.gz: d5b80763ac2da7bf9720b564716d5f8b1e0bb578f43cad3e7b2ecd8b526393a72a0d682cb4eb2b32936f8c3503e8f76f226e87695db467b3516689c6518bfc47
7
+ data.tar.gz: 9468e0254e8ad17fe0c90e88a477f739718ca32263179a9bac51ab7f8bcafd82231a0c1bfb242777ef76da13ffde2e06a7a49d44f1b5e356bc394c8d2c162328
data/.rubocop.yml CHANGED
@@ -1,17 +1,9 @@
1
1
  plugins:
2
- - rubocop-rails
2
+ - rubocop-nueca
3
3
  - rubocop-rake
4
- - rubocop-rspec
5
4
 
6
- AllCops:
7
- TargetRubyVersion: 3.3
8
- NewCops: enable
9
-
10
- Layout/LineLength:
11
- Max: 120
12
-
13
- RSpec/NestedGroups:
14
- Max: 5
5
+ Style/ClassMethodsDefinitions:
6
+ Enabled: false
15
7
 
16
- RSpec/VerifiedDoubles:
8
+ Style/OptionHash:
17
9
  Enabled: false
data/Rakefile CHANGED
@@ -9,4 +9,4 @@ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[spec rubocop]
12
+ task default: [:spec, :rubocop]
@@ -86,7 +86,7 @@ module NuecaRailsInterfaces
86
86
 
87
87
  # Paginates the collection based on query or settings.
88
88
  def apply_pagination!
89
- raise 'Invalid pagination settings.' unless correct_pagination_settings?
89
+ raise 'Invalid pagination settings.' unless correct_pagination_settings? # rubocop:disable Style/ImplicitRuntimeError
90
90
  return unless @pagination_flag
91
91
 
92
92
  @collection = collection.paginate(page: fetch_page_value, per_page: fetch_per_page_value)
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NuecaRailsInterfaces
4
+ module V3
5
+ # V3 Form Interface delegates model validation to the actual model instances
6
+ # rather than reimplementing model validations inside the form.
7
+ #
8
+ # Models managed by the form are declared explicitly with the `model` macro.
9
+ # Their attributes arrive as a nested hash keyed by the model name, which is
10
+ # what `fields_for :school_year, f.object.school_year` produces in the view —
11
+ # e.g. `school_year: { name: "..." }`. This maps directly to:
12
+ # `params.expect(my_form: [{ school_year: [:name] }, :other_field])`
13
+ #
14
+ # Only models listed in the `model` macro are processed; any key whose name
15
+ # matches a declared model and whose value is a Hash (or Parameters) is treated
16
+ # as model attributes. Everything else is treated as a regular form attribute.
17
+ # Fields inside a model hash that the model does not recognise are silently
18
+ # discarded.
19
+ #
20
+ # Custom form-level fields (fields that belong to the form, not to any model)
21
+ # are declared as plain `attr_accessor`s on the form class, just like any other
22
+ # ActiveModel attribute.
23
+ #
24
+ # Calling `valid?` on the form runs form-level validations first, then calls
25
+ # `valid?` on each model instance and propagates any model errors onto the form
26
+ # under the model name key (e.g. `errors[:payment]`). `fields_for :payment`
27
+ # in the view binds to `form.payment` and reads that object's own errors for
28
+ # per-field error rendering, so both levels work naturally together.
29
+ #
30
+ # The `attributes` method returns a hash of model instances merged with any
31
+ # regular form attributes. It can be overridden in the form class.
32
+ module FormInterface
33
+ # Prepended so these methods precede ActiveModel::Model's versions in the
34
+ # method resolution order. When `base.include(ActiveModel::Model)` is called
35
+ # inside the `included` hook, ActiveModel ends up earlier in the ancestor chain,
36
+ # which would otherwise cause ActiveModel::API#initialize to intercept `new`
37
+ # before our param-splitting logic runs.
38
+ module InstanceMethods
39
+ def initialize(options = {})
40
+ @model_instances = {}
41
+ @regular_attributes = extract_regular_attributes(options)
42
+
43
+ # Pre-instantiate declared models that were not present in the params
44
+ # (e.g. the form is used in a `new` action with no input yet).
45
+ (self.class.declared_models - @model_instances.keys).each do |model_sym|
46
+ @model_instances[model_sym] = model_sym.to_s.camelize.constantize.new
47
+ end
48
+
49
+ super(**@regular_attributes)
50
+ end
51
+
52
+ def valid?(context = nil)
53
+ # Clears errors and runs form-level validations.
54
+ super
55
+
56
+ error_key = self.class.flatten_errors? ? :base : nil
57
+
58
+ @model_instances.each do |model_name, model_instance|
59
+ next if model_instance.valid?
60
+
61
+ model_instance.errors.each do |error|
62
+ errors.add(error_key || model_name, error.full_message)
63
+ end
64
+ end
65
+
66
+ errors.empty?
67
+ end
68
+
69
+ def method_missing(name, *args, &)
70
+ return @model_instances[name] if @model_instances.key?(name)
71
+
72
+ super
73
+ end
74
+
75
+ def respond_to_missing?(name, include_private = false)
76
+ @model_instances.key?(name) || super
77
+ end
78
+
79
+ private
80
+
81
+ # Builds the model instance from the given attributes hash, assigning only
82
+ # fields the model recognises. Unknown fields are silently discarded.
83
+ # `attrs` may be a plain Hash or ActionController::Parameters — both are
84
+ # normalised to a plain Hash before processing; field whitelisting via
85
+ # Splits options into model instances and regular form attributes.
86
+ # Hash-valued keys matching a declared model are built into model instances.
87
+ # Hash-valued keys for undeclared models are silently discarded.
88
+ # All other (scalar) keys are returned as regular attributes for super.
89
+ def extract_regular_attributes(options)
90
+ regular_attrs = {}
91
+ options.each do |key, value|
92
+ if self.class.declared_models.include?(key.to_sym) && value.respond_to?(:each_pair)
93
+ build_model_instance(key.to_sym, value)
94
+ elsif !value.respond_to?(:each_pair)
95
+ regular_attrs[key] = value
96
+ end
97
+ end
98
+ regular_attrs
99
+ end
100
+
101
+ # `model_field?` provides the equivalent of strong-param filtering here.
102
+ def build_model_instance(model_name, attrs)
103
+ model_class = model_name.to_s.camelize.constantize
104
+ attrs_hash = attrs.respond_to?(:to_unsafe_h) ? attrs.to_unsafe_h : attrs.to_h
105
+ model_attrs = attrs_hash.each_with_object({}) do |(field, value), hash|
106
+ hash[field.to_sym] = value if model_field?(model_class, field.to_s)
107
+ end
108
+ @model_instances[model_name] = model_class.new(**model_attrs)
109
+ end
110
+
111
+ # A field belongs to the model if the model class declares it via
112
+ # `attribute_names` (ActiveRecord) or has an assignment method
113
+ # (ActiveModel attr_accessor, etc.).
114
+ def model_field?(model_class, field_name)
115
+ (model_class.respond_to?(:attribute_names) && model_class.attribute_names.include?(field_name)) ||
116
+ model_class.method_defined?("#{field_name}=")
117
+ end
118
+ end
119
+
120
+ class << self
121
+ def included(base)
122
+ base.include(ActiveModel::Model)
123
+ base.prepend(InstanceMethods)
124
+ define_class_macros(base)
125
+ end
126
+
127
+ private
128
+
129
+ def define_class_macros(base)
130
+ base.instance_variable_set(:@declared_models, [])
131
+ base.instance_variable_set(:@flatten_errors, false)
132
+
133
+ base.define_singleton_method(:flatten_errors) { @flatten_errors = true }
134
+ base.define_singleton_method(:flatten_errors?) { @flatten_errors }
135
+
136
+ # Declares one or more models managed by this form. Each symbol must be
137
+ # the underscored model name (e.g. `model :payment`, `model :school_year`).
138
+ # Only declared models are processed from params. Declared models are always instantiated
139
+ # (with blank attributes) even when their params are absent.
140
+ base.define_singleton_method(:model) do |*model_names|
141
+ model_names.each { |n| @declared_models << n.to_sym }
142
+ end
143
+
144
+ base.define_singleton_method(:declared_models) { @declared_models }
145
+
146
+ base.define_singleton_method(:check) do |*arguments|
147
+ instance = NuecaRailsInterfaces::Util.process_class_arguments(self, *arguments)
148
+ instance.valid?
149
+ instance
150
+ end
151
+ end
152
+ end
153
+
154
+ # Returns a merged hash of model instances and regular form attributes.
155
+ # Defined here (included, not prepended) so that a form class can override it
156
+ # by defining its own `attributes` method — the class-level definition is found
157
+ # first in the MRO, falling back to this default when no override exists.
158
+ def attributes
159
+ result = {}
160
+ result.merge!(@model_instances)
161
+ result.merge!(@regular_attributes.transform_keys(&:to_sym))
162
+ result.with_indifferent_access
163
+ end
164
+ end
165
+ end
166
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NuecaRailsInterfaces
4
- VERSION = '1.1.2'
4
+ VERSION = '1.2.8'
5
5
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- raise 'Rails not found' unless defined?(Rails)
3
+ raise 'Rails not found' unless defined?(Rails) # rubocop:disable Style/ImplicitRuntimeError
4
4
 
5
5
  require 'to_bool'
6
6
 
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.files = Dir.chdir(__dir__) do
17
17
  `git ls-files -z`.split("\x0").reject do |f|
18
18
  (File.expand_path(f) == __FILE__) ||
19
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .gitlab-ci.yml appveyor Gemfile])
19
+ f.start_with?('bin/', 'test/', 'spec/', 'features/', '.git', '.gitlab-ci.yml', 'appveyor', 'Gemfile')
20
20
  end
21
21
  end
22
22
  spec.bindir = 'exe'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nueca_rails_interfaces
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tien
@@ -64,6 +64,7 @@ files:
64
64
  - lib/nueca_rails_interfaces/v1/service_interface.rb
65
65
  - lib/nueca_rails_interfaces/v2/form_interface.rb
66
66
  - lib/nueca_rails_interfaces/v2/service_interface.rb
67
+ - lib/nueca_rails_interfaces/v3/form_interface.rb
67
68
  - lib/nueca_rails_interfaces/v3/service_interface.rb
68
69
  - lib/nueca_rails_interfaces/version.rb
69
70
  - nueca_rails_interfaces.gemspec
@@ -86,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
87
  - !ruby/object:Gem::Version
87
88
  version: '0'
88
89
  requirements: []
89
- rubygems_version: 3.7.1
90
+ rubygems_version: 4.0.7
90
91
  specification_version: 4
91
92
  summary: Interfaces for known object entities in Rails Development at Nueca.
92
93
  test_files: []