Module: NextStation::Operation::ClassMethods

Included in:
NextStation::Operation
Defined in:
lib/next_station/operation/class_methods.rb

Overview

The ‘ClassMethods` module provides a set of class-level methods for defining the structure, validation, dependencies, and processing logic of an operation. It introduces a DSL for custom error handling, step management, schema enforcement, and parameter validation.

Error Handling

  • Allows defining custom error types for the operation using a DSL.

Result Management

  • Supports setting and retrieving the key where the operation result is stored.

  • Enables schema enforcement for result values using a Dry::Struct schema.

Steps and Branches

  • Facilitates defining execution steps and branches within an operation.

  • Allows querying the presence of specific steps within the operation.

Validation

  • Provides integration with Dry::Validation for validating operation parameters.

  • Enables enforcing or skipping validation on demand.

Dependencies

  • Supports defining and managing dependencies for the operation.

Methods

Instance Method Summary collapse

Instance Method Details

#branch(condition) { ... } ⇒ Object

Adds a branch to the operation.

Parameters:

  • condition (Proc)

Yields:



174
175
176
177
# File 'lib/next_station/operation/class_methods.rb', line 174

def branch(condition, &block)
  @root ||= Node.new(:root)
  @root.branch(condition, &block)
end

#call(params = {}, context = {}, deps: {}) ⇒ Object

Convenience method to instantiate and call the operation.



271
272
273
# File 'lib/next_station/operation/class_methods.rb', line 271

def call(params = {}, context = {}, deps: {})
  new(deps: deps).call(params, context)
end

#dependenciesHash

Returns The defined dependencies.

Returns:

  • (Hash)

    The defined dependencies.



266
267
268
# File 'lib/next_station/operation/class_methods.rb', line 266

def dependencies
  @dependencies || (superclass.respond_to?(:dependencies) ? superclass.dependencies : {})
end

#depends(deps) ⇒ Object

Defines dependencies for the operation.

Examples:

depends mailer: -> { Mailer.new }

depends repository: UserRepository.new

Usage inside a step:

def send_email
  # Access dependencies using the dependency() method
  dependency(:mailer).send_welcome(state.params[:email])
  # rest of the step
end

You can override the dependencies when instantiating the operation by passing the deps: argument:

mock_mailer = double("Mailer")
operation = CreateUser.new(deps: { mailer: mock_mailer })
operation.call(email: "test@example.com")

Parameters:

  • deps (Hash)

    A mapping of dependency names to values or Procs.



261
262
263
# File 'lib/next_station/operation/class_methods.rb', line 261

def depends(deps)
  @dependencies = dependencies.merge(deps)
end

#disable_result_schemaObject

Disables result schema enforcement.



145
146
147
# File 'lib/next_station/operation/class_methods.rb', line 145

def disable_result_schema
  @schema_enforced = false
end

#enforce_result_schemaObject

Enables result schema enforcement.



140
141
142
# File 'lib/next_station/operation/class_methods.rb', line 140

def enforce_result_schema
  @schema_enforced = true
end

#error_definitionsHash

Returns The registered error definitions.

Returns:

  • (Hash)

    The registered error definitions.



84
85
86
87
88
89
90
91
# File 'lib/next_station/operation/class_methods.rb', line 84

def error_definitions
  parent_defs = if superclass.respond_to?(:error_definitions)
                  superclass.error_definitions
                else
                  {}
                end
  parent_defs.merge(@error_definitions || {})
end

#errors(external_source = nil) { ... } ⇒ Object

Defines error types for the operation.

Parameters:

  • external_source (Class, Hash, nil) (defaults to: nil)

    An external error collection class or a hash of definitions.

Yields:

  • The block defining errors via ErrorsDSL.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/next_station/operation/class_methods.rb', line 59

def errors(external_source = nil, &block)
  @error_definitions ||= {}

  # 1. Handle external source (e.g., SharedErrors < NextStation::Errors)
  if external_source.respond_to?(:definitions)
    @error_definitions.merge!(external_source.definitions)
  elsif external_source.is_a?(Hash)
    external_source.each do |type, config|
      definition = ErrorDefinition.new(type)
      definition.message(config[:message]) if config[:message]
      definition.help_url(config[:help_url]) if config[:help_url]
      definition.validate!
      @error_definitions[type] = definition
    end
  end

  # 2. Handle inline block
  if block_given?
    dsl = ErrorsDSL.new
    dsl.instance_eval(&block)
    @error_definitions.merge!(dsl.definitions)
  end
end

#force_validation!Object

Forces validation even if not explicitly defined in steps.



220
221
222
# File 'lib/next_station/operation/class_methods.rb', line 220

def force_validation!
  @validation_enforced = true
end

#has_step?(name, nodes = steps) ⇒ Boolean

Checks if a step exists in the operation.

Parameters:

  • name (Symbol)
  • nodes (Array<Node>) (defaults to: steps)

Returns:

  • (Boolean)


240
241
242
243
244
# File 'lib/next_station/operation/class_methods.rb', line 240

def has_step?(name, nodes = steps)
  nodes.any? do |node|
    node.name == name || (node.type == :branch && has_step?(name, node.children))
  end
end

#loaded_pluginsArray<Module>

Returns The plugins loaded into this operation.

Returns:

  • (Array<Module>)

    The plugins loaded into this operation.



294
295
296
# File 'lib/next_station/operation/class_methods.rb', line 294

def loaded_plugins
  @loaded_plugins ||= (superclass.respond_to?(:loaded_plugins) ? superclass.loaded_plugins.dup : [])
end

#plugin(name) ⇒ Object

Enables a plugin for the operation.

Parameters:

  • name (Symbol)

    The registered plugin name.



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/next_station/operation/class_methods.rb', line 277

def plugin(name)
  require 'dry-configurable'
  mod = NextStation::Plugins.load_plugin(name)
  loaded_plugins << mod

  extend mod::ClassMethods if mod.const_defined?(:ClassMethods)
  include mod::InstanceMethods if mod.const_defined?(:InstanceMethods)
  NextStation::Operation::Node.include mod::DSL if mod.const_defined?(:DSL)

  if mod.const_defined?(:Errors) && mod::Errors.respond_to?(:definitions)
    errors(mod::Errors.definitions)
  end

  mod.configure(self) if mod.respond_to?(:configure)
end

#process { ... } ⇒ Object

Defines the root execution block for the operation.

Yields:

  • The block defining steps and branches.



159
160
161
# File 'lib/next_station/operation/class_methods.rb', line 159

def process(&block)
  @root = Node.new(:root, &block)
end

#result_at(key) ⇒ Object

Defines the key in the state where the final result is stored.

Parameters:

  • key (Symbol)


95
96
97
# File 'lib/next_station/operation/class_methods.rb', line 95

def result_at(key)
  @result_key = key
end

#result_classClass?

Returns The Dry::Struct class for the result.

Returns:

  • (Class, nil)

    The Dry::Struct class for the result.



135
136
137
# File 'lib/next_station/operation/class_methods.rb', line 135

def result_class
  @result_class || (superclass.result_class if superclass.respond_to?(:result_class))
end

#result_keySymbol?

Returns The key where the result is stored.

Returns:

  • (Symbol, nil)

    The key where the result is stored.



100
101
102
# File 'lib/next_station/operation/class_methods.rb', line 100

def result_key
  @result_key || (superclass.result_key if superclass.respond_to?(:result_key))
end

#result_schema(struct_class = nil) { ... } ⇒ Object

Defines a Dry::Struct schema for the result value.

Parameters:

  • struct_class (Class, nil) (defaults to: nil)

    A Dry::Struct class or nil if a block is provided.

Yields:

  • The block defining the schema.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/next_station/operation/class_methods.rb', line 107

def result_schema(struct_class = nil, &block)
  require 'dry-struct'

  if @result_class
    raise NextStation::DoubleResultSchemaError, 'result_schema has already been defined'
  end

  if struct_class && block_given?
    raise NextStation::DoubleResultSchemaError, 'result_schema accepts either a Dry::Struct class OR a block, but not both.'
  end

  if struct_class
    if struct_class.is_a?(Class) && struct_class < Dry::Struct
      @result_class = struct_class
    else
      raise ArgumentError, 'result_schema requires a subclass of Dry::Struct'
    end
  elsif block_given?
    @result_class = Class.new(Dry::Struct, &block)
    const_set(:ResultSchema, @result_class) unless const_defined?(:ResultSchema, false)
  else
    raise ArgumentError, 'result_schema requires either a Dry::Struct class or a block'
  end

  @schema_enforced = true
end

#schema_enforced?Boolean

Returns Whether schema enforcement is enabled.

Returns:

  • (Boolean)

    Whether schema enforcement is enabled.



150
151
152
153
154
155
# File 'lib/next_station/operation/class_methods.rb', line 150

def schema_enforced?
  return @schema_enforced unless @schema_enforced.nil?
  return superclass.schema_enforced? if superclass.respond_to?(:schema_enforced?)

  false
end

#skip_validation!Object

Skips validation even if defined.



225
226
227
# File 'lib/next_station/operation/class_methods.rb', line 225

def skip_validation!
  @validation_enforced = false
end

#step(method_name, options = {}) ⇒ Object

Adds a step to the operation.

Parameters:

  • method_name (Symbol)
  • options (Hash) (defaults to: {})


166
167
168
169
# File 'lib/next_station/operation/class_methods.rb', line 166

def step(method_name, options = {})
  @root ||= Node.new(:root)
  @root.step(method_name, options)
end

#stepsArray<Node>

Returns The steps defined for the operation.

Returns:

  • (Array<Node>)

    The steps defined for the operation.



180
181
182
# File 'lib/next_station/operation/class_methods.rb', line 180

def steps
  @root&.children || (superclass.steps if superclass.respond_to?(:steps)) || []
end

#validate_with(contract_or_block = nil) { ... } ⇒ Object

Defines a Dry::Validation::Contract to validate the params.

Parameters:

  • contract_or_block (Class, nil) (defaults to: nil)

    A Contract class or nil if a block is provided.

Yields:

  • The block defining the validation rules.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/next_station/operation/class_methods.rb', line 188

def validate_with(contract_or_block = nil, &block)
  require 'dry-validation'
  @validation_contract_class = if block_given?
                                 Class.new(Dry::Validation::Contract) do
                                   config.messages.backend = :yaml
                                   config.messages.top_namespace = 'next_station_validations'
                                   config.messages.load_paths << File.expand_path('../../config/errors.yml', __FILE__)
                                   instance_eval(&block)
                                 end
                               elsif contract_or_block.is_a?(Class) && contract_or_block < Dry::Validation::Contract
                                 contract_or_block
                               else
                                 raise ValidationError,
                                       'validate_with requires a block or a Dry::Validation::Contract class'
                               end

  @validation_enforced = true
end

#validation_contract_classClass?

Returns The validation contract class.

Returns:

  • (Class, nil)

    The validation contract class.



208
209
210
211
212
# File 'lib/next_station/operation/class_methods.rb', line 208

def validation_contract_class
  @validation_contract_class || (if superclass.respond_to?(:validation_contract_class)
                                   superclass.validation_contract_class
                                 end)
end

#validation_contract_instanceDry::Validation::Contract?

Returns An instance of the validation contract.

Returns:

  • (Dry::Validation::Contract, nil)

    An instance of the validation contract.



215
216
217
# File 'lib/next_station/operation/class_methods.rb', line 215

def validation_contract_instance
  @validation_contract_instance ||= validation_contract_class&.new
end

#validation_enforced?Boolean

Returns Whether validation is enforced.

Returns:

  • (Boolean)

    Whether validation is enforced.



230
231
232
233
234
# File 'lib/next_station/operation/class_methods.rb', line 230

def validation_enforced?
  return @validation_enforced unless @validation_enforced.nil?

  superclass.respond_to?(:validation_enforced?) ? superclass.validation_enforced? : false
end