Skip to content

Commit

Permalink
Extract model from settings
Browse files Browse the repository at this point in the history
  • Loading branch information
nepalez committed Sep 12, 2017
1 parent a97f239 commit f4e2858
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 89 deletions.
2 changes: 1 addition & 1 deletion lib/evil/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ class Client
require_relative "client/chaining"
require_relative "client/options"
require_relative "client/policy"
require_relative "client/model"
require_relative "client/settings"
require_relative "client/schema"
require_relative "client/container"
require_relative "client/builder"
require_relative "client/connection"
require_relative "client/formatter"
require_relative "client/resolver"

require_relative "client/dictionary"

include Chaining
Expand Down
79 changes: 79 additions & 0 deletions lib/evil/client/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
class Evil::Client
#
# Data structure with validators and memoizers
#
class Model
extend Dry::Initializer

@policy = Policy

class << self
# @!method options(key, type = nil, opts = {})
# Creates or updates the settings' initializer
#
# @see [http://dry-rb.org/gems/dry-initializer]
#
# @param [#to_sym] key Symbolic name of the option
# @param [#call] type (nil) Type coercer for the option
# @option opts [#call] :type Another way to assign type coercer
# @option opts [#call] :default Proc containing default value
# @option opts [Boolean] :optional Whether it can be missed
# @option opts [#to_sym] :as The name of settings variable
# @option opts [false, :private, :protected] :reader Reader method type
# @return [self]
#
def option(key, type = nil, as: key.to_sym, **opts)
NameError.check!(as)
super
self
end
undef_method :param # model initializes with [#options] only

# Creates or reloads memoized attribute
#
# @param [#to_sym] key The name of the attribute
# @param [Proc] block The body of new attribute
# @return [self]
#
def let(key, &block)
NameError.check!(key)
define_method(key) do
instance_variable_get(:"@#{key}") ||
instance_variable_set(:"@#{key}", instance_exec(&block))
end
self
end

def policy
@policy ||= superclass.policy.for(self)
end

# Add validation rule to the [#policy]
#
# @param [Proc] block The body of new attribute
# @return [self]
#
def validate(&block)
policy.validate(&block)
self
end

def new(op = {})
op = Hash(op).each_with_object({}) { |(k, v), obj| obj[k.to_sym] = v }
super(op).tap { |item| in_english { policy[item].validate! } }
rescue => error
raise ValidationError, error.message

This comment has been minimized.

Copy link
@Envek

Envek Nov 27, 2017

Member

This is very nasty thing. Any exception occured inside method will be thrown out and it will be hard to find where it was raised and why. Happy debugging bitches!

end

private

def in_english(&block)
available_locales = I18n.available_locales
I18n.available_locales = %i[en]
I18n.with_locale(:en, &block)
ensure
I18n.available_locales = available_locales
end
end
end
end
20 changes: 10 additions & 10 deletions lib/evil/client/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@ class << self
# @param [Class] settings Settings class to validate
# @return [Class]
#
def for(settings)
def for(model)
Class.new(self).tap do |klass|
klass.send :instance_variable_set, :@settings, settings
klass.send :instance_variable_set, :@model, model
end
end

# Reference to the settings class whose instances validates the policy
# Reference to the model whose instances are validated by the policy
#
# @return [Class, nil]
#
attr_reader :settings
attr_reader :model

# Delegates the name of the policy to the name of checked settings
# Delegates the name of the policy to the name of checked model
#
# @return [String, nil]
#
def name
"#{settings}.policy"
"#{model}.policy"
end
alias_method :to_s, :name
alias_method :to_sym, :name
Expand All @@ -36,21 +36,21 @@ def name

def scope
@scope ||= %i[evil client errors] << \
Tram::Policy::Inflector.underscore(settings.to_s)
Tram::Policy::Inflector.underscore(model.to_s)
end
end

# An instance of settings to be checked by the policy
param :settings
param :model

private

def respond_to_missing?(name, *)
settings.respond_to?(name)
model.respond_to?(name)
end

def method_missing(*args)
respond_to_missing?(*args) ? settings.__send__(*args) : super
respond_to_missing?(*args) ? model.__send__(*args) : super
end
end
end
80 changes: 7 additions & 73 deletions lib/evil/client/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ class Evil::Client
#
# Container for settings assigned to some operation or scope.
#
class Settings
class Settings < Model
Names.clean(self) # Remove unnecessary methods from the instance
extend ::Dry::Initializer

@policy = Policy

class << self
# Subclasses itself for a given schema
Expand Down Expand Up @@ -37,50 +34,6 @@ def name
alias_method :to_str, :name
alias_method :inspect, :name

undef_method :param

# Creates or updates the settings' initializer
#
# @see [http://dry-rb.org/gems/dry-initializer]
#
# @param [#to_sym] key Symbolic name of the option
# @param [#call] type Type coercer for the option
# @option opts [#call] :type Another way to assign type coercer
# @option opts [#call] :default Proc containing default value
# @option opts [Boolean] :optional Whether it can be missed
# @option opts [#to_sym] :as The name of settings variable
# @option opts [false, :private, :protected] :reader Reader method type
# @return [self]
#
def option(key, type = nil, as: key.to_sym, **opts)
NameError.check!(as)
super
self
end

# Creates or reloads memoized attribute
#
# @param [#to_sym] key The name of the attribute
# @param [Proc] block The body of new attribute
# @return [self]
#
def let(key, &block)
NameError.check!(key)
define_method(key) do
instance_variable_get(:"@#{key}") ||
instance_variable_set(:"@#{key}", instance_exec(&block))
end
self
end

# Policy class that collects all the necessary validators
#
# @return [Class] a subclass of [Tram::Policy] named after the scope
#
def policy
@policy ||= superclass.policy.for(self)
end

# Add validation rule to the [#policy]
#
# @param [Proc] block The body of new attribute
Expand All @@ -97,22 +50,12 @@ def validate(&block)
# @param [Hash<#to_sym, Object>, nil] opts
# @return [Evil::Client::Settings]
#
def new(logger, opts = {})
logger&.debug(self) { "initializing with options #{opts}..." }
opts = Hash(opts).each_with_object({}) { |(k, v), o| o[k.to_sym] = v }
in_english { super logger, opts }
rescue => error
raise ValidationError, error.message
end

private

def in_english(&block)
available_locales = I18n.available_locales
I18n.available_locales = %i[en]
I18n.with_locale(:en, &block)
ensure
I18n.available_locales = available_locales
def new(logger, op = {})
logger&.debug(self) { "initializing with options #{op}..." }
super(op).tap do |item|
item.logger = logger
logger&.debug(item) { "initialized" }
end
end
end

Expand Down Expand Up @@ -156,14 +99,5 @@ def inspect
end
alias_method :to_str, :inspect
alias_method :to_s, :inspect

private

def initialize(logger, **options)
super(options)
@logger = logger
self.class.policy[self].validate!
logger&.debug(self) { "initialized" }
end
end
end
2 changes: 2 additions & 0 deletions spec/fixtures/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ en:
users:
filter:
filter_given: You should define some filter with either name, email, or id
test/model:
name_present: "The user has no name"
87 changes: 87 additions & 0 deletions spec/unit/model_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
RSpec.describe Evil::Client::Model do
before { class Test::Model < described_class; end }

let(:model) { klass.new(options) }
let(:klass) { Test::Model }
let(:options) { { "id" => 42, "name" => "Andrew" } }
let(:dsl_methods) do
%i[options datetime logger scope basic_auth key_auth token_auth]
end

describe ".policy" do
subject { klass.policy }

it "subclasses Evil::Client::Policy" do
expect(subject.superclass).to eq described_class.policy
expect(described_class.policy.superclass).to eq Tram::Policy
end

it "refers back to the model" do
expect(subject.model).to eq klass
end
end

describe ".option" do
it "is defined by Dry::Initializer DSL" do
expect(klass).to be_a Dry::Initializer
end

it "fails when method name is reserved for DSL" do
dsl_methods.each do |name|
expect { klass.option name }
.to raise_error Evil::Client::NameError
end
end

it "allows the option to be renamed" do
expect { klass.option :basic_auth, as: :something }.not_to raise_error
end
end

describe ".let" do
before do
klass.option :id
klass.let(:square_id) { id**2 }
end

subject { model.square_id }

it "adds the corresponding memoizer to the instance" do
expect(subject).to eq(42**2)
end

it "fails when method name is reserved for DSL" do
dsl_methods.each do |name|
expect { klass.let(name) { 0 } }
.to raise_error Evil::Client::NameError
end
end
end

describe ".validate" do
before do
klass.option :name
klass.validate { errors.add :name_present if name.to_s == "" }
end

let(:options) { { "name" => "" } }

it "adds validation for an instance" do
# see spec/fixtures/locale/en.yml
expect { model }
.to raise_error(Evil::Client::ValidationError, /The user has no name/)
end
end

describe ".new" do
subject { model }

context "with wrong options" do
before { klass.option :user, as: :customer }

it "raises Evil::Client::ValidationError" do
expect { subject }.to raise_error Evil::Client::ValidationError, /user/
end
end
end
end
4 changes: 2 additions & 2 deletions spec/unit/policy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
end

it "keeps reference to the settings" do
expect(subject.settings).to eq settings
expect(subject.model).to eq settings
end

it "takes the name from settings clsass" do
it "takes the name from settings class" do
expect(subject.name).to eq "Foo.policy"
end
end
Expand Down
7 changes: 4 additions & 3 deletions spec/unit/settings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
subject { klass.policy }

it "subclasses Evil::Client::Policy" do
expect(subject.superclass).to eq Evil::Client::Policy
expect(subject.superclass).to eq described_class.policy
expect(described_class.policy.superclass).to eq Evil::Client::Policy
end

it "refers back to the settings" do
expect(subject.settings).to eq klass
expect(subject.model).to eq klass
end
end

Expand All @@ -44,7 +45,7 @@
end

it "refers back to the settings" do
expect(subject.settings).to eq scope_klass
expect(subject.model).to eq scope_klass
end
end
end
Expand Down

0 comments on commit f4e2858

Please sign in to comment.