Skip to content

Commit

Permalink
Improve form object attributes model referencing
Browse files Browse the repository at this point in the history
  • Loading branch information
pyromaniac committed Aug 7, 2024
1 parent 6f779ea commit 6329482
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 58 deletions.
4 changes: 2 additions & 2 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2024-08-06 04:56:51 UTC using RuboCop version 1.65.0.
# on 2024-08-07 02:24:58 UTC using RuboCop version 1.65.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand All @@ -27,7 +27,7 @@ Metrics/ClassLength:
# Offense count: 1
# Configuration parameters: CountComments, CountAsOne.
Metrics/ModuleLength:
Max: 140
Max: 141

# Offense count: 3
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Allow referencing and arbitrary model attrbiute from form object attribute with `model_name: "Post#title"` [\#50](https://github.com/BookingSync/operations/pull/50) ([pyromaniac](https://github.com/pyromaniac))
- Allow passing multiple `hydrators:` to Operations::Form [\#49](https://github.com/BookingSync/operations/pull/49) ([pyromaniac](https://github.com/pyromaniac))

### Improvements
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ class ActiveRecordRepository
include Dry::Monads[:result]
extend Dry::Initializer

param :model, type: Types.Instance(Class).constrained(lt: ActiveRecord::Base)
param :model, type: Types::Class.constrained(lt: ActiveRecord::Base)

def create(**attributes)
record = model.new(**attributes)
Expand Down Expand Up @@ -933,8 +933,14 @@ class Post::Update
end

class Post::Update::ModelMap
def call(_path)
Post
MAPPING = {
%w[published_at] => Post, # a model can be passed but beware of circular dependencies, better use strings
%w[title] => "Post", # or a model name - safer option
%w[content] => "Post#body" # referencing different attribute is possible, useful for naming migration or translations
}.freeze

def call(path)
MAPPING[path] # returns the mapping for a single path
end
end
```
Expand Down
36 changes: 16 additions & 20 deletions lib/operations/form/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,35 @@
# legacy UI.
class Operations::Form::Attribute
extend Dry::Initializer
include Dry::Equalizer(:name, :collection, :model_name, :form)
include Operations::Inspect.new(:name, :collection, :model_name, :form)
include Dry::Equalizer(:name, :collection, :model_class, :model_attribute, :form)
include Operations::Inspect.new(:name, :collection, :model_class, :model_attribute, :form)

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

def model_type
@model_type ||= owning_model.type_for_attribute(string_name) if model_name
end
def model_class
return @model_class if defined?(@model_class)

def model_human_name(options = {})
owning_model.human_attribute_name(string_name, options) if model_name
@model_class = model_name.is_a?(String) ? model_name.split("#").first.constantize : model_name
end

def model_validators
@model_validators ||= model_name ? owning_model.validators_on(string_name) : []
end
def model_attribute
return @model_attribute if defined?(@model_attribute)

def model_localized_attr_name(locale)
owning_model.localized_attr_name_for(string_name, locale) if model_name
@model_attribute = model_class && (model_name.to_s.split("#").second.presence || name.to_s)
end

private
def model_type
model_class.type_for_attribute(model_attribute) if model_name
end

def owning_model
@owning_model ||= model_name.is_a?(String) ? model_name.constantize : model_name
def model_human_name(options = {})
model_class.human_attribute_name(model_attribute, options) if model_name
end

def string_name
@string_name ||= name.to_s
def model_validators
model_name ? model_class.validators_on(model_attribute) : []
end
end
9 changes: 5 additions & 4 deletions lib/operations/form/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,13 @@ def reflect_on_association(...); end

# :nodoc:
module InstanceMethods
def type_for_attribute(name)
self.class.attributes[name.to_sym].model_type
# Copied from globalize-accessors, should be deprecated and removed as it is not a core method
def localized_attr_name_for(attr_name, locale)
"#{attr_name}_#{locale.to_s.underscore}"
end

def localized_attr_name_for(name, locale)
self.class.attributes[name.to_sym].model_localized_attr_name(locale)
def type_for_attribute(name)
self.class.attributes[name.to_sym].model_type
end

def has_attribute?(name) # rubocop:disable Naming/PredicateName
Expand Down
3 changes: 2 additions & 1 deletion spec/operations/command_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,8 @@ def call; end
#<Operations::Form::Attribute
name=:name,
collection=false,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=nil>}>,
form_hydrator=#<Proc:0x>,
configuration=#<Operations::Configuration info_reporter=nil \
Expand Down
98 changes: 85 additions & 13 deletions spec/operations/form/attribute_spec.rb
Original file line number Diff line number Diff line change
@@ -1,56 +1,128 @@
# frozen_string_literal: true

RSpec.describe Operations::Form::Attribute do
subject(:attribute) { described_class.new(name, **attribute_options) }
subject(:attribute) { described_class.new(name, **options) }

let(:name) { "name" }
let(:attribute_options) { {} }
let(:options) { {} }

describe "#model_class" do
subject(:model_class) { attribute.model_class }

it { is_expected.to be_nil }

context "with model_name class" do
let(:options) { { model_name: User } }

it { is_expected.to eq User }
end

context "with model_name present" do
let(:options) { { model_name: "User" } }

it { is_expected.to eq User }
end

context "with model_name column present" do
let(:options) { { model_name: "User#age" } }

it { is_expected.to eq User }
end
end

describe "#model_attribute" do
subject(:model_attribute) { attribute.model_attribute }

it { is_expected.to be_nil }

context "with model_name class" do
let(:options) { { model_name: User } }

it { is_expected.to eq "name" }
end

context "with model_name present" do
let(:options) { { model_name: "User" } }

it { is_expected.to eq "name" }
end

context "with model_name column present" do
let(:options) { { model_name: "User#age" } }

it { is_expected.to eq "age" }
end
end

describe "#model_type" do
subject(:model_type) { attribute.model_type }

it { is_expected.to be_nil }

context "with model_name class" do
let(:options) { { model_name: User } }

it { is_expected.to have_attributes(type: :string) }
end

context "with model_name present" do
let(:attribute_options) { { model_name: "User" } }
let(:options) { { model_name: "User" } }

it { is_expected.to have_attributes(type: :string) }
end

context "with model_name column present" do
let(:options) { { model_name: "User#age" } }

it { is_expected.to have_attributes(type: :integer) }
end
end

describe "#model_human_name" do
subject(:model_human_name) { attribute.model_human_name }

it { is_expected.to be_nil }

context "with model_name class" do
let(:options) { { model_name: User } }

it { is_expected.to eq "Name" }
end

context "with model_name present" do
let(:attribute_options) { { model_name: "User" } }
let(:options) { { model_name: "User" } }

it { is_expected.to eq "Name" }
end

context "with model_name column present" do
let(:options) { { model_name: "User#age" } }

it { is_expected.to eq "Age" }
end
end

describe "#model_validators" do
subject(:model_validators) { attribute.model_validators }

it { is_expected.to eq [] }

context "with model_name present" do
let(:attribute_options) { { model_name: User } }
context "with model_name class" do
let(:options) { { model_name: User } }

it { is_expected.to eq(User.validators_on(:name)) }
end
end

describe "#model_localized_attr_name" do
subject(:model_localized_attr_name) { attribute.model_localized_attr_name(:fr) }
context "with model_name present" do
let(:options) { { model_name: "User" } }

it { is_expected.to be_nil }
it { is_expected.to eq(User.validators_on(:name)) }
end

context "with model_name present" do
let(:attribute_options) { { model_name: User } }
context "with model_name column present" do
let(:options) { { model_name: "User#age" } }

it { is_expected.to eq "name_fr" }
it { is_expected.to be_empty }
end
end
end
24 changes: 16 additions & 8 deletions spec/operations/form/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,49 +86,57 @@
#<Operations::Form::Attribute
name=:name,
collection=false,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=nil>,
:tags=>
#<Operations::Form::Attribute
name=:tags,
collection=true,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=nil>,
:author=>
#<Operations::Form::Attribute
name=:author,
collection=false,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=#<Class
attributes={:title=>
#<Operations::Form::Attribute
name=:title,
collection=false,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=nil>}>>,
:posts=>
#<Operations::Form::Attribute
name=:posts,
collection=true,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=#<Class
attributes={:title=>
#<Operations::Form::Attribute
name=:title,
collection=false,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=nil>,
:id=>
#<Operations::Form::Attribute
name=:id,
collection=false,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=nil>,
:text=>
#<Operations::Form::Attribute
name=:text,
collection=false,
model_name=nil,
model_class=nil,
model_attribute=nil,
form=nil>}>>}>
INSPECT
end
Expand Down
10 changes: 7 additions & 3 deletions spec/operations/form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def call(_, **)
let(:options) { default_options }

before do
stub_const("DummyModel", Class.new)
stub_const("DummyOperation", operation)
end

Expand Down Expand Up @@ -178,19 +179,22 @@ def call(_, **)
#<Operations::Form::Attribute
name=:entities,
collection=true,
model_name="DummyModel",
model_class=DummyModel,
model_attribute="entities",
form=#<Class
attributes={:id=>
#<Operations::Form::Attribute
name=:id,
collection=false,
model_name="DummyModel",
model_class=DummyModel,
model_attribute="id",
form=nil>}>>,
:name=>
#<Operations::Form::Attribute
name=:name,
collection=false,
model_name="DummyModel",
model_class=DummyModel,
model_attribute="name",
form=nil>}>>
INSPECT
end
Expand Down
5 changes: 1 addition & 4 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@
ActiveRecord::Schema.define do
create_table :users do |t|
t.column :name, :string
t.column :age, :integer
end
end

class User < ActiveRecord::Base
validates :name, presence: true

def self.localized_attr_name_for(name, locale)
"#{name}_#{locale}"
end
end

RSpec.configure do |config|
Expand Down

0 comments on commit 6329482

Please sign in to comment.