Class: Atacama::Contract

Inherits:
Object
  • Object
show all
Defined in:
lib/atacama/contract.rb

Overview

This class enables a DSL for creating a contract for the initializer

Constant Summary collapse

RESERVED_KEYS =
%i[call initialize context].freeze
Types =

Namespace alias for easier reading when defining types.

Atacama::Types
NameInterface =
Types::Strict::Symbol.constrained(excluded_from: RESERVED_KEYS)
ContextInterface =
Types::Strict::Hash | Types.Instance(Context)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(context: {}) ⇒ Contract

Returns a new instance of Contract.

Parameters:

  • context (Hash) (defaults to: {})

    the values to satisfy the option definition

Raises:

  • (Dry::Types::ConstraintError)

    a type check failure



134
135
136
137
138
139
140
141
142
# File 'lib/atacama/contract.rb', line 134

def initialize(context: {}, **)
  ContextInterface[context] # Validate the type

  @context = Context.new(self.class.injected).tap do |ctx|
    ctx.merge!(context.is_a?(Context) ? context.to_h : context)
  end

  Validator.call(options: self.class.options, context: @context, klass: self.class)
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



129
130
131
# File 'lib/atacama/contract.rb', line 129

def context
  @context
end

Class Method Details

.call(context = {}) { ... } ⇒ Object

The main interface to executing contracts. Given a set of options it will check the parameter types as well as return types, if defined.

Parameters:

  • arguments (Hash)

    keyword arguments that match the defined options

Yields:

  • the block is evaluated in the context of the instance call method

Returns:

  • The value of the #call instance method.



54
55
56
# File 'lib/atacama/contract.rb', line 54

def call(context = {}, &block)
  new(context: context).call(&block).tap { |result| validate_return(result) }
end

.inherited(subclass) ⇒ Object

Executed by the Ruby VM at subclass time. Ensure that all internal state is copied to the new subclass.



109
110
111
112
113
114
115
# File 'lib/atacama/contract.rb', line 109

def inherited(subclass)
  subclass.returns(return_type)

  options.each do |name, parameter|
    subclass.option(name, type: parameter.type)
  end
end

.inject(injected) ⇒ Class

Inject dependencies statically in to the Contract object. Allows for easier composition of contracts when used in a Transaction.

Examples:

SampleClass.inject(user: User.new).call(attributes: { name: "Cindy" })

Parameters:

  • injected (Hash)

    the options to inject in to the initializer

Returns:

  • (Class)

    a new class object that contains the injection



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/atacama/contract.rb', line 67

def inject(injected)
  Validator.call({
    options: Hash[injected.keys.zip(options.values_at(*injected.keys))],
    context: Context.new(injected),
    klass: self
  })

  Class.new(self) do
    self.injected = injected
  end
end

.injectedObject



123
124
125
126
# File 'lib/atacama/contract.rb', line 123

def injected
  # Silences the VM warning about accessing uninitalized ivar
  defined?(@injected) ? @injected : {}
end

.injected=(hash) ⇒ Object



118
119
120
# File 'lib/atacama/contract.rb', line 118

def injected=(hash)
  @injected = Types::Strict::Hash[hash]
end

.option(name, type: nil) ⇒ Object

Define an initializer value.

Examples:

Set an option

option :model. type: Types.Instance(User)

Parameters:

  • name (Symbol)

    name of the argument

  • type (Dry::Type?) (defaults to: nil)

    the type object to optionally check



31
32
33
34
35
36
# File 'lib/atacama/contract.rb', line 31

def option(name, type: nil)
  options[NameInterface[name]] = Parameter.new(name: name, type: type)

  define_method(name) { @context[name] }
  define_method("#{name}?") { !!@context[name] }
end

.optionsHash<String, Atacama::Parameter>

The defined options on the contract.

Returns:



103
104
105
# File 'lib/atacama/contract.rb', line 103

def options
  @options ||= {}
end

.return_typeDry::Type?

The defined return type on the Contract.

Returns:

  • (Dry::Type?)

    the type object to optionally check



82
83
84
# File 'lib/atacama/contract.rb', line 82

def return_type
  defined?(@returns) && @returns
end

.returns(type) ⇒ Object

Set the return type for the contract. This is only validated automatically through the #call class method.

Parameters:

  • type (Dry::Type?)

    the type object to optionally check



42
43
44
# File 'lib/atacama/contract.rb', line 42

def returns(type) # rubocop:disable Style/TrivialAccessors
  @returns = type
end

.validate_return(value) ⇒ Object

Execute type checking on a value for the defined return value. Useful when executing ‘new` on these objects.

Parameters:

  • value (Any)

    the object to type check

Raises:

  • (Dry::Types::ConstraintError)

    a type check failure



92
93
94
95
96
97
98
# File 'lib/atacama/contract.rb', line 92

def validate_return(value)
  Atacama.check(return_type, value) do |e|
    raise ReturnTypeMismatchError, Atacama.format_exception(self, e,
      'The return value was an incorrect type.',
    )
  end
end

Instance Method Details

#callObject

This method is abstract.

The default entrypoint for all Contracts. This is executed and type checked when called from the Class#call.



157
158
159
# File 'lib/atacama/contract.rb', line 157

def call
  self
end

#inspectObject



145
146
147
148
149
150
151
152
# File 'lib/atacama/contract.rb', line 145

def inspect
  "#<#{self.class}:0x%x %s>" % [
    object_id,
    self.class.options.keys.map do |option|
      "#{option}: #{context.send(option).inspect[0..20]}"
    end.join(' ')
  ]
end