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 +4 -4
- data/.rubocop.yml +84 -0
- data/Gemfile.lock +1 -1
- data/lib/atacama/contract/parameter.rb +4 -8
- data/lib/atacama/contract/validator.rb +9 -5
- data/lib/atacama/contract.rb +94 -27
- data/lib/atacama/transaction.rb +49 -11
- data/lib/atacama/types.rb +28 -7
- data/lib/atacama/version.rb +1 -1
- data/lib/atacama.rb +10 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2d78853c9e718c4ea9ff6ea4fbc8e729cd2f48cf
|
4
|
+
data.tar.gz: addda9f05177a741fc8d2dd36d465646e37249c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
@@ -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
|
-
#
|
16
|
-
# @
|
17
|
-
def
|
18
|
-
|
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 |
|
27
|
-
|
28
|
-
|
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
|
data/lib/atacama/contract.rb
CHANGED
@@ -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
|
-
|
20
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
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
|
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
|
-
#
|
45
|
-
#
|
46
|
-
|
47
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
#
|
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
|
data/lib/atacama/transaction.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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 |
|
11
|
-
if
|
12
|
-
map.each
|
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
|
-
|
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 |
|
21
|
-
|
22
|
-
|
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
|
data/lib/atacama/version.rb
CHANGED
data/lib/atacama.rb
CHANGED
@@ -5,7 +5,14 @@ require 'atacama/transaction'
|
|
5
5
|
require 'atacama/step'
|
6
6
|
|
7
7
|
module Atacama
|
8
|
-
|
9
|
-
|
10
|
-
|
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.
|
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-
|
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
|