edgycircle_toolbox 0.1.0
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 +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/README.md +214 -0
- data/Rakefile +10 -0
- data/edgycircle_toolbox.gemspec +39 -0
- data/lib/edgycircle_toolbox.rb +5 -0
- data/lib/edgycircle_toolbox/cqrs/command.rb +31 -0
- data/lib/edgycircle_toolbox/cqrs/command_builder.rb +42 -0
- data/lib/edgycircle_toolbox/cqrs/command_bus.rb +62 -0
- data/lib/edgycircle_toolbox/cqrs/command_result.rb +33 -0
- data/lib/edgycircle_toolbox/cqrs/handler.rb +9 -0
- data/lib/edgycircle_toolbox/cqrs/message_bus.rb +22 -0
- data/lib/edgycircle_toolbox/cqrs/model_collector.rb +18 -0
- data/lib/edgycircle_toolbox/has_attributes.rb +37 -0
- data/lib/edgycircle_toolbox/sonapi/blueprint.rb +28 -0
- data/lib/edgycircle_toolbox/sonapi/deserializer.rb +34 -0
- data/lib/edgycircle_toolbox/sonapi/dynamic_resource.rb +13 -0
- data/lib/edgycircle_toolbox/sonapi/error_resource.rb +22 -0
- data/lib/edgycircle_toolbox/sonapi/resource.rb +57 -0
- data/lib/edgycircle_toolbox/sonapi/serializable_error.rb +17 -0
- data/lib/edgycircle_toolbox/sonapi/serializer.rb +33 -0
- data/lib/edgycircle_toolbox/sonapi/validation_error.rb +22 -0
- data/lib/edgycircle_toolbox/version.rb +3 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0fac7454ff83b9d4e105dd363d09f0db834c9216
|
4
|
+
data.tar.gz: 394e20e69e366e0049e81cd827e7fd3e2bf67e1b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 35b8761a11d9e893dd0872c10fee0f01e9ea57c6a0c161089b89a005e4434524e7a2f05ce2519e596b3b2fb4daea06f4a31f2836e3a9292d90d8d32ce1e531c5
|
7
|
+
data.tar.gz: 72421b139359fe670c455ba756f15164f79a38bf9c84f6c70777b64df7f12491d1c065473677ecae032f565cc06afe28062976e065b5d46c009d4e0c8ec81df9
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
A loose collection of Ruby related code that currently has no specific place to live in.
|
2
|
+
|
3
|
+
## HasAttributes Module
|
4
|
+
~~~ruby
|
5
|
+
require 'edgycircle_toolbox/has_attributes'
|
6
|
+
|
7
|
+
class Dummy
|
8
|
+
include EdgycircleToolbox::HasAttributes
|
9
|
+
attributes :a, :b
|
10
|
+
end
|
11
|
+
|
12
|
+
dummy = Dummy.new({ a: 1, b: 2, c: 3 })
|
13
|
+
|
14
|
+
dummy.a # => 1
|
15
|
+
dummy.b # => 2
|
16
|
+
dummy.c # => NoMethodError
|
17
|
+
|
18
|
+
dummy.attributes # => [:a, :b]
|
19
|
+
~~~
|
20
|
+
|
21
|
+
## CQRS CommandResult
|
22
|
+
Returning `self` from `add_error`, `add_event` and `set_command` allows us write concise code like `return result.add_error(:authentication_error) unless authenticated?`.
|
23
|
+
~~~ruby
|
24
|
+
require 'edgycircle_toolbox/cqrs/command_result'
|
25
|
+
|
26
|
+
command = :command_a
|
27
|
+
result = EdgycircleToolbox::CQRS::CommandResult.new(command)
|
28
|
+
|
29
|
+
result.add_error(:error) # => result
|
30
|
+
result.add_event(:event) # => result
|
31
|
+
result.set_command(:command_b) # => result
|
32
|
+
result.failure? # => true
|
33
|
+
~~~
|
34
|
+
|
35
|
+
## CQRS Handler Module
|
36
|
+
A handler class has to implement `call(command)` and accept a command instance as parameter.
|
37
|
+
~~~ruby
|
38
|
+
require 'edgycircle_toolbox/cqrs/handler'
|
39
|
+
|
40
|
+
class DummyHandler
|
41
|
+
include EdgycircleToolbox::CQRS::Handler
|
42
|
+
|
43
|
+
def call(command)
|
44
|
+
# Business Logic
|
45
|
+
end
|
46
|
+
end
|
47
|
+
~~~
|
48
|
+
|
49
|
+
## CQRS Command Module
|
50
|
+
A command class can have a schema and attributes. The schema can be used by the command bus to validate the data before a command is built. Currently the definitions are redundant, changing this could be an improvement made in the future. Every command has a default attribute `id` and a schema to validate its presence and make sure it's a string.
|
51
|
+
|
52
|
+
The module uses [dry-validation](http://dry-rb.org/gems/dry-validation/) for the schema and its validation.
|
53
|
+
~~~ruby
|
54
|
+
require 'edgycircle_toolbox/cqrs/command'
|
55
|
+
|
56
|
+
class DummyCommand
|
57
|
+
include EdgycircleToolbox::CQRS::Command
|
58
|
+
|
59
|
+
schema do
|
60
|
+
required(:title).filled(:str?)
|
61
|
+
end
|
62
|
+
|
63
|
+
attributes :title
|
64
|
+
end
|
65
|
+
~~~
|
66
|
+
|
67
|
+
## CQRS Command Bus
|
68
|
+
Can build commands from a parameter hash if the data fits the commands schema. A submitted command is passed to the corresponding handler. Events from the command handler get published on the message bus. Both `build` and `submit` return a `CommandResult`.
|
69
|
+
~~~ruby
|
70
|
+
require 'edgycircle_toolbox/cqrs/command_bus'
|
71
|
+
|
72
|
+
command_bus = EdgycircleToolbox::CQRS::CommandBus.new
|
73
|
+
command_bus.register EnterTicketCommand, EnterTicketHandler
|
74
|
+
|
75
|
+
command_result = command_bus.build(parameter_hash)
|
76
|
+
command_result = command_bus.submit(command_result.command)
|
77
|
+
~~~
|
78
|
+
|
79
|
+
## CQRS Message Bus
|
80
|
+
~~~ruby
|
81
|
+
require 'edgycircle_toolbox/cqrs/message_bus'
|
82
|
+
|
83
|
+
class RandomEvent
|
84
|
+
end
|
85
|
+
|
86
|
+
class CommonEvent
|
87
|
+
end
|
88
|
+
|
89
|
+
EdgycircleToolbox::CQRS::MessageBus.subscribe([RandomEvent], ->(event) { puts 'Output 1' })
|
90
|
+
EdgycircleToolbox::CQRS::MessageBus.subscribe([RandomEvent, CommonEvent], ->(event) { puts 'Output 2' })
|
91
|
+
|
92
|
+
EdgycircleToolbox::CQRS::MessageBus.publish(RandomEvent.new)
|
93
|
+
# =>
|
94
|
+
# Output 1
|
95
|
+
# Output 2
|
96
|
+
|
97
|
+
EdgycircleToolbox::CQRS::MessageBus.publish(CommonEvent.new)
|
98
|
+
# =>
|
99
|
+
# Output 2
|
100
|
+
~~~
|
101
|
+
|
102
|
+
## CQRS Model Collector
|
103
|
+
Can be used to collect data based on events.
|
104
|
+
~~~ruby
|
105
|
+
require 'edgycircle_toolbox/cqrs/model_collector'
|
106
|
+
|
107
|
+
class RandomEvent
|
108
|
+
end
|
109
|
+
|
110
|
+
class CommonEvent
|
111
|
+
end
|
112
|
+
|
113
|
+
EdgycircleToolbox::CQRS::ModelCollector.register(RandomEvent, ->(event) { [1, 2] })
|
114
|
+
EdgycircleToolbox::CQRS::ModelCollector.register(RandomEvent, ->(event) { [3] })
|
115
|
+
EdgycircleToolbox::CQRS::ModelCollector.register(CommonEvent, ->(event) { [4] })
|
116
|
+
|
117
|
+
EdgycircleToolbox::CQRS::ModelCollector.for_events([
|
118
|
+
RandomEvent.new,
|
119
|
+
CommonEvent.new
|
120
|
+
])
|
121
|
+
# =>
|
122
|
+
# [1, 2, 3, 4]
|
123
|
+
~~~
|
124
|
+
|
125
|
+
## Sonapi Resource Module
|
126
|
+
~~~ruby
|
127
|
+
require 'edgycircle_toolbox/sonapi/resource'
|
128
|
+
|
129
|
+
class CommandResource
|
130
|
+
include EdgycircleToolbox::Sonapi::Resource
|
131
|
+
|
132
|
+
type "commands"
|
133
|
+
|
134
|
+
dynamic_attributes Proc.new { |object| object.attribute_names - [:id] }
|
135
|
+
parameter_filter Proc.new { |name, value| true }
|
136
|
+
end
|
137
|
+
|
138
|
+
class TicketResource
|
139
|
+
include EdgycircleToolbox::Sonapi::Resource
|
140
|
+
|
141
|
+
type "tickets"
|
142
|
+
|
143
|
+
attribute :estimate
|
144
|
+
end
|
145
|
+
~~~
|
146
|
+
|
147
|
+
## Sonapi Dynamic Resource
|
148
|
+
~~~ruby
|
149
|
+
require 'edgycircle_toolbox/sonapi/dynamic_resource'
|
150
|
+
|
151
|
+
class Dummy
|
152
|
+
end
|
153
|
+
|
154
|
+
class Tree
|
155
|
+
end
|
156
|
+
|
157
|
+
class DummyResource
|
158
|
+
include EdgycircleToolbox::Sonapi::Resource
|
159
|
+
|
160
|
+
type "dummies"
|
161
|
+
end
|
162
|
+
|
163
|
+
class TreeResource
|
164
|
+
include EdgycircleToolbox::Sonapi::Resource
|
165
|
+
|
166
|
+
type "trees"
|
167
|
+
end
|
168
|
+
|
169
|
+
EdgycircleToolbox::Sonapi::DynamicResource.register(Dummy, DummyResource)
|
170
|
+
EdgycircleToolbox::Sonapi::DynamicResource.register(Tree, TreeResource)
|
171
|
+
|
172
|
+
EdgycircleToolbox::Sonapi::DynamicResource.serialize([Dummy.new, Tree.new])
|
173
|
+
~~~
|
174
|
+
|
175
|
+
## Sonapi Error Resource
|
176
|
+
The `ErrorResource` can serialize errors conforming to the `SerializableError` interface.
|
177
|
+
~~~ruby
|
178
|
+
require 'edgycircle_toolbox/sonapi/error_resource'
|
179
|
+
|
180
|
+
EdgycircleToolbox::Sonapi::ErrorResource.serialize([error_a, error_b])
|
181
|
+
~~~
|
182
|
+
|
183
|
+
## Sonapi SerializableError Module
|
184
|
+
An error class has to implement the `title`, `pointer` and `detail` methods.
|
185
|
+
~~~ruby
|
186
|
+
require 'edgycircle_toolbox/sonapi/serializable_error'
|
187
|
+
|
188
|
+
class EmailTakenError
|
189
|
+
include EdgycircleToolbox::Sonapi::SerializableError
|
190
|
+
|
191
|
+
def initialize(email)
|
192
|
+
@email = email
|
193
|
+
end
|
194
|
+
|
195
|
+
def title
|
196
|
+
"Email Taken Error"
|
197
|
+
end
|
198
|
+
|
199
|
+
def pointer
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
|
203
|
+
def detail
|
204
|
+
"The email #{@email} is already taken, please use something different"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
~~~
|
208
|
+
|
209
|
+
## Sonapi ValidationError
|
210
|
+
~~~ruby
|
211
|
+
require 'edgycircle_toolbox/sonapi/validation_error'
|
212
|
+
|
213
|
+
EdgycircleToolbox::Sonapi::ValidationError.new(:title, "Title is not long enough")
|
214
|
+
~~~
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'edgycircle_toolbox/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "edgycircle_toolbox"
|
8
|
+
spec.version = EdgycircleToolbox::VERSION
|
9
|
+
spec.authors = ["David Strauß"]
|
10
|
+
spec.email = ["david@strauss.io"]
|
11
|
+
|
12
|
+
spec.summary = %q{A loose collection of Ruby related code that currently has specific place to live in.}
|
13
|
+
spec.description = %q{A loose collection of Ruby related code that currently has specific place to live in.}
|
14
|
+
spec.homepage = "https://www.edgycircle.com"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
"public gem pushes."
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
34
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
36
|
+
|
37
|
+
spec.add_dependency "dry-validation", "~> 0.10"
|
38
|
+
spec.add_dependency "dry-container", "~> 0.5"
|
39
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'dry-validation'
|
2
|
+
require 'edgycircle_toolbox/has_attributes'
|
3
|
+
|
4
|
+
module EdgycircleToolbox
|
5
|
+
module CQRS
|
6
|
+
module Command
|
7
|
+
module ClassMethods
|
8
|
+
def schema(&block)
|
9
|
+
if block_given?
|
10
|
+
base = Dry::Validation.Schema(build: false) do
|
11
|
+
required(:id).filled(:str?)
|
12
|
+
end
|
13
|
+
|
14
|
+
@schema = Dry::Validation.Form(rules: base.rules, &block)
|
15
|
+
else
|
16
|
+
@schema
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.included(base)
|
22
|
+
base.extend(ClassMethods)
|
23
|
+
base.include(HasAttributes)
|
24
|
+
|
25
|
+
base.class_eval do
|
26
|
+
attributes :id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'dry-container'
|
2
|
+
|
3
|
+
module EdgycircleToolbox
|
4
|
+
module CQRS
|
5
|
+
class CommandBuilder
|
6
|
+
class DecoratorContainer
|
7
|
+
include Dry::Container::Mixin
|
8
|
+
|
9
|
+
configure do |config|
|
10
|
+
config.registry = ->(container, key, item, options) { (container[key] ||= []).push(item) }
|
11
|
+
config.resolver = ->(container, key) { container[key] || [] }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@command_container = Dry::Container.new
|
17
|
+
@decorator_container = DecoratorContainer.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def register(command_class)
|
21
|
+
key = command_class.name.split("::").last.gsub("Command", "")
|
22
|
+
@command_container.register(key, command_class)
|
23
|
+
end
|
24
|
+
|
25
|
+
def decorate(command_class, decorator_class)
|
26
|
+
@decorator_container.register(command_class, decorator_class)
|
27
|
+
end
|
28
|
+
|
29
|
+
def build(valid_parameters, context = nil)
|
30
|
+
command_class = @command_container.resolve(valid_parameters[:name])
|
31
|
+
decorator_classes = @decorator_container.resolve(command_class)
|
32
|
+
command = command_class.new(valid_parameters)
|
33
|
+
decorator_classes.inject(command) { |command, decorator_class| decorator_class.new(command, context) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate(original_parameters)
|
37
|
+
command_class = @command_container.resolve(original_parameters[:name])
|
38
|
+
command_class.schema.call(original_parameters)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'dry-container'
|
2
|
+
require 'edgycircle_toolbox/cqrs/message_bus'
|
3
|
+
require 'edgycircle_toolbox/cqrs/command_result'
|
4
|
+
require 'edgycircle_toolbox/cqrs/command_builder'
|
5
|
+
require 'edgycircle_toolbox/sonapi/validation_error'
|
6
|
+
|
7
|
+
module EdgycircleToolbox
|
8
|
+
module CQRS
|
9
|
+
class CommandBus
|
10
|
+
def initialize
|
11
|
+
@container = Dry::Container.new
|
12
|
+
@command_builder = CommandBuilder.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def register(command, handler)
|
16
|
+
command_builder.register(command)
|
17
|
+
|
18
|
+
container.namespace(:handlers) do
|
19
|
+
register(command, handler)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def decorate_command(command_class, decorator_class)
|
24
|
+
command_builder.decorate(command_class, decorator_class)
|
25
|
+
end
|
26
|
+
|
27
|
+
def submit(command)
|
28
|
+
result = container.resolve("handlers.#{command.class.name}").new.call(command)
|
29
|
+
result.events.each { |event| MessageBus.publish(event) }
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def build(original_parameters, context = nil)
|
34
|
+
result = CommandResult.new
|
35
|
+
validation = command_builder.validate(original_parameters)
|
36
|
+
|
37
|
+
if validation.failure?
|
38
|
+
build_validation_errors(validation).each { |error| result.add_error(error) }
|
39
|
+
else
|
40
|
+
validated_parameters = validation.output
|
41
|
+
command = command_builder.build(validated_parameters, context)
|
42
|
+
result.set_command(command)
|
43
|
+
end
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
attr_reader :container, :command_builder
|
50
|
+
|
51
|
+
def command_class_for(name)
|
52
|
+
container.resolve("commands.#{name}")
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_validation_errors(validation)
|
56
|
+
validation.messages(full: true).map do |attribute, messages|
|
57
|
+
messages.map { |message| ValidationError.new(attribute, message) }
|
58
|
+
end.flatten
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module EdgycircleToolbox
|
2
|
+
module CQRS
|
3
|
+
class CommandResult
|
4
|
+
attr_reader :command, :errors, :events
|
5
|
+
|
6
|
+
def initialize(command = nil)
|
7
|
+
@command = command
|
8
|
+
@events = []
|
9
|
+
@errors = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_error(error)
|
13
|
+
errors << error
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_event(event)
|
18
|
+
events << event
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_command(command)
|
23
|
+
@command = command
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure?
|
28
|
+
errors.size > 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'dry-container'
|
2
|
+
|
3
|
+
module EdgycircleToolBox
|
4
|
+
module CQRS
|
5
|
+
class MessageBus
|
6
|
+
extend Dry::Container::Mixin
|
7
|
+
|
8
|
+
configure do |config|
|
9
|
+
config.registry = ->(container, key, item, options) { (container[key] ||= []).push(item) }
|
10
|
+
config.resolver = ->(container, key) { container[key] || [] }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.subscribe(events, callable)
|
14
|
+
events.each { |event| register(event, callable) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.publish(event)
|
18
|
+
resolve(event.class).each { |callable| callable.call(event) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'dry-container'
|
2
|
+
|
3
|
+
module EdgycircleToolbox
|
4
|
+
module CQRS
|
5
|
+
class ModelCollector
|
6
|
+
extend Dry::Container::Mixin
|
7
|
+
|
8
|
+
configure do |config|
|
9
|
+
config.registry = ->(container, key, item, options) { (container[key] ||= []).push(item) }
|
10
|
+
config.resolver = ->(container, key) { container[key] || [] }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.for_events(events)
|
14
|
+
events.map { |event| resolve(event.class).map { |callable| callable.call(event) } }.flatten
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module EdgycircleToolbox
|
2
|
+
module HasAttributes
|
3
|
+
module ClassMethods
|
4
|
+
def attributes(*attributes)
|
5
|
+
@attributes ||= []
|
6
|
+
|
7
|
+
return @attributes unless attributes.any?
|
8
|
+
|
9
|
+
attributes.each do |attribute|
|
10
|
+
define_attr_accessor(attribute)
|
11
|
+
@attributes << attribute
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def define_attr_accessor(attribute)
|
16
|
+
attr_accessor(attribute)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.included(base)
|
21
|
+
base.class_eval do
|
22
|
+
extend ClassMethods
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(attributes = {})
|
27
|
+
attributes.each do |key, value|
|
28
|
+
setter = "#{key}="
|
29
|
+
public_send(setter, value) if respond_to?(setter)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes
|
34
|
+
self.class.attributes
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module EdgycircleToolbox
|
2
|
+
module Sonapi
|
3
|
+
class Blueprint
|
4
|
+
attr_accessor :type, :attribute_names, :dynamic_attributes, :parameter_filter
|
5
|
+
|
6
|
+
def initialize(type, attribute_names = [])
|
7
|
+
@type = type
|
8
|
+
@attribute_names = attribute_names
|
9
|
+
@dynamic_attributes = Proc.new { [] }
|
10
|
+
@parameter_filter = Proc.new do |name, value|
|
11
|
+
@attribute_names.include?(name.to_s) || name == :id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_attribute(attribute_name)
|
16
|
+
@attribute_names << attribute_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def attribute_names(object)
|
20
|
+
@attribute_names + dynamic_attributes.call(object).map(&:to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
def deserialize_parameter?(name, value)
|
24
|
+
parameter_filter.call(name, value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module EdgycircleToolbox
|
2
|
+
module Sonapi
|
3
|
+
class Deserializer
|
4
|
+
attr_reader :blueprint
|
5
|
+
|
6
|
+
def initialize(blueprint)
|
7
|
+
@blueprint = blueprint
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(parameters)
|
11
|
+
Hash[
|
12
|
+
filter_parameter_pairs(parameter_pairs(parameters))
|
13
|
+
]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def parameter_pairs(parameters)
|
18
|
+
id_pair = [:id, parameters["data"]["id"]]
|
19
|
+
|
20
|
+
parameters["data"]["attributes"].to_a.map do |pair|
|
21
|
+
[format_attribute_name(pair[0]), pair[1]]
|
22
|
+
end << id_pair
|
23
|
+
end
|
24
|
+
|
25
|
+
def format_attribute_name(attribute_name)
|
26
|
+
attribute_name.to_s.gsub("-", "_").to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
def filter_parameter_pairs(pairs)
|
30
|
+
pairs.select { |pair| blueprint.deserialize_parameter?(pair[0], pair[1]) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module EdgycircleToolbox
|
2
|
+
module Sonapi
|
3
|
+
class ErrorResource
|
4
|
+
def self.serialize(errors)
|
5
|
+
{
|
6
|
+
"errors" => errors.map do |error|
|
7
|
+
hash = {
|
8
|
+
"title" => error.title,
|
9
|
+
"detail" => error.detail
|
10
|
+
}
|
11
|
+
|
12
|
+
if error.pointer
|
13
|
+
hash["source"] = { "pointer" => error.pointer }
|
14
|
+
end
|
15
|
+
|
16
|
+
hash
|
17
|
+
end
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'edgycircle_toolbox/sonapi/blueprint'
|
2
|
+
require 'edgycircle_toolbox/sonapi/serializer'
|
3
|
+
require 'edgycircle_toolbox/sonapi/deserializer'
|
4
|
+
|
5
|
+
module EdgycircleToolbox
|
6
|
+
module Sonapi
|
7
|
+
module Resource
|
8
|
+
module ClassMethods
|
9
|
+
def type(type)
|
10
|
+
@blueprint = Blueprint.new(type)
|
11
|
+
end
|
12
|
+
|
13
|
+
def serialize_resource(object)
|
14
|
+
Serializer.new(@blueprint).call(object)
|
15
|
+
end
|
16
|
+
|
17
|
+
def serialize(object, options = {})
|
18
|
+
if object.respond_to?(:map)
|
19
|
+
data = object.map { |item| serialize_resource(item) }
|
20
|
+
else
|
21
|
+
data = serialize_resource(object)
|
22
|
+
end
|
23
|
+
|
24
|
+
result = {
|
25
|
+
"data" => data
|
26
|
+
}
|
27
|
+
|
28
|
+
if options[:included_resources]
|
29
|
+
result.merge!({ "included" => options[:included_resources] })
|
30
|
+
end
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def deserialize(parameters)
|
36
|
+
Deserializer.new(@blueprint).call(parameters)
|
37
|
+
end
|
38
|
+
|
39
|
+
def attribute(attribute_name)
|
40
|
+
@blueprint.add_attribute attribute_name.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def dynamic_attributes(callable)
|
44
|
+
@blueprint.dynamic_attributes = callable
|
45
|
+
end
|
46
|
+
|
47
|
+
def parameter_filter(callable)
|
48
|
+
@blueprint.parameter_filter = callable
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.included(base)
|
53
|
+
base.extend(ClassMethods)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module EdgycircleToolbox
|
2
|
+
module Sonapi
|
3
|
+
class Serializer
|
4
|
+
attr_reader :blueprint
|
5
|
+
|
6
|
+
def initialize(blueprint)
|
7
|
+
@blueprint = blueprint
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(object)
|
11
|
+
{
|
12
|
+
"type" => blueprint.type,
|
13
|
+
"id" => object.id,
|
14
|
+
"attributes" => collect_attributes(object)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def collect_attributes(object)
|
20
|
+
Hash[
|
21
|
+
blueprint.attribute_names(object).map do |attribute_name|
|
22
|
+
[format_attribute_name(attribute_name), object.send(attribute_name)]
|
23
|
+
end
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
def format_attribute_name(attribute_name)
|
28
|
+
attribute_name.gsub("_", "-")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module EdgycircleToolbox
|
2
|
+
module Sonapi
|
3
|
+
class ValidationError
|
4
|
+
include SerializableError
|
5
|
+
|
6
|
+
attr_reader :detail, :attribute
|
7
|
+
|
8
|
+
def initialize(attribute, detail)
|
9
|
+
@attribute = attribute
|
10
|
+
@detail = detail
|
11
|
+
end
|
12
|
+
|
13
|
+
def pointer
|
14
|
+
"data/attributes/#{attribute.to_s.gsub("_", "-")}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def title
|
18
|
+
"Validation Error"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: edgycircle_toolbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Strauß
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-01-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.13'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.13'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: dry-validation
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.10'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: dry-container
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.5'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.5'
|
83
|
+
description: A loose collection of Ruby related code that currently has specific place
|
84
|
+
to live in.
|
85
|
+
email:
|
86
|
+
- david@strauss.io
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- Gemfile
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- edgycircle_toolbox.gemspec
|
96
|
+
- lib/edgycircle_toolbox.rb
|
97
|
+
- lib/edgycircle_toolbox/cqrs/command.rb
|
98
|
+
- lib/edgycircle_toolbox/cqrs/command_builder.rb
|
99
|
+
- lib/edgycircle_toolbox/cqrs/command_bus.rb
|
100
|
+
- lib/edgycircle_toolbox/cqrs/command_result.rb
|
101
|
+
- lib/edgycircle_toolbox/cqrs/handler.rb
|
102
|
+
- lib/edgycircle_toolbox/cqrs/message_bus.rb
|
103
|
+
- lib/edgycircle_toolbox/cqrs/model_collector.rb
|
104
|
+
- lib/edgycircle_toolbox/has_attributes.rb
|
105
|
+
- lib/edgycircle_toolbox/sonapi/blueprint.rb
|
106
|
+
- lib/edgycircle_toolbox/sonapi/deserializer.rb
|
107
|
+
- lib/edgycircle_toolbox/sonapi/dynamic_resource.rb
|
108
|
+
- lib/edgycircle_toolbox/sonapi/error_resource.rb
|
109
|
+
- lib/edgycircle_toolbox/sonapi/resource.rb
|
110
|
+
- lib/edgycircle_toolbox/sonapi/serializable_error.rb
|
111
|
+
- lib/edgycircle_toolbox/sonapi/serializer.rb
|
112
|
+
- lib/edgycircle_toolbox/sonapi/validation_error.rb
|
113
|
+
- lib/edgycircle_toolbox/version.rb
|
114
|
+
homepage: https://www.edgycircle.com
|
115
|
+
licenses:
|
116
|
+
- MIT
|
117
|
+
metadata:
|
118
|
+
allowed_push_host: https://rubygems.org
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 2.4.5
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: A loose collection of Ruby related code that currently has specific place
|
139
|
+
to live in.
|
140
|
+
test_files: []
|