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 +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
|