atacama 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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