Skip to content

Commit

Permalink
Introduce new form object
Browse files Browse the repository at this point in the history
  • Loading branch information
pyromaniac committed Feb 24, 2024
1 parent 1c1c6ab commit 65c4fbd
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Metrics/AbcSize:
# Offense count: 1
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 163
Max: 146

# Offense count: 1
# Configuration parameters: CountComments, CountAsOne.
Expand Down
1 change: 1 addition & 0 deletions lib/operations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
require "operations/form/base"
require "operations/form/attribute"
require "operations/form/builder"
require "operations/form/deprecated_legacy_model_map_implementation"
require "operations/command"
require "operations/result"

Expand Down
9 changes: 2 additions & 7 deletions lib/operations/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,7 @@ def sentry_context
option :preconditions, Operations::Types::Array.of(Operations::Types.Interface(:call)), default: -> { [] }
option :on_success, Operations::Types::Array.of(Operations::Types.Interface(:call)), default: -> { [] }
option :on_failure, Operations::Types::Array.of(Operations::Types.Interface(:call)), default: -> { [] }
option :form_model_map, Operations::Types::Hash.map(
Operations::Types::Coercible::Array.of(
Operations::Types::String | Operations::Types::Symbol | Operations::Types.Instance(Regexp)
),
Operations::Types::String
), default: proc { {} }
option :form_model_map, Operations::Form::DeprecatedLegacyModelMapImplementation::TYPE, default: proc { {} }
option :form_base, Operations::Types::Class, default: proc { ::Operations::Form::Base }
option :form_class, Operations::Types::Class.optional, default: proc {}, reader: false
option :form_hydrator, Operations::Types.Interface(:call), default: proc { FORM_HYDRATOR }
Expand Down Expand Up @@ -391,7 +386,7 @@ def build_form_class
.new(base_class: form_base)
.build(
key_map: contract.class.schema.key_map,
model_map: form_model_map,
model_map: Operations::Form::DeprecatedLegacyModelMapImplementation.new(form_model_map),
namespace: operation.class,
class_name: form_class_name
)
Expand Down
47 changes: 46 additions & 1 deletion lib/operations/form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

# Configures and defines a form object factory.
class Operations::Form
include Dry::Equalizer(:key_map, :model_map, :hydrator, :base_class)
include Operations::Inspect.new(:key_map, :model_map, :hydrator, :base_class, :form_class)

# We need to make deprecated inheritance from Operations::Form act exactly the
# same way as from Operations::Form::Base. In order to do this, we are encapsulating all the
# inheritable functionality in 2 modules and removing methods defined in Operations::Form
Expand All @@ -19,6 +22,48 @@ def self.inherited(subclass)
end

subclass.extend Operations::Form::Base::ClassMethods
subclass.include Operations::Form::Base::InstanceMethods
subclass.prepend Operations::Form::Base::InstanceMethods
end

include Dry::Initializer.define(lambda do
param :key_map_source, Operations::Types.Interface(:contract) |
Operations::Types.Interface(:schema) | Operations::Types.Interface(:key_map)
option :model_map, Operations::Types.Interface(:call), optional: true, default: proc {}
option :hydrator, Operations::Types.Interface(:call), optional: true, default: proc {}
option :base_class, Operations::Types::Class, default: proc { ::Operations::Form::Base }
end)

def call(operation_result)
form_class.new(
hydrator.call(form_class, operation_result.params, **operation_result.context),
messages: operation_result.errors.to_h
)
end

def to_hash
{
key_map: key_map,
model_map: model_map.class.name,
hydrator: hydrator.class.name,
base_class: base_class.name
}
end

private

def form_class
@form_class ||= Operations::Form::Builder
.new(base_class: base_class)
.build(key_map: key_map, model_map: model_map)
end

def key_map
@key_map ||= if key_map_source.respond_to?(:contract)
key_map_source.contract.schema.key_map
elsif key_map_source.respond_to?(:schema)
key_map_source.schema.key_map
else
key_map_source.key_map
end
end
end
7 changes: 4 additions & 3 deletions lib/operations/form/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
# legacy UI.
class Operations::Form::Attribute
extend Dry::Initializer
include Dry::Equalizer(:name, :collection, :form, :model_name)
include Dry::Equalizer(:name, :collection, :model_name, :form)
include Operations::Inspect.new(:name, :collection, :model_name, :form)

param :name, type: Operations::Types::Coercible::Symbol
option :collection, type: Operations::Types::Bool, optional: true, default: proc { false }
option :form, type: Operations::Types::Class, optional: true
option :model_name,
type: Operations::Types::String | Operations::Types.Instance(Class).constrained(lt: ActiveRecord::Base),
type: (Operations::Types::String | Operations::Types.Instance(Class).constrained(lt: ActiveRecord::Base)).optional,
optional: true
option :form, type: Operations::Types::Class.optional, optional: true, default: proc {}

def model_type
@model_type ||= owning_model.type_for_attribute(string_name) if model_name
Expand Down
47 changes: 17 additions & 30 deletions lib/operations/form/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,24 @@ def traverse(key_map, model_map, namespace, class_name, path)
form = Class.new(base_class)
namespace.const_set(class_name, form) if namespace && class_name

key_map.each do |key|
key_path = path + [key.name]
key_map.each { |key| define_attribute(form, key, path, model_map) }
form
end

case key
when Dry::Schema::Key::Array
nested_form = traverse(key.member, model_map, form, key.name.to_s.underscore.classify, key_path)
form.attribute(key.name, form: nested_form, collection: true, **model_name(model_map, key_path))
when Dry::Schema::Key::Hash
traverse_hash(form, model_map, key, path)
when Dry::Schema::Key
form.attribute(key.name, **model_name(model_map, key_path))
else
raise "Unknown key_map key: #{key.class}"
end
def define_attribute(form, key, path, model_map)
key_path = path + [key.name]

case key
when Dry::Schema::Key::Array
nested_form = traverse(key.member, model_map, form, key.name.to_s.underscore.classify, key_path)
form.attribute(key.name, form: nested_form, collection: true, model_name: model_map&.call(key_path))
when Dry::Schema::Key::Hash
traverse_hash(form, model_map, key, path)
when Dry::Schema::Key
form.attribute(key.name, model_name: model_map&.call(key_path))
else
raise "Unknown key_map key: #{key.class}"
end

form
end

def traverse_hash(form, model_map, hash_key, path)
Expand All @@ -56,7 +57,7 @@ def traverse_hash(form, model_map, hash_key, path)

key_path = path + [name]
nested_form = traverse(members, model_map, form, name.underscore.camelize, key_path)
form.attribute(name, form: nested_form, collection: collection, **model_name(model_map, key_path))
form.attribute(name, form: nested_form, collection: collection, model_name: model_map&.call(key_path))
end

def specify_form_attributes(hash_key, nested_attributes_suffix, nested_attributes_collection)
Expand All @@ -68,18 +69,4 @@ def specify_form_attributes(hash_key, nested_attributes_suffix, nested_attribute
[hash_key.name, hash_key.members, false]
end
end

def model_name(model_map, path)
_, model_name = model_map.find do |pathspec, _model|
path.size == pathspec.size && path.zip(pathspec).all? do |slug, pattern|
pattern.is_a?(Regexp) ? pattern.match?(slug) : slug == pattern
end
end

if model_name
{ model_name: model_name }
else
{}
end
end
end
26 changes: 26 additions & 0 deletions lib/operations/form/deprecated_legacy_model_map_implementation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

# Traverses the passed {Dry::Schema::KeyMap} and generates
# {Operations::Form::Base} classes on the fly. Handles nested structures.
#
# @see Operations::Form::Base
class Operations::Form::DeprecatedLegacyModelMapImplementation
extend Dry::Initializer

TYPE = Operations::Types::Hash.map(
Operations::Types::Coercible::Array.of(
Operations::Types::String | Operations::Types::Symbol | Operations::Types.Instance(Regexp)
),
Operations::Types::String
)

param :model_map_hash, TYPE, default: proc { {} }

def call(path)
model_map_hash.find do |pathspec, _model|
path.size == pathspec.size && path.zip(pathspec).all? do |slug, pattern|
pattern.is_a?(Regexp) ? pattern.match?(slug) : slug == pattern
end
end&.second
end
end
25 changes: 23 additions & 2 deletions spec/operations/command_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1008,8 +1008,29 @@ def call; end
subject(:pretty_inspect) { command.pretty_inspect }

specify do
expect(pretty_inspect)
.to match(%r{\A#<Operations::Command\n operation=[^,]+,\n contract=[^,]+,\n policies=[^,]+,\n})
expect(pretty_inspect.gsub(%r{Proc:0x[^>]+}, "Proc:0x").gsub(%r{Class:0x[^>]+}, "Class:0x")).to eq(<<~INSPECT)
#<Operations::Command
operation=#<Proc:0x>,
contract=#<#<Class:0x> schema=#<Dry::Schema::Processor keys=[:name] rules={:name=>"key?(:name) \
AND key[name](str? AND filled?)"}> rules=[#<Dry::Validation::Rule keys=[]>]>,
policies=[#<Proc:0x>],
idempotency=[],
preconditions=[#<Proc:0x>],
on_success=[#<Proc:0x>],
on_failure=[#<Proc:0x>],
form_model_map={},
form_base=#<Class attributes={}>,
form_class=#<Class
attributes={:name=>
#<Operations::Form::Attribute
name=:name,
collection=false,
model_name=nil,
form=nil>}>,
form_hydrator=#<Proc:0x>,
configuration=#<Operations::Configuration info_reporter=nil \
error_reporter=#<Proc:0x> transaction=#<Proc:0x> after_commit=#<Proc:0x>>>
INSPECT
end
end
end
63 changes: 49 additions & 14 deletions spec/operations/form/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,47 @@
expect(pretty_inspect).to eq(<<~INSPECT)
#<Class
attributes={:name=>
#<Operations::Form::Attribute name=:name collection=false form=nil model_name=nil>,
#<Operations::Form::Attribute
name=:name,
collection=false,
model_name=nil,
form=nil>,
:tags=>
#<Operations::Form::Attribute name=:tags collection=true form=nil model_name=nil>,
#<Operations::Form::Attribute
name=:tags,
collection=true,
model_name=nil,
form=nil>,
:author=>
#<Operations::Form::Attribute name=:author collection=false form=Dummy::Author model_name=nil>,
#<Operations::Form::Attribute
name=:author,
collection=false,
model_name=nil,
form=#<Class
attributes={:title=>
#<Operations::Form::Attribute
name=:title,
collection=false,
model_name=nil,
form=nil>}>>,
:posts=>
#<Operations::Form::Attribute name=:posts collection=true form=Dummy::Post model_name=nil>}>
#<Operations::Form::Attribute
name=:posts,
collection=true,
model_name=nil,
form=#<Class
attributes={:title=>
#<Operations::Form::Attribute
name=:title,
collection=false,
model_name=nil,
form=nil>,
:text=>
#<Operations::Form::Attribute
name=:text,
collection=false,
model_name=nil,
form=nil>}>>}>
INSPECT
end
end
Expand Down Expand Up @@ -361,16 +395,17 @@
subject(:pretty_inspect) { form.pretty_inspect }

specify do
expect(pretty_inspect).to match(%r{
#<Dummy::Form \
\sattributes=\{:name=>nil, \
\s\s:tags=>\[], \
\s\s:author=>\n\s\s\s#<Dummy::Author \
\s\s\s\sattributes=\{:title=>nil\}, \
\s\s\s\serrors=#<ActiveModel::Errors \[.*?\]>>, \
\s\s:posts=>\[\]\}, \
\serrors=#<ActiveModel::Errors \[.*?\]>>
}x)
expect(pretty_inspect.gsub(%r{ActiveModel::Errors[^>]+}, "ActiveModel::Errors")).to eq(<<~INSPECT)
#<Dummy::Form
attributes={:name=>nil,
:tags=>[],
:author=>
#<Dummy::Author
attributes={:title=>nil},
errors=#<ActiveModel::Errors>>,
:posts=>[]},
errors=#<ActiveModel::Errors>>
INSPECT
end
end
end
Expand Down
5 changes: 3 additions & 2 deletions spec/operations/form/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
end
end
let(:namespace) { stub_const("DummyNamespace", Module.new) }
let(:model_map) { { ["name"] => "Dummy1", ["translations", %r{singular|plural}] => "Dummy2" } }
let(:model_map_hash) { { ["name"] => "Dummy1", ["translations", %r{singular|plural}] => "Dummy2" } }
let(:model_map) { Operations::Form::DeprecatedLegacyModelMapImplementation.new(model_map_hash) }

it "defines attributes tree correctly" do
expect(form_class).to be < base_class
Expand Down Expand Up @@ -104,7 +105,7 @@
key_map: schema.key_map,
namespace: namespace,
class_name: "MyForm",
model_map: {}
model_map: nil
)
end

Expand Down
Loading

0 comments on commit 65c4fbd

Please sign in to comment.