atacama 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5cf86d5c337258aa67e7c9f25dfe42a0bc82a913
4
- data.tar.gz: a1f304895a629d1c13c9455536e853acd1e5371e
3
+ metadata.gz: 2d78853c9e718c4ea9ff6ea4fbc8e729cd2f48cf
4
+ data.tar.gz: addda9f05177a741fc8d2dd36d465646e37249c0
5
5
  SHA512:
6
- metadata.gz: 78d72f1ee206d6b4f253531eef2ec3e1a4ad93c572fdbde98887ba74267542e269ef9c534a9d1fc2853a5ffe9148242002583aa0bca863d61d0a3a12b7ec6e1b
7
- data.tar.gz: 5f2b0f9530c7b675930af229cd5577ffdf24c6addc3443c7db277fb1b0a3a3731ec7d95c7f1f509fce8aa7eac92d3cab5479ea46a517fdc66080264b43226fbb
6
+ metadata.gz: 88ca8a4083003137ca041c1b985cbe4f8046d5777c1b45fa75994fe7f435c5fddc9b8eb83344789bc6e0a10f925ca348e01dff8ae9c9361c560863f2d7a1d317
7
+ data.tar.gz: 1166397fca1f7348584534140b3edc31418384cb4cc9c4e3d7e735608f6000eab5028524e585b7c5a0db1dea90656b1d4041bf258e94f071d64820a855e8a914
data/.rubocop.yml ADDED
@@ -0,0 +1,84 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.2
3
+ # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
4
+ # to ignore them, so only the ones explicitly set in this file are enabled.
5
+ Exclude:
6
+ - '**/bin/*'
7
+ - '**/db/**/*'
8
+ - '**/vendor/**/*'
9
+ - '**/node_modules/**/*'
10
+ - '**/*.gemspec'
11
+
12
+ Bundler/OrderedGems:
13
+ Enabled: true
14
+
15
+ Layout/IndentArray:
16
+ EnforcedStyle: consistent
17
+
18
+ Metrics/CyclomaticComplexity:
19
+ Max: 10
20
+
21
+ Metrics/LineLength:
22
+ Max: 100
23
+
24
+ Metrics/ClassLength:
25
+ Max: 100
26
+ Exclude:
27
+ - '*/test/**/*'
28
+ - '*_test.rb'
29
+
30
+ Metrics/MethodLength:
31
+ Max: 20
32
+
33
+ Style/Documentation:
34
+ Enabled: false
35
+
36
+ Style/Lambda:
37
+ Enabled: false
38
+
39
+ Style/ParallelAssignment:
40
+ Enabled: false
41
+
42
+ Style/DocumentationMethod:
43
+ Enabled: false
44
+
45
+ Style/TrailingCommaInArrayLiteral:
46
+ Enabled: false
47
+
48
+ Style/TrailingCommaInHashLiteral:
49
+ Enabled: false
50
+
51
+ Style/ClassAndModuleChildren:
52
+ Enabled: true
53
+ Exclude:
54
+ - '**/test/**/*'
55
+ - 'cli/*'
56
+
57
+ Style/BracesAroundHashParameters:
58
+ Enabled: false
59
+
60
+ Metrics/BlockLength:
61
+ Enabled: true
62
+ Exclude:
63
+ - '**/*_test.rb'
64
+
65
+ Layout/MultilineMethodCallIndentation:
66
+ Enabled: false
67
+
68
+ Style/RescueModifier:
69
+ Enabled: false
70
+
71
+ Style/GlobalVars:
72
+ Enabled: false
73
+
74
+ Layout/IndentHash:
75
+ Enabled: false
76
+
77
+ Naming/MethodName:
78
+ Enabled: false
79
+
80
+ Lint/AssignmentInCondition:
81
+ Enabled: false
82
+
83
+ Style/DoubleNegation:
84
+ Enabled: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atacama (0.1.6)
4
+ atacama (0.1.7)
5
5
  dry-types (~> 0.13.2)
6
6
 
7
7
  GEM
@@ -12,14 +12,10 @@ module Atacama
12
12
 
13
13
  # Determine the validity of a value for an optionally given type. Raises a
14
14
  # type error on failure.
15
- # @raise [Atacama::TypeError]
16
- # @returns Boolean
17
- def valid?(value)
18
- return true if type.nil?
19
- type[value]
20
- true
21
- rescue Dry::Types::ConstraintError => error
22
- raise TypeError, error.message
15
+ #
16
+ # @raise [Dry::Types::ConstraintError]
17
+ def validate!(value)
18
+ type[value] && nil unless type.nil?
23
19
  end
24
20
  end
25
21
  end
@@ -9,9 +9,10 @@ module Atacama
9
9
 
10
10
  # @param options [Hash] options schema
11
11
  # @param context [Atacama::Context] keyword arguments to validate
12
- def initialize(options:, context:)
12
+ def initialize(options:, context:, klass:)
13
13
  @options = options
14
14
  @context = context
15
+ @klass = klass
15
16
  end
16
17
 
17
18
  def call
@@ -20,12 +21,15 @@ module Atacama
20
21
 
21
22
  private
22
23
 
23
- attr_reader :options, :context
24
+ attr_reader :options, :context, :klass
24
25
 
25
26
  def detect_invalid_types!
26
- options.each do |(key, parameter)|
27
- raise ArgumentError, "option not found: #{key}" unless context.key?(key)
28
- parameter.valid? context[key]
27
+ options.each do |key, parameter|
28
+ begin
29
+ parameter.validate! context[key]
30
+ rescue Dry::Types::ConstraintError => e
31
+ raise OptionTypeMismatchError, %(#{klass} option :#{key} invalid: #{e.message})
32
+ end
29
33
  end
30
34
  end
31
35
  end
@@ -8,63 +8,127 @@ require 'atacama/contract/context'
8
8
  module Atacama
9
9
  # This class enables a DSL for creating a contract for the initializer
10
10
  class Contract
11
+ # @private
11
12
  RESERVED_KEYS = %i[call initialize context].freeze
12
13
 
14
+ # Namespace alias for easier reading when defining types.
13
15
  Types = Atacama::Types
14
16
 
17
+ # @private
15
18
  NameInterface = Types::Strict::Symbol.constrained(excluded_from: RESERVED_KEYS)
19
+
20
+ # @private
16
21
  ContextInterface = Types::Strict::Hash | Types.Instance(Context)
17
22
 
18
23
  class << self
19
- def injected=(hash)
20
- @injected = Types::Strict::Hash[hash]
24
+ # Define an initializer value.
25
+ #
26
+ # @example Set an option
27
+ # option :model. type: Types.Instance(User)
28
+ #
29
+ # @param name [Symbol] name of the argument
30
+ # @param type [Dry::Type?] the type object to optionally check
31
+ def option(name, type: nil)
32
+ options[NameInterface[name]] = Parameter.new(name: name, type: type)
33
+
34
+ define_method(name) { @context[name] }
35
+ define_method("#{name}?") { !!@context[name] }
21
36
  end
22
37
 
23
- def injected
24
- # Silences the VM warning about accessing uninitalized ivar
25
- defined?(@injected) ? @injected : {}
38
+ # Set the return type for the contract. This is only validated automatically
39
+ # through the #call class method.
40
+ #
41
+ # @param type [Dry::Type?] the type object to optionally check
42
+ def returns(type) # rubocop:disable Style/TrivialAccessors
43
+ @returns = type
26
44
  end
27
45
 
28
- def options
29
- @options ||= {}
46
+ # The main interface to executing contracts. Given a set of options it
47
+ # will check the parameter types as well as return types, if defined.
48
+ #
49
+ # @param arguments [Hash] keyword arguments that match the defined options
50
+ #
51
+ # @yield the block is evaluated in the context of the instance call method
52
+ #
53
+ # @return The value of the #call instance method.
54
+ def call(context = {}, &block)
55
+ new(context: context).call(&block).tap { |result| validate_return(result) }
30
56
  end
31
57
 
32
- def returns(type)
33
- @returns = type
58
+ # Inject dependencies statically in to the Contract object. Allows for easier
59
+ # composition of contracts when used in a Transaction.
60
+ #
61
+ # @example
62
+ # SampleClass.inject(user: User.new).call(attributes: { name: "Cindy" })
63
+ #
64
+ # @param injected [Hash] the options to inject in to the initializer
65
+ #
66
+ # @return [Class] a new class object that contains the injection
67
+ def inject(injected)
68
+ Validator.call({
69
+ options: Hash[injected.keys.zip(options.values_at(*injected.keys))],
70
+ context: Context.new(injected),
71
+ klass: self
72
+ })
73
+
74
+ Class.new(self) do
75
+ self.injected = injected
76
+ end
34
77
  end
35
78
 
79
+ # The defined return type on the Contract.
80
+ #
81
+ # @return [Dry::Type?] the type object to optionally check
36
82
  def return_type
37
83
  defined?(@returns) && @returns
38
84
  end
39
85
 
86
+ # Execute type checking on a value for the defined return value. Useful
87
+ # when executing `new` on these objects.
88
+ #
89
+ # @raise [Dry::Types::ConstraintError] a type check failure
90
+ #
91
+ # @param value [Any] the object to type check
40
92
  def validate_return(value)
41
- return_type && return_type[value]
93
+ Atacama.check(return_type, value) do |e|
94
+ raise ReturnTypeMismatchError, "#{self} return value invalid: #{e.message}"
95
+ end
42
96
  end
43
97
 
44
- # Define an initializer value.
45
- # @param [Symbol] name of the argument
46
- def option(name, **kwargs)
47
- options[NameInterface[name]] = Parameter.new(name: name, **kwargs)
48
-
49
- define_method(name) { @context[name] }
50
- define_method("#{name}?") { !!@context[name] }
98
+ # The defined options on the contract.
99
+ #
100
+ # @return [Hash<String, Atacama::Parameter>]
101
+ def options
102
+ @options ||= {}
51
103
  end
52
104
 
53
- def call(context = {}, &block)
54
- new(context: context).call(&block).tap do |result|
55
- validate_return(result)
105
+ # Executed by the Ruby VM at subclass time. Ensure that all internal state
106
+ # is copied to the new subclass.
107
+ def inherited(subclass)
108
+ subclass.returns(return_type)
109
+
110
+ options.each do |name, parameter|
111
+ subclass.option(name, type: parameter.type)
56
112
  end
57
113
  end
58
114
 
59
- def inject(injected)
60
- clone.tap do |clone|
61
- clone.injected = injected
62
- end
115
+ # @private
116
+ def injected=(hash)
117
+ @injected = Types::Strict::Hash[hash]
118
+ end
119
+
120
+ # @private
121
+ def injected
122
+ # Silences the VM warning about accessing uninitalized ivar
123
+ defined?(@injected) ? @injected : {}
63
124
  end
64
125
  end
65
126
 
66
127
  attr_reader :context
67
128
 
129
+ # @raise [Dry::Types::ConstraintError] a type check failure
130
+ #
131
+ # @param context [Hash] the values to satisfy the option definition
68
132
  def initialize(context: {}, **)
69
133
  ContextInterface[context] # Validate the type
70
134
 
@@ -72,19 +136,22 @@ module Atacama
72
136
  ctx.merge!(context.is_a?(Context) ? context.to_h : context)
73
137
  end
74
138
 
75
- Validator.call(options: self.class.options, context: @context)
139
+ Validator.call(options: self.class.options, context: @context, klass: self.class)
76
140
  end
77
141
 
78
- # Pretty pretty printing.
142
+ # @private
79
143
  def inspect
80
144
  "#<#{self.class}:0x%x %s>" % [
81
145
  object_id,
82
146
  self.class.options.keys.map do |option|
83
- "#{option}: #{context.send(option).inspect}"
147
+ "#{option}: #{context.send(option).inspect[0..20]}"
84
148
  end.join(' ')
85
149
  ]
86
150
  end
87
151
 
152
+ # @abstract
153
+ # The default entrypoint for all Contracts. This is executed and
154
+ # type checked when called from the Class#call.
88
155
  def call
89
156
  self
90
157
  end
@@ -9,6 +9,7 @@ module Atacama
9
9
  class Transaction < Contract
10
10
  include Values::Methods
11
11
 
12
+ # The return value of all Transactions.
12
13
  class Result < Contract
13
14
  option :value, type: Types::Any
14
15
  option :transaction, type: Types.Instance(Context)
@@ -17,28 +18,62 @@ module Atacama
17
18
  class << self
18
19
  attr_reader :return_option
19
20
 
20
- def returns_option(key, type)
21
+ def inherited(subclass)
22
+ super(subclass)
23
+ subclass.returns_option return_option, return_type
24
+ steps.each do |step|
25
+ subclass.step(step.name, with: step.with, yielding: step.yielding)
26
+ end
27
+ end
28
+
29
+ # Return the value of a given Option in the pipeline.
30
+ #
31
+ # @param key [Symbol] the option to read
32
+ # @param type [Dry::Type?] the type object to optionally check
33
+ def returns_option(key, type = nil)
21
34
  @return_option = key
22
35
 
23
36
  returns(
24
37
  Types.Instance(Result).constructor do |options|
25
- type[options.value]
38
+ Atacama.check(type, options.value) do |e|
39
+ raise ResultTypeMismatchError, "Invalid Result value for #{self}: #{e.message}"
40
+ end
41
+
26
42
  options
27
43
  end
28
44
  )
29
45
  end
30
46
 
31
- # @returns [Array<Atacama::Transaction::Definition>]
32
- def steps
33
- @steps ||= []
34
- end
35
-
36
47
  # Add a step to the processing queue.
48
+ #
49
+ # @example
50
+ # step :extract, with: UserParamsExtractor
51
+ #
52
+ # @example a yielding step
53
+ # step :wrap, with: Wrapper do
54
+ # step :extract, with: UserParamsExtractor
55
+ # end
56
+ #
37
57
  # @param name [Symbol] a unique name for a step
38
- def step(name, **kwargs, &block)
39
- kwargs[:yielding] = block_given? ? Class.new(self, &block) : nil
40
- kwargs[:with] ||= nil
41
- steps.push Definition.call(name: name, **kwargs)
58
+ # @param with [Contract, Proc, nil] the callable to execute
59
+ #
60
+ # @yield The captured block allows defining of child steps. The wrapper must implement yield.
61
+ def step(name, with: nil, yielding: nil, &block)
62
+ add_step({
63
+ name: name,
64
+ with: with,
65
+ yielding: yielding || block_given? ? Class.new(self, &block) : nil
66
+ })
67
+ end
68
+
69
+ # @private
70
+ def add_step(params)
71
+ steps.push(Definition.call(params))
72
+ end
73
+
74
+ # @private
75
+ def steps
76
+ @steps ||= []
42
77
  end
43
78
  end
44
79
 
@@ -48,6 +83,9 @@ module Atacama
48
83
  @return_value = nil
49
84
  end
50
85
 
86
+ # Trigger execution of the Transaction pipeline.
87
+ #
88
+ # @return [Atacama::Transaction::Result] final result with value
51
89
  def call
52
90
  execute(self.class.steps)
53
91
  Result.call(value: return_value, transaction: context)
data/lib/atacama/types.rb CHANGED
@@ -6,20 +6,41 @@ module Atacama
6
6
  include Dry::Types.module
7
7
  Boolean = Types::True | Types::False
8
8
 
9
+ # Defines a type which checks that the Option value contains a valid
10
+ # data structure.
11
+ #
12
+ # @param map [Hash] schema definition of the option value
13
+ #
14
+ # @return [Dry::Type]
9
15
  def self.Option(**map)
10
- Instance(Values::Option).constructor do |options|
11
- if options.is_a? Values::Option
12
- map.each { |key, type| type[options.value[key]] }
16
+ Instance(Values::Option).constructor do |value_object|
17
+ if value_object.is_a? Values::Option
18
+ map.each do |key, type|
19
+ Atacama.check(type, value_object.value[key]) do |e|
20
+ raise OptionTypeMismatchError, "Invalid Option value type: #{e.message}"
21
+ end
22
+ end
13
23
  end
14
24
 
15
- options
25
+ value_object
16
26
  end
17
27
  end
18
28
 
29
+ # Defines a type which checks that the Return value contains a valid
30
+ # object
31
+ #
32
+ # @param type [Dry::Type]
33
+ #
34
+ # @return [Dry::Type]
19
35
  def self.Return(type)
20
- Instance(Values::Return).constructor do |options|
21
- type[options.value] if options.is_a? Values::Return
22
- options
36
+ Instance(Values::Return).constructor do |value_object|
37
+ if value_object.is_a?(Values::Return)
38
+ Atacama.check(type, value_object.value) do |e|
39
+ raise ReturnTypeMismatchError, "Invalid Return Value type: #{e.message}"
40
+ end
41
+ end
42
+
43
+ value_object
23
44
  end
24
45
  end
25
46
  end
@@ -1,3 +1,3 @@
1
1
  module Atacama
2
- VERSION = '0.1.6'.freeze
2
+ VERSION = '0.1.7'.freeze
3
3
  end
data/lib/atacama.rb CHANGED
@@ -5,7 +5,14 @@ require 'atacama/transaction'
5
5
  require 'atacama/step'
6
6
 
7
7
  module Atacama
8
- ArgumentError = Class.new(StandardError)
9
- TypeError = Class.new(StandardError)
10
- MissingReturn = Class.new(StandardError)
8
+ OptionTypeMismatchError = Class.new(StandardError)
9
+ ReturnTypeMismatchError = Class.new(StandardError)
10
+ ResultTypeMismatchError = Class.new(StandardError)
11
+
12
+ def self.check(type, value)
13
+ type && type[value]
14
+ nil
15
+ rescue Dry::Types::ConstraintError => e
16
+ yield e
17
+ end
11
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atacama
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Johnston
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-12 00:00:00.000000000 Z
11
+ date: 2018-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-types
@@ -74,6 +74,7 @@ extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
76
  - ".gitignore"
77
+ - ".rubocop.yml"
77
78
  - ".travis.yml"
78
79
  - Gemfile
79
80
  - Gemfile.lock