Class: CommandModel::Model

Inherits:
Object
  • Object
show all
Extended by:
ActiveModel::Naming
Includes:
ActiveModel::Conversion, ActiveModel::Validations
Defined in:
lib/command_model/model.rb

Constant Summary collapse

Parameter =
Data.define(:name, :converters, :validations)
Dependency =
Data.define(:name, :default, :allow_blank)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parameters = {}, dependencies = {}) ⇒ Model

Accepts a parameters hash or another of the same class. If another instance of the same class is passed in then the parameters are copied to the new object.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/command_model/model.rb', line 171

def initialize(parameters={}, dependencies={})
  @type_conversion_errors = {}
  set_parameters parameters

  dependencies = dependencies.symbolize_keys
  self.class.dependencies.each do |dependency|
    value = dependencies.fetch(dependency.name, dependency.default.call)
    if value.blank? && !dependency.allow_blank
      raise ArgumentError, "Dependency #{dependency.name} cannot be blank"
    end
    self.send "#{dependency.name}=", dependencies.fetch(dependency.name, dependency.default.call)
  end

  unknown_dependencies = dependencies.keys - self.class.dependencies.map(&:name)
  if unknown_dependencies.present?
    raise ArgumentError, "Unknown dependencies: #{bad_dependencies.join(", ")}"
  end
end

Class Method Details

.attr_type_converting_writer(name, converters) ⇒ Object

:nodoc



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/command_model/model.rb', line 58

def self.attr_type_converting_writer(name, converters) #:nodoc
  converters = converters.map do |c|
    if c.respond_to? :call
      c
    else
      case c.to_s
      when "integer"
        CommandModel::Convert::Integer.new
      when "decimal"
        CommandModel::Convert::Decimal.new
      when "float"
        CommandModel::Convert::Float.new
      when "date"
        CommandModel::Convert::Date.new
      when "boolean"
        CommandModel::Convert::Boolean.new
      else
        raise ArgumentError, "unknown converter #{c}"
      end
    end
  end

  define_method "#{name}=" do |value|
    converted_value = converters.reduce(value) { |v, c| c.call(v) }
    instance_variable_set "@#{name}", converted_value
    instance_variable_get("@type_conversion_errors").delete(name)
    instance_variable_get "@#{name}"
  rescue CommandModel::Convert::ConvertError => e
    instance_variable_get("@type_conversion_errors")[name] = e.target_type
    instance_variable_set "@#{name}", value
  end
end

.dependenciesObject

Returns array of all dependencies defined for class.



119
120
121
# File 'lib/command_model/model.rb', line 119

def self.dependencies
  @dependencies ||= [].freeze
end

.dependency(*names, default: nil, allow_blank: false) ⇒ Object

Dependency requires one or more attributes as its first parameter(s). A dependency is something that is required for the command to execute that is not user supplied input. For example, a database connection, a logger, or the current user.

Keyword Arguments

  • default - An object that will be used as the default value for the dependency or a callable object that will be called to get the default value.

  • allow_blank - If true, the dependency can be nil or blank. If false, the dependency must be present.

Examples

dependency :current_user
dependency :stdout, default: -> { $stdout }


107
108
109
110
111
112
113
114
115
116
# File 'lib/command_model/model.rb', line 107

def self.dependency(*names, default: nil, allow_blank: false)
  @dependencies ||= [].freeze
  names.each do |name|
    name = name.to_sym
    attr_reader name
    private attr_writer name
    default_callable = default.respond_to?(:call) ? default : -> { default }
    @dependencies = (@dependencies + [Dependency.new(name, default_callable, allow_blank)]).freeze
  end
end

.execute(attributes_or_command, dependencies = {}, &block) ⇒ Object

Executes a block of code if the command model is valid.

Accepts either a command model or a hash of attributes with which to create a new command model.

Examples

RenameUserCommand.execute(login: "john") do |command|
  if allowed_to_rename_user?
    self. = command.
  else
    command.errors.add :base, "not allowed to rename"
  end
end


137
138
139
140
141
142
143
144
145
146
# File 'lib/command_model/model.rb', line 137

def self.execute(attributes_or_command, dependencies={}, &block)
  command = if attributes_or_command.kind_of? self
    raise ArgumentError, "cannot pass dependencies with already initialized command" if dependencies.present?
    attributes_or_command
  else
    new(attributes_or_command, dependencies)
  end

  command.call &block
end

.failure(error) ⇒ Object

Quickly create a failed command object. Requires one parameter with the description of what went wrong. This is used when the command takes no parameters to want to take advantage of the success? and errors properties of a command object.



161
162
163
164
165
166
# File 'lib/command_model/model.rb', line 161

def self.failure(error)
  new.tap do |instance|
    instance.execution_attempted!
    instance.errors.add(:base, error)
  end
end

.inherited(subclass) ⇒ Object



7
8
9
10
# File 'lib/command_model/model.rb', line 7

def self.inherited(subclass)
  subclass.instance_variable_set :@parameters, parameters.dup.freeze
  subclass.instance_variable_set :@dependencies, dependencies.dup.freeze
end

.parameter(*args) ⇒ Object

Parameter requires one or more attributes as its first parameter(s). It accepts an options hash as its last parameter.

Options

  • convert - An object or array of objects that respond to call and convert the assigned value as necessary. Built-in converters exist for integer, decimal, float, date, and boolean. These built-in converters can be specified by symbol.

  • validations - All other options are considered validations and are passed to ActiveModel::Validates.validates

Examples

parameter :gender
parameter :name, presence: true
parameter :birthdate, convert: :date
parameter :height, :weight,
  convert: [CommandModel::Convert::StringMutator.new { |s| s.gsub(",", "")}, :integer],
  presence: true,
  numericality: { greater_than_or_equal_to: 0 }


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/command_model/model.rb', line 35

def self.parameter(*args)
  options = args.last.kind_of?(Hash) ? args.pop.clone : {}
  converters = options.delete(:convert)

  @parameters ||= [].freeze
  args.each do |name|
    attr_reader name

    if converters
      attr_type_converting_writer name, Array(converters)
    else
      attr_writer name
    end
    validates name, options.clone if options.present? # clone options because validates mutates the hash :(
    @parameters = (@parameters + [Parameter.new(name, converters, options)]).freeze
  end
end

.parametersObject

Returns array of all parameters defined for class



54
55
56
# File 'lib/command_model/model.rb', line 54

def self.parameters
  @parameters ||= [].freeze
end

.successObject

Quickly create a successful command object. This is used when the command takes no parameters to want to take advantage of the success? and errors properties of a command object.



151
152
153
154
155
# File 'lib/command_model/model.rb', line 151

def self.success
  new.tap do |instance|
    instance.execution_attempted!
  end
end

Instance Method Details

#call(&block) ⇒ Object

Executes the command by calling the method execute if the validations pass.



192
193
194
195
196
# File 'lib/command_model/model.rb', line 192

def call(&block)
  execute(&block) if valid?
  execution_attempted!
  self
end

#execute {|_self| ... } ⇒ Object

Performs the actual command execution. It does not test if the command parameters are valid. Typically, call should be called instead of calling execute directly.

execute should be overridden in descendent classes

Yields:

  • (_self)

Yield Parameters:



203
204
205
# File 'lib/command_model/model.rb', line 203

def execute
  yield self if block_given?
end

#execution_attempted!Object

Record that an attempt was made to execute this command whether or not it was successful.



209
210
211
# File 'lib/command_model/model.rb', line 209

def execution_attempted! #:nodoc:
  @execution_attempted = true
end

#execution_attempted?Boolean

True if execution has been attempted on this command

Returns:

  • (Boolean)


214
215
216
# File 'lib/command_model/model.rb', line 214

def execution_attempted?
  @execution_attempted
end

#parametersObject

Returns hash of all parameter names and values



224
225
226
227
228
# File 'lib/command_model/model.rb', line 224

def parameters
  self.class.parameters.each_with_object({}) do |parameter, hash|
    hash[parameter.name] = send(parameter.name)
  end
end

#persisted?Boolean

:nodoc:

Returns:

  • (Boolean)


239
240
241
# File 'lib/command_model/model.rb', line 239

def persisted?
  false
end

#set_parameters(hash_or_instance) ⇒ Object

Sets parameter(s) from hash or instance of same class



231
232
233
234
235
236
# File 'lib/command_model/model.rb', line 231

def set_parameters(hash_or_instance)
  parameters = extract_parameters_from_hash_or_instance(hash_or_instance)
  parameters.each do |k,v|
    send "#{k}=", v
  end
end

#success?Boolean

Command has been executed without errors

Returns:

  • (Boolean)


219
220
221
# File 'lib/command_model/model.rb', line 219

def success?
  execution_attempted? && errors.empty?
end