Class: GLCommand::Callable

Inherits:
Object
  • Object
show all
Includes:
Validatable
Defined in:
lib/gl_command/callable.rb

Direct Known Subclasses

Chainable

Constant Summary collapse

DEFAULT_OPTS =
{ raise_errors: false, skip_unknown_parameters: true, in_chain: false }.freeze
RESERVED_WORDS =
(DEFAULT_OPTS.keys + GLCommand::ChainableContext.reserved_words).sort.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Validatable

#validatable_valid?, #validate_validatable!

Constructor Details

#initialize(context = nil) ⇒ Callable

Returns a new instance of Callable.



108
109
110
# File 'lib/gl_command/callable.rb', line 108

def initialize(context = nil)
  @context = context
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



106
107
108
# File 'lib/gl_command/callable.rb', line 106

def context
  @context
end

Class Method Details

.allows(*attributes, **strong_attributes) ⇒ Object



51
52
53
# File 'lib/gl_command/callable.rb', line 51

def allows(*attributes, **strong_attributes)
  @allows ||= strong_args_hash(*attributes, **strong_attributes).freeze
end

.argumentsObject

arguments are what’s passed to the .call command (the allows and requires)



61
62
63
64
65
66
67
68
69
70
71
# File 'lib/gl_command/callable.rb', line 61

def arguments
  return @arguments if defined?(@arguments)

  duplicated_keys = requires.keys & allows.keys
  raise "Duplicated: #{duplicated_keys} - in both requires and allows" if duplicated_keys.any?

  @arguments = (requires.keys + allows.keys).freeze

  delegate(*@arguments + returns, to: :context)
  @arguments
end

.arguments_and_returnsObject

arguments_and_returns is just the keys (names) of the arguments and returns



74
75
76
# File 'lib/gl_command/callable.rb', line 74

def arguments_and_returns
  (arguments + returns).uniq
end

.build_context(raise_errors: false, skip_unknown_parameters: false, error: nil, **arguments_and_returns) ⇒ Object

error can be passed to build context, useful for stubbing in tests



39
40
41
42
43
44
45
# File 'lib/gl_command/callable.rb', line 39

def build_context(raise_errors: false, skip_unknown_parameters: false, error: nil,
                  **arguments_and_returns)
  new_context = context_class.new(self, raise_errors:, skip_unknown_parameters:,
                                        **arguments_and_returns)
  new_context.error = error if error.present?
  new_context
end

.call(*posargs, **args) ⇒ Object

Make raise_errors and skip_unknown_parameters reserved and raise if they’re passed in



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/gl_command/callable.rb', line 14

def call(*posargs, **args)
  if arguments_and_returns.intersect?(RESERVED_WORDS)
    raise ArgumentError,
          "You used reserved word(s): #{arguments_and_returns & RESERVED_WORDS}\n" \
          '(check GLCommand::Callable::RESERVED_WORDS for the full list)'
  end

  if posargs.any?
    raise ArgumentError,
          "`call` only supports keyword args, not positional - you passed: '#{posargs}'"
  end

  # DEFAULT_OPTS contains skip_unknown_parameters: true - so it raises on call
  # (rather than in context initialize) to make errors more legible
  opts = DEFAULT_OPTS.merge(raise_errors: args.delete(:raise_errors),
                            in_chain: args.delete(:in_chain)).compact
  # args are passed in in perform_call(args) so that invalid args raise in a legible place
  new(build_context(**args.merge(opts))).perform_call(args)
end

.call!(*posargs, **args) ⇒ Object



34
35
36
# File 'lib/gl_command/callable.rb', line 34

def call!(*posargs, **args)
  call(*posargs, **args.merge(raise_errors: true))
end

.chain?Boolean

Used internally by GLCommand (probably don’t reference in your own GLCommands) is true in GLCommand::Chainable

Returns:

  • (Boolean)


80
81
82
# File 'lib/gl_command/callable.rb', line 80

def chain?
  false
end

.error_handlersObject



88
89
90
# File 'lib/gl_command/callable.rb', line 88

def error_handlers
  @error_handlers ||= {}
end

.requires(*attributes, **strong_attributes) ⇒ Object



47
48
49
# File 'lib/gl_command/callable.rb', line 47

def requires(*attributes, **strong_attributes)
  @requires ||= strong_args_hash(*attributes, **strong_attributes).freeze
end

.rescue_from(error_class, with:) ⇒ Object



84
85
86
# File 'lib/gl_command/callable.rb', line 84

def rescue_from(error_class, with:)
  error_handlers[error_class] = with
end

.returns(*attributes, **strong_attributes) ⇒ Object



55
56
57
58
# File 'lib/gl_command/callable.rb', line 55

def returns(*attributes, **strong_attributes)
  # NOTE: Because returns aren't validated, we don't store the types (only store keys)
  @returns ||= strong_args_hash(*attributes, **strong_attributes).keys.freeze
end

Instance Method Details

#callObject

Ensure that call is overridden in subclass



137
138
139
# File 'lib/gl_command/callable.rb', line 137

def call
  raise 'You must define the `call` instance method on your GLCommand'
end

#perform_call(args) ⇒ Object



112
113
114
115
116
117
118
119
120
121
# File 'lib/gl_command/callable.rb', line 112

def perform_call(args)
  raise_for_invalid_args!(**args)
  instrument_command(:before_call)
  call_with_callbacks
  instrument_command(:after_call)
  raise_unless_chained_or_skipped if self.class.chain? # defined in GLCommand::Chainable
  context.failure? ? handle_failure : context
rescue StandardError => e
  handle_failure(e)
end

#rollbackObject

define a rollback method if you want to have actions for rolling back it is called in handle_failure



134
# File 'lib/gl_command/callable.rb', line 134

def rollback; end

#stop_and_fail!(passed_error = nil, no_notify: false) ⇒ Object



123
124
125
126
127
128
129
130
# File 'lib/gl_command/callable.rb', line 123

def stop_and_fail!(passed_error = nil, no_notify: false)
  # manually setting instance_variable because @no_notify shouldn't be updated in commands
  # (e.g. context shouldn't have an attr_writer)
  context.instance_variable_set(:@no_notify, no_notify)
  context.error = passed_error

  raise context.no_notifiable_error_to_raise # See comment in #handle_failure
end