nucleus-core 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/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/exe/nucleus +4 -0
- data/lib/nucleus_core/aggregate.rb +1 -0
- data/lib/nucleus_core/basic_object.rb +24 -0
- data/lib/nucleus_core/cli.rb +6 -0
- data/lib/nucleus_core/exceptions.rb +5 -0
- data/lib/nucleus_core/extensions/array.rb +11 -0
- data/lib/nucleus_core/extensions/rack.rb +86 -0
- data/lib/nucleus_core/operation.rb +68 -0
- data/lib/nucleus_core/policy.rb +23 -0
- data/lib/nucleus_core/repository.rb +6 -0
- data/lib/nucleus_core/responder.rb +130 -0
- data/lib/nucleus_core/response_adapter.rb +81 -0
- data/lib/nucleus_core/version.rb +3 -0
- data/lib/nucleus_core/views/error_view.rb +17 -0
- data/lib/nucleus_core/views/view.rb +5 -0
- data/lib/nucleus_core/workflow.rb +180 -0
- data/lib/nucleus_core.rb +94 -0
- metadata +194 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b2545a3c813f8b6c7fc829e527ed77dc5105b10857af9ea2621bcb8ac81264de
|
4
|
+
data.tar.gz: 2b1724c47837c91d2039625f3efdea8fbc550d3513f0a9a253b0cb5104dd7c8c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84e7a40ba56b63f86dedf43cf139386cb73e128e30986ba3fb169ac39e46dea24625658f466183302a5706edb9aca5b1414b8392663f753abf417807a2b32dae
|
7
|
+
data.tar.gz: 5f7237b583e69352ebd3a805cd3bd4bbbe73951a9e06b128f8bbac7950631a110fd1abcd246c284fc30c6a958574d6bb804fbf4077f31606fd0b0ecab4f63f01
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 dodgerogers
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
# NucleusCore
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/nucleus-core)
|
4
|
+
[](https://app.circleci.com/pipelines/github/dodgerogers/nucleus-core?branch=main)
|
5
|
+
[](https://codeclimate.com/github/dodgerogers/nucleus-core)
|
6
|
+
|
7
|
+
NucleusCore Core is a framework to express and orchestrate business logic in a way that is agnostic to the framework.
|
8
|
+
|
9
|
+
## This gem is still very much in development. A `nucleus-rails` gem will handle the adaptation of NucleusCore::View objects to the rails rendering methods.
|
10
|
+
|
11
|
+
Here are the classes NucleusCore exposes, they have preordained responsibilities, can be composed together, and tested simply in isolation from the framework.
|
12
|
+
|
13
|
+
- Policy (Authorization) - Can this user perform this process?
|
14
|
+
- Operation (Services) - Executes a single unit of business logic, or side effect (ScheduleAppointment, CancelOrder, UpdateAddress).
|
15
|
+
- Workflow (Service Orchestration) - Excecutes multiple units of work, and side effects (ApproveLoan, TakePayment, CancelFulfillments).
|
16
|
+
- View (Presentation) - A presentation object which can render to multiple formats.
|
17
|
+
- Repository (Data access) - Interacts with data sources to hide the implementation details to callers, and return Aggregates. Data sources could be an API, ActiveRecord, SQL, a local file, etc.
|
18
|
+
- Aggregate (Domain/business Object) - Maps data from the data source to an object the aplication defines, known as an anti corruption layer.
|
19
|
+
|
20
|
+
Below is an example using NucleusCore Core with Rails:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
# controllers/payments_controller.rb
|
24
|
+
class PaymentsController < ApplicationController
|
25
|
+
def create
|
26
|
+
NucleusCore::Responder.handle_response do
|
27
|
+
policy.enforce!(:can_write?)
|
28
|
+
|
29
|
+
context, _process = HandleCheckoutWorkflow.call(invoice_params)
|
30
|
+
|
31
|
+
return context if !context.success?
|
32
|
+
|
33
|
+
return PaymentView.new(cart: context.cart, paid: context.paid)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def policy
|
40
|
+
Policy.new(current_user)
|
41
|
+
end
|
42
|
+
|
43
|
+
def invoice_params
|
44
|
+
params.slice(:cart_id)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# workflows/handle_checkout_workflow.rb
|
49
|
+
class HandleCheckoutWorkflow < NucleusCore::Workflow
|
50
|
+
def define
|
51
|
+
start_node(continue: :calculate_amount)
|
52
|
+
register_node(
|
53
|
+
state: :calculate_amount,
|
54
|
+
operation: FetchShoppingCart,
|
55
|
+
determine_signal: ->(context) { context.cart.total > 10 ? :discount : :pay },
|
56
|
+
signals: { discount: :apply_discount, pay: :take_payment }
|
57
|
+
)
|
58
|
+
register_node(
|
59
|
+
state: :apply_discount,
|
60
|
+
operation: ApplyDiscountToShoppingCart,
|
61
|
+
signals: { continue: :take_payment }
|
62
|
+
)
|
63
|
+
register_node(
|
64
|
+
state: :take_payment,
|
65
|
+
operation: ->(context) { context.paid = context.cart.paid },
|
66
|
+
determine_signal: ->(_) { :completed }
|
67
|
+
)
|
68
|
+
register_node(
|
69
|
+
state: :completed,
|
70
|
+
determine_signal: ->(_) { :wait }
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# app/operations/fetch_shopping_cart.rb
|
76
|
+
class FetchShoppingCart < NucleusCore::Operation
|
77
|
+
def call
|
78
|
+
cart = ShoppingCartRepository.find(context.cart_id)
|
79
|
+
|
80
|
+
context.cart = cart
|
81
|
+
rescue NucleusCore::NotFound => e
|
82
|
+
context.fail!(e.message, exception: e)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# app/repositories/shopping_cart_repository.rb
|
87
|
+
class ShoppingCartRepository < NucleusCore::Repository
|
88
|
+
def self.find(cart_id)
|
89
|
+
cart = ShoppingCart.find(cart_id)
|
90
|
+
|
91
|
+
return ShoppingCart::Aggregate.new(cart)
|
92
|
+
rescue ActiveRecord::RecordNotFound => e
|
93
|
+
raise NucleusCore::NotFound, e.message
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.discount(cart_id, percentage)
|
97
|
+
cart = find(cart_id, percentage=0.5)
|
98
|
+
|
99
|
+
cart.update!(total: cart.total * percentage, paid: true)
|
100
|
+
|
101
|
+
return ShoppingCart::Aggregate.new(cart)
|
102
|
+
rescue NucleusCore::NotFound => e
|
103
|
+
raise e
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class ShoppingCart < ActiveRecord::Base
|
108
|
+
# ...
|
109
|
+
end
|
110
|
+
|
111
|
+
# app/aggregates/shopping_cart.rb
|
112
|
+
class ShoppingCart::Aggregate < NucleusCore::Aggregate
|
113
|
+
def initialize(cart)
|
114
|
+
super(id: cart.id, total: cart.total, paid: cart.paid, created_at: cart.created_at)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# app/operations/apply_discount_to_shopping_cart.rb
|
119
|
+
class ApplyDiscountToShoppingCart < NucleusCore::Operation
|
120
|
+
def call
|
121
|
+
cart = ShoppingCartRepository.discount(context.cart_id, 0.75)
|
122
|
+
|
123
|
+
context.cart
|
124
|
+
rescue NucleusCore::NotFound => e
|
125
|
+
context.fail!(e.message, exception: e)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# app/views/payments_view.rb
|
130
|
+
class NucleusCore::PaymentView < NucleusCore::View
|
131
|
+
def initialize(cart)
|
132
|
+
super(total: "$#{cart.total}", paid: cart.paid, created_at: cart.created_at)
|
133
|
+
end
|
134
|
+
|
135
|
+
def json_response
|
136
|
+
content = {
|
137
|
+
payment: {
|
138
|
+
price: price,
|
139
|
+
paid: paid,
|
140
|
+
created_at: created_at,
|
141
|
+
signature: SecureRandom.hex
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
NucleusCore::JsonResponse.new(content: content)
|
146
|
+
end
|
147
|
+
|
148
|
+
def pdf_response
|
149
|
+
pdf_string = generate_pdf_string(price, paid)
|
150
|
+
|
151
|
+
NucleusCore::PdfResponse.new(content: pdf_string)
|
152
|
+
end
|
153
|
+
|
154
|
+
private def generate_pdf_string(price, paid)
|
155
|
+
# pdf string genration...
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
---
|
161
|
+
|
162
|
+
- [Quick start](#quick-start)
|
163
|
+
- [Support](#support)
|
164
|
+
- [License](#license)
|
165
|
+
- [Code of conduct](#code-of-conduct)
|
166
|
+
- [Contribution guide](#contribution-guide)
|
167
|
+
|
168
|
+
## Quick start
|
169
|
+
|
170
|
+
```
|
171
|
+
$ gem install nucleus-core
|
172
|
+
```
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
require "nucleus-core"
|
176
|
+
```
|
177
|
+
|
178
|
+
## Support
|
179
|
+
|
180
|
+
If you want to report a bug, or have ideas, feedback or questions about the gem, [let me know via GitHub issues](https://github.com/dodgerogers/nucleus_core/issues/new) and I will do my best to provide a helpful answer. Happy hacking!
|
181
|
+
|
182
|
+
## License
|
183
|
+
|
184
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
185
|
+
|
186
|
+
## Code of conduct
|
187
|
+
|
188
|
+
Everyone interacting in this project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
189
|
+
|
190
|
+
## Contribution guide
|
191
|
+
|
192
|
+
Pull requests are welcome!
|
data/exe/nucleus
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
class NucleusCore::Aggregate < NucleusCore::BasicObject; end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module NucleusCore
|
2
|
+
class BasicObject
|
3
|
+
def initialize(attrs={})
|
4
|
+
attrs.each_pair do |key, value|
|
5
|
+
define_singleton_method(key.to_s) do
|
6
|
+
instance_variable_get("@#{key}")
|
7
|
+
end
|
8
|
+
|
9
|
+
define_singleton_method("#{key}=") do |val|
|
10
|
+
instance_variable_set("@#{key}", val)
|
11
|
+
end
|
12
|
+
|
13
|
+
instance_variable_set("@#{key}", value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_h
|
18
|
+
instance_variables
|
19
|
+
.reduce({}) do |acc, var|
|
20
|
+
acc.merge(var.to_s.delete("@") => instance_variable_get(var))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
class NucleusCore::BaseException < StandardError; end
|
2
|
+
class NucleusCore::NotAuthorized < NucleusCore::BaseException; end
|
3
|
+
class NucleusCore::NotFound < NucleusCore::BaseException; end
|
4
|
+
class NucleusCore::Unprocessable < NucleusCore::BaseException; end
|
5
|
+
class NucleusCore::BadRequest < NucleusCore::BaseException; end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Rack::Utils patch for status code
|
2
|
+
module NucleusCore
|
3
|
+
module Rack
|
4
|
+
class Utils
|
5
|
+
HTTP_STATUS_CODES = {
|
6
|
+
100 => "Continue",
|
7
|
+
101 => "Switching Protocols",
|
8
|
+
102 => "Processing",
|
9
|
+
103 => "Early Hints",
|
10
|
+
200 => "OK",
|
11
|
+
201 => "Created",
|
12
|
+
202 => "Accepted",
|
13
|
+
203 => "Non-Authoritative Information",
|
14
|
+
204 => "No Content",
|
15
|
+
205 => "Reset Content",
|
16
|
+
206 => "Partial Content",
|
17
|
+
207 => "Multi-Status",
|
18
|
+
208 => "Already Reported",
|
19
|
+
226 => "IM Used",
|
20
|
+
300 => "Multiple Choices",
|
21
|
+
301 => "Moved Permanently",
|
22
|
+
302 => "Found",
|
23
|
+
303 => "See Other",
|
24
|
+
304 => "Not Modified",
|
25
|
+
305 => "Use Proxy",
|
26
|
+
306 => "(Unused)",
|
27
|
+
307 => "Temporary Redirect",
|
28
|
+
308 => "Permanent Redirect",
|
29
|
+
400 => "Bad Request",
|
30
|
+
401 => "Unauthorized",
|
31
|
+
402 => "Payment Required",
|
32
|
+
403 => "Forbidden",
|
33
|
+
404 => "Not Found",
|
34
|
+
405 => "Method Not Allowed",
|
35
|
+
406 => "Not Acceptable",
|
36
|
+
407 => "Proxy Authentication Required",
|
37
|
+
408 => "Request Timeout",
|
38
|
+
409 => "Conflict",
|
39
|
+
410 => "Gone",
|
40
|
+
411 => "Length Required",
|
41
|
+
412 => "Precondition Failed",
|
42
|
+
413 => "Payload Too Large",
|
43
|
+
414 => "URI Too Long",
|
44
|
+
415 => "Unsupported Media Type",
|
45
|
+
416 => "Range Not Satisfiable",
|
46
|
+
417 => "Expectation Failed",
|
47
|
+
421 => "Misdirected Request",
|
48
|
+
422 => "Unprocessable Entity",
|
49
|
+
423 => "Locked",
|
50
|
+
424 => "Failed Dependency",
|
51
|
+
425 => "Too Early",
|
52
|
+
426 => "Upgrade Required",
|
53
|
+
428 => "Precondition Required",
|
54
|
+
429 => "Too Many Requests",
|
55
|
+
431 => "Request Header Fields Too Large",
|
56
|
+
451 => "Unavailable for Legal Reasons",
|
57
|
+
500 => "Internal Server Error",
|
58
|
+
501 => "Not Implemented",
|
59
|
+
502 => "Bad Gateway",
|
60
|
+
503 => "Service Unavailable",
|
61
|
+
504 => "Gateway Timeout",
|
62
|
+
505 => "HTTP Version Not Supported",
|
63
|
+
506 => "Variant Also Negotiates",
|
64
|
+
507 => "Insufficient Storage",
|
65
|
+
508 => "Loop Detected",
|
66
|
+
509 => "Bandwidth Limit Exceeded",
|
67
|
+
510 => "Not Extended",
|
68
|
+
511 => "Network Authentication Required"
|
69
|
+
}.freeze
|
70
|
+
|
71
|
+
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map do |code, message|
|
72
|
+
[message.downcase.gsub(/\s|-|'/, "_").to_sym, code]
|
73
|
+
end.flatten]
|
74
|
+
|
75
|
+
def self.status_code(status)
|
76
|
+
if status.is_a?(Symbol)
|
77
|
+
return SYMBOL_TO_STATUS_CODE.fetch(status) do
|
78
|
+
raise ArgumentError, "Unrecognized status code #{status.inspect}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
status.to_i
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
|
3
|
+
module NucleusCore
|
4
|
+
class Operation
|
5
|
+
class Context < OpenStruct
|
6
|
+
class Error < StandardError
|
7
|
+
attr_reader :exception
|
8
|
+
|
9
|
+
def initialize(message, opts={})
|
10
|
+
@exception = opts[:exception]
|
11
|
+
|
12
|
+
super(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :failure
|
17
|
+
|
18
|
+
def initialize(attrs={})
|
19
|
+
@failure = false
|
20
|
+
|
21
|
+
super(attrs)
|
22
|
+
end
|
23
|
+
|
24
|
+
def success?
|
25
|
+
!@failure
|
26
|
+
end
|
27
|
+
|
28
|
+
def fail!(message, attrs={})
|
29
|
+
@failure = true
|
30
|
+
|
31
|
+
self.message = message
|
32
|
+
self.exception = attrs.delete(:exception)
|
33
|
+
|
34
|
+
raise Context::Error, message
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :context
|
39
|
+
|
40
|
+
def initialize(args={})
|
41
|
+
@context = args.is_a?(Context) ? args : Context.new(args)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.call(args={})
|
45
|
+
operation = new(args)
|
46
|
+
|
47
|
+
operation.call
|
48
|
+
|
49
|
+
operation.context
|
50
|
+
rescue Context::Error
|
51
|
+
operation.context
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.rollback(context)
|
55
|
+
operation = new(context)
|
56
|
+
|
57
|
+
operation.rollback
|
58
|
+
|
59
|
+
operation.context
|
60
|
+
end
|
61
|
+
|
62
|
+
def call
|
63
|
+
end
|
64
|
+
|
65
|
+
def rollback
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "nucleus_core/exceptions"
|
2
|
+
|
3
|
+
module NucleusCore
|
4
|
+
class Policy
|
5
|
+
attr_reader :user, :record
|
6
|
+
|
7
|
+
def initialize(user, record=nil)
|
8
|
+
@user = user
|
9
|
+
@record = record
|
10
|
+
end
|
11
|
+
|
12
|
+
def enforce!(*policy_methods)
|
13
|
+
policy_methods.each do |policy_method_and_args|
|
14
|
+
next if send(*policy_method_and_args)
|
15
|
+
|
16
|
+
name = Array.wrap(policy_method_and_args).first
|
17
|
+
message = "You do not have access to: #{name}"
|
18
|
+
|
19
|
+
raise NucleusCore::NotAuthorized, message
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module NucleusCore
|
4
|
+
module Responder
|
5
|
+
def set_request_format(request=nil)
|
6
|
+
@request_format = request&.format&.to_sym || :json
|
7
|
+
end
|
8
|
+
|
9
|
+
def request_format
|
10
|
+
@request_format ||= set_request_format
|
11
|
+
end
|
12
|
+
|
13
|
+
# rubocop:disable Lint/RescueException:
|
14
|
+
def handle_response(&block)
|
15
|
+
entity = proc_to_lambda(&block)
|
16
|
+
|
17
|
+
render_entity(entity)
|
18
|
+
rescue Exception => e
|
19
|
+
handle_exception(e)
|
20
|
+
end
|
21
|
+
# rubocop:enable Lint/RescueException:
|
22
|
+
|
23
|
+
def handle_exception(exception)
|
24
|
+
logger(exception)
|
25
|
+
|
26
|
+
status = exception_to_status(exception)
|
27
|
+
attrs = { message: exception.message, status: status }
|
28
|
+
error = NucleusCore::ErrorView.new(attrs)
|
29
|
+
|
30
|
+
render_entity(error)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Calling `return` in a block/proc returns from the outer calling scope as well.
|
34
|
+
# Lambdas do not have this limitation. So we convert the proc returned
|
35
|
+
# from a block method into a lambda to avoid 'return' exiting the method early.
|
36
|
+
# https://stackoverflow.com/questions/2946603/ruby-convert-proc-to-lambda
|
37
|
+
def proc_to_lambda(&block)
|
38
|
+
define_singleton_method(:_proc_to_lambda_, &block)
|
39
|
+
|
40
|
+
method(:_proc_to_lambda_).to_proc.call
|
41
|
+
end
|
42
|
+
|
43
|
+
def render_entity(entity)
|
44
|
+
return handle_context(entity) if entity.is_a?(NucleusCore::Operation::Context)
|
45
|
+
return render_response(entity) if subclass_of(entity, NucleusCore::ResponseAdapter)
|
46
|
+
return render_view(entity) if subclass_of(entity, NucleusCore::View)
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_context(context)
|
50
|
+
return render_nothing(context) if context.success?
|
51
|
+
return handle_exception(context.exception) if context.exception
|
52
|
+
|
53
|
+
message = context.message
|
54
|
+
attrs = { message: message, status: :unprocessable_entity }
|
55
|
+
error_view = NucleusCore::ErrorView.new(attrs)
|
56
|
+
|
57
|
+
render_view(error_view)
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_view(view)
|
61
|
+
format_rendering = "#{request_format}_response".to_sym
|
62
|
+
renders_format = view.respond_to?(format_rendering)
|
63
|
+
format_response = view.send(format_rendering) if renders_format
|
64
|
+
|
65
|
+
raise NucleusCore::BadRequest, "#{request_format} is not supported" if format_response.nil?
|
66
|
+
|
67
|
+
render_response(format_response)
|
68
|
+
end
|
69
|
+
|
70
|
+
def render_response(entity)
|
71
|
+
render_headers(entity.headers)
|
72
|
+
|
73
|
+
render_method = {
|
74
|
+
NucleusCore::JsonResponse => :render_json,
|
75
|
+
NucleusCore::XmlResponse => :render_xml,
|
76
|
+
NucleusCore::PdfResponse => :render_pdf,
|
77
|
+
NucleusCore::CsvResponse => :render_csv,
|
78
|
+
NucleusCore::TextResponse => :render_text,
|
79
|
+
NucleusCore::NoResponse => :render_nothing
|
80
|
+
}.fetch(entity.class, nil)
|
81
|
+
|
82
|
+
response_adapter&.send(render_method, entity)
|
83
|
+
end
|
84
|
+
|
85
|
+
def render_headers(headers={})
|
86
|
+
(headers || {}).each do |k, v|
|
87
|
+
formatted_key = k.titleize.gsub(/\s *|_/, "-")
|
88
|
+
|
89
|
+
response_adapter&.set_header(formatted_key, v)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# rubocop:disable Lint/DuplicateBranch
|
94
|
+
def exception_to_status(exception)
|
95
|
+
config = exception_map
|
96
|
+
|
97
|
+
case exception
|
98
|
+
when NucleusCore::NotFound, *config.not_found
|
99
|
+
:not_found
|
100
|
+
when NucleusCore::BadRequest, *config.bad_request
|
101
|
+
:bad_request
|
102
|
+
when NucleusCore::NotAuthorized, *config.forbidden
|
103
|
+
:forbidden
|
104
|
+
when NucleusCore::Unprocessable, *config.unprocessable
|
105
|
+
:unprocessable_entity
|
106
|
+
when NucleusCore::BaseException, *config.server_error
|
107
|
+
:internal_server_error
|
108
|
+
else
|
109
|
+
:internal_server_error
|
110
|
+
end
|
111
|
+
end
|
112
|
+
# rubocop:enable Lint/DuplicateBranch
|
113
|
+
|
114
|
+
def subclass_of(entity, *classes)
|
115
|
+
Set[*entity.class.ancestors].intersect?(classes.to_set)
|
116
|
+
end
|
117
|
+
|
118
|
+
def logger(object, log_level=:info)
|
119
|
+
NucleusCore.configuration.logger&.send(log_level, object)
|
120
|
+
end
|
121
|
+
|
122
|
+
def exception_map
|
123
|
+
NucleusCore.configuration.exceptions_map
|
124
|
+
end
|
125
|
+
|
126
|
+
def response_adapter
|
127
|
+
NucleusCore.configuration.response_adapter
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class NucleusCore::ResponseAdapter < NucleusCore::BasicObject
|
2
|
+
def initialize(attrs={})
|
3
|
+
attributes = defaults
|
4
|
+
.merge(attrs)
|
5
|
+
.slice(*defaults.keys)
|
6
|
+
.tap do |hash|
|
7
|
+
hash[:status] = status_code(hash[:status])
|
8
|
+
end
|
9
|
+
|
10
|
+
super(attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def defaults
|
16
|
+
{ content: "", headers: {}, status: 200, location: nil }
|
17
|
+
end
|
18
|
+
|
19
|
+
def status_code(status=nil)
|
20
|
+
status = NucleusCore::Rack::Utils.status_code(status)
|
21
|
+
default_status = 200
|
22
|
+
|
23
|
+
status.zero? ? default_status : status
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class NucleusCore::NoResponse < NucleusCore::ResponseAdapter
|
28
|
+
def initialize(attrs={})
|
29
|
+
attrs = attrs.merge(content: nil, type: "text/html; charset=utf-8")
|
30
|
+
|
31
|
+
super(attrs)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class NucleusCore::TextResponse < NucleusCore::ResponseAdapter
|
36
|
+
def initialize(attrs={})
|
37
|
+
attrs = attrs.merge(type: "application/text")
|
38
|
+
|
39
|
+
super(attrs)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class NucleusCore::JsonResponse < NucleusCore::ResponseAdapter
|
44
|
+
def initialize(attrs={})
|
45
|
+
attrs = attrs.merge(type: "application/json")
|
46
|
+
|
47
|
+
super(attrs)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class NucleusCore::XmlResponse < NucleusCore::ResponseAdapter
|
52
|
+
def initialize(attrs={})
|
53
|
+
attrs = attrs.merge(type: "application/xml")
|
54
|
+
|
55
|
+
super(attrs)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class NucleusCore::CsvResponse < NucleusCore::ResponseAdapter
|
60
|
+
def initialize(attrs={})
|
61
|
+
attrs = attrs.merge(
|
62
|
+
disposition: "attachment",
|
63
|
+
filename: attrs.fetch(:filename) { "response.csv" },
|
64
|
+
type: "text/csv; charset=UTF-8;"
|
65
|
+
)
|
66
|
+
|
67
|
+
super(attrs)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class NucleusCore::PdfResponse < NucleusCore::ResponseAdapter
|
72
|
+
def initialize(attrs={})
|
73
|
+
attrs = attrs.merge(
|
74
|
+
disposition: "inline",
|
75
|
+
filename: attrs.fetch(:filename) { "response.pdf" },
|
76
|
+
type: "application/pdf"
|
77
|
+
)
|
78
|
+
|
79
|
+
super(attrs)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "nucleus_core/response_adapter"
|
2
|
+
|
3
|
+
class NucleusCore::ErrorView < NucleusCore::View
|
4
|
+
def initialize(attrs={})
|
5
|
+
super(
|
6
|
+
{}.tap do |a|
|
7
|
+
a[:status] = attrs.fetch(:status, :unprocessable_entity)
|
8
|
+
a[:message] = attrs.fetch(:message, nil)
|
9
|
+
a[:errors] = attrs.fetch(:errors, [])
|
10
|
+
end
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def json_response
|
15
|
+
NucleusCore::JsonResponse.new(content: to_h, status: status)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ClassLength, Metrics/AbcSize:
|
2
|
+
module NucleusCore
|
3
|
+
class Workflow
|
4
|
+
class Node
|
5
|
+
attr_reader :state, :operation, :rollback, :signals, :prepare_context, :determine_signal
|
6
|
+
|
7
|
+
def initialize(attrs={})
|
8
|
+
@state = attrs[:state]
|
9
|
+
@operation = attrs[:operation]
|
10
|
+
@rollback = attrs[:rollback]
|
11
|
+
@signals = attrs[:signals]
|
12
|
+
@prepare_context = attrs[:prepare_context]
|
13
|
+
@determine_signal = attrs[:determine_signal]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Process
|
18
|
+
attr_accessor :state, :visited
|
19
|
+
|
20
|
+
def initialize(state)
|
21
|
+
@state = state
|
22
|
+
@visited = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def visit(state)
|
26
|
+
@state = state
|
27
|
+
@visited.push(state)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# States
|
32
|
+
###########################################################################
|
33
|
+
INITIAL_STATE = :initial
|
34
|
+
|
35
|
+
# Signals
|
36
|
+
###########################################################################
|
37
|
+
CONTINUE = :continue
|
38
|
+
WAIT = :wait
|
39
|
+
|
40
|
+
# Node statuses
|
41
|
+
###########################################################################
|
42
|
+
OK = :ok
|
43
|
+
FAILED = :failed
|
44
|
+
|
45
|
+
attr_accessor :process, :nodes, :context
|
46
|
+
|
47
|
+
def initialize(process: nil, context: {})
|
48
|
+
@nodes = {}
|
49
|
+
@process = process || Process.new(INITIAL_STATE)
|
50
|
+
@context = build_context(context)
|
51
|
+
|
52
|
+
init_nodes
|
53
|
+
end
|
54
|
+
|
55
|
+
def register_node(attrs={})
|
56
|
+
node = Node.new(attrs)
|
57
|
+
|
58
|
+
@nodes[node.state] = node
|
59
|
+
end
|
60
|
+
|
61
|
+
def start_node(signals={})
|
62
|
+
raise ArgumentError, "#{self.class}##{__method__}: missing signals" if signals.empty?
|
63
|
+
|
64
|
+
register_node(state: INITIAL_STATE, signals: signals)
|
65
|
+
end
|
66
|
+
|
67
|
+
def init_nodes
|
68
|
+
define
|
69
|
+
end
|
70
|
+
|
71
|
+
# Override this method to draw the workflow graph
|
72
|
+
def define
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.call(signal: nil, process: nil, context: {})
|
76
|
+
workflow = new(process: process, context: context)
|
77
|
+
|
78
|
+
workflow.validate_nodes!
|
79
|
+
workflow.execute(signal)
|
80
|
+
|
81
|
+
[workflow.context, workflow.process]
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.rollback(process:, context:)
|
85
|
+
workflow = new(process: process, context: context)
|
86
|
+
visited = workflow.process.visited.clone
|
87
|
+
|
88
|
+
visited.reverse_each do |state|
|
89
|
+
node = workflow.nodes[state]
|
90
|
+
|
91
|
+
next node.operation.rollback(context) if node.operation.is_a?(NucleusCore::Operation)
|
92
|
+
next node.rollback.call(context) if node.rollback.is_a?(Proc)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def validate_nodes!
|
97
|
+
start_nodes = nodes.values.count do |node|
|
98
|
+
node.state == INITIAL_STATE
|
99
|
+
end
|
100
|
+
|
101
|
+
raise ArgumentError, "#{self.class}: missing `:initial` start node" if start_nodes.zero?
|
102
|
+
raise ArgumentError, "#{self.class}: more than one start node detected" if start_nodes > 1
|
103
|
+
end
|
104
|
+
|
105
|
+
def execute(signal=nil)
|
106
|
+
signal ||= CONTINUE
|
107
|
+
current_state = process.state
|
108
|
+
next_signal = (fetch_node(current_state)&.signals || {})[signal]
|
109
|
+
current_node = fetch_node(next_signal)
|
110
|
+
|
111
|
+
context.fail!("invalid signal: #{signal}") if current_node.nil?
|
112
|
+
|
113
|
+
while next_signal
|
114
|
+
status, next_signal, @context = execute_node(current_node, context)
|
115
|
+
|
116
|
+
break if status == FAILED
|
117
|
+
|
118
|
+
process.visit(current_node.state)
|
119
|
+
current_node = fetch_node(next_signal)
|
120
|
+
|
121
|
+
break if next_signal == WAIT
|
122
|
+
end
|
123
|
+
|
124
|
+
context
|
125
|
+
rescue NucleusCore::Operation::Context::Error
|
126
|
+
context
|
127
|
+
rescue StandardError => e
|
128
|
+
fail_context(@context, e)
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def build_context(context={})
|
134
|
+
return context if context.is_a?(NucleusCore::Operation::Context)
|
135
|
+
|
136
|
+
NucleusCore::Operation::Context.new(context)
|
137
|
+
end
|
138
|
+
|
139
|
+
def execute_node(node, context)
|
140
|
+
context = prepare_context(node, context)
|
141
|
+
operation = node.operation
|
142
|
+
|
143
|
+
operation&.call(context)
|
144
|
+
|
145
|
+
status = context.success? ? OK : FAILED
|
146
|
+
next_signal = determine_signal(node, context)
|
147
|
+
|
148
|
+
[status, next_signal, context]
|
149
|
+
end
|
150
|
+
|
151
|
+
def prepare_context(node, context)
|
152
|
+
return node.prepare_context.call(context) if node.prepare_context.is_a?(Proc)
|
153
|
+
return send(node.prepare_context, context) if node.prepare_context.is_a?(Symbol)
|
154
|
+
|
155
|
+
context
|
156
|
+
end
|
157
|
+
|
158
|
+
def determine_signal(node, context)
|
159
|
+
signal = CONTINUE
|
160
|
+
signal = node.determine_signal.call(context) if node.determine_signal.is_a?(Proc)
|
161
|
+
signal = send(node.determine_signal, context) if node.determine_signal.is_a?(Symbol)
|
162
|
+
node_signals = node.signals || {}
|
163
|
+
|
164
|
+
node_signals[signal]
|
165
|
+
end
|
166
|
+
|
167
|
+
def fetch_node(state)
|
168
|
+
nodes[state]
|
169
|
+
end
|
170
|
+
|
171
|
+
def fail_context(context, exception)
|
172
|
+
message = "Unhandled exception #{self.class}: #{exception.message}"
|
173
|
+
|
174
|
+
context.fail!(message, exception: exception)
|
175
|
+
rescue NucleusCore::Operation::Context::Error
|
176
|
+
context
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ClassLength, Metrics/AbcSize:
|
data/lib/nucleus_core.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
require "nucleus_core/exceptions"
|
3
|
+
require "json"
|
4
|
+
require "set"
|
5
|
+
|
6
|
+
Dir[File.join(__dir__, "nucleus_core", "extensions", "*.rb")].sort.each { |file| require file }
|
7
|
+
|
8
|
+
module NucleusCore
|
9
|
+
autoload :CLI, "nucleus_core/cli"
|
10
|
+
autoload :VERSION, "nucleus_core/version"
|
11
|
+
autoload :BasicObject, "nucleus_core/basic_object"
|
12
|
+
autoload :View, "nucleus_core/views/view"
|
13
|
+
autoload :ErrorView, "nucleus_core/views/error_view"
|
14
|
+
autoload :ResponseAdapter, "nucleus_core/response_adapter"
|
15
|
+
autoload :Aggregate, "nucleus_core/aggregate"
|
16
|
+
autoload :Policy, "nucleus_core/policy"
|
17
|
+
autoload :Operation, "nucleus_core/operation"
|
18
|
+
autoload :Workflow, "nucleus_core/workflow"
|
19
|
+
autoload :Responder, "nucleus_core/responder"
|
20
|
+
|
21
|
+
class Configuration
|
22
|
+
attr_reader :exceptions_map, :response_adapter
|
23
|
+
attr_accessor :logger
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@logger = nil
|
27
|
+
@response_adapter = nil
|
28
|
+
@exceptions_map = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def exceptions_map=(args={})
|
32
|
+
@exceptions_map = format_exceptions(args)
|
33
|
+
end
|
34
|
+
|
35
|
+
def response_adapter=(adapter)
|
36
|
+
@response_adapter = format_adapter(adapter)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def objectify(hash)
|
42
|
+
OpenStruct.new(hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
ERROR_STATUSES = %i[not_found bad_request unauthorized unprocessable server_error].freeze
|
46
|
+
|
47
|
+
def format_exceptions(exceptions={})
|
48
|
+
exception_defaults = ERROR_STATUSES.reduce({}) { |acc, ex| acc.merge(ex => nil) }
|
49
|
+
|
50
|
+
objectify(
|
51
|
+
(exceptions || exception_defaults)
|
52
|
+
.slice(*exception_defaults.keys)
|
53
|
+
.reduce({}) do |acc, (key, value)|
|
54
|
+
acc.merge(key => Array.wrap(value))
|
55
|
+
end
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_adapter(adapter={})
|
60
|
+
adapter.tap do |a|
|
61
|
+
verify_adapter!(a)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
ADAPTER_METHODS = %i[
|
66
|
+
render_json render_xml render_text render_pdf render_csv render_nothing set_header
|
67
|
+
].freeze
|
68
|
+
|
69
|
+
def verify_adapter!(adapter)
|
70
|
+
current_adapter_methods = Set[*(adapter.methods - Object.methods)]
|
71
|
+
required_adapter_methods = ADAPTER_METHODS.to_set
|
72
|
+
|
73
|
+
return if current_adapter_methods == required_adapter_methods
|
74
|
+
|
75
|
+
missing = current_adapter_methods.subtract(required_adapter_methods)
|
76
|
+
|
77
|
+
raise ArgumentError, "responder.adapter must implement: #{missing}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class << self
|
82
|
+
def configuration
|
83
|
+
@configuration ||= Configuration.new
|
84
|
+
end
|
85
|
+
|
86
|
+
def reset
|
87
|
+
@configuration = Configuration.new
|
88
|
+
end
|
89
|
+
|
90
|
+
def configure
|
91
|
+
yield(configuration)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
metadata
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nucleus-core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- dodgerogers
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-02-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest-ci
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.4'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest-reporters
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '13.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '13.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.43.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.43.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.26.1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.26.1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-packaging
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.5.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.5.2
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-performance
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.15.2
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.15.2
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop-rake
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - '='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.6.0
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - '='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.6.0
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- dodgerogers@hotmail.com
|
142
|
+
executables:
|
143
|
+
- nucleus
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- LICENSE.txt
|
148
|
+
- README.md
|
149
|
+
- exe/nucleus
|
150
|
+
- lib/nucleus_core.rb
|
151
|
+
- lib/nucleus_core/aggregate.rb
|
152
|
+
- lib/nucleus_core/basic_object.rb
|
153
|
+
- lib/nucleus_core/cli.rb
|
154
|
+
- lib/nucleus_core/exceptions.rb
|
155
|
+
- lib/nucleus_core/extensions/array.rb
|
156
|
+
- lib/nucleus_core/extensions/rack.rb
|
157
|
+
- lib/nucleus_core/operation.rb
|
158
|
+
- lib/nucleus_core/policy.rb
|
159
|
+
- lib/nucleus_core/repository.rb
|
160
|
+
- lib/nucleus_core/responder.rb
|
161
|
+
- lib/nucleus_core/response_adapter.rb
|
162
|
+
- lib/nucleus_core/version.rb
|
163
|
+
- lib/nucleus_core/views/error_view.rb
|
164
|
+
- lib/nucleus_core/views/view.rb
|
165
|
+
- lib/nucleus_core/workflow.rb
|
166
|
+
homepage: https://github.com/dodgerogers/nucleus-core
|
167
|
+
licenses:
|
168
|
+
- MIT
|
169
|
+
metadata:
|
170
|
+
bug_tracker_uri: https://github.com/dodgerogers/nucleus-core/issues
|
171
|
+
changelog_uri: https://github.com/dodgerogers/nucleus-core/releases
|
172
|
+
source_code_uri: https://github.com/dodgerogers/nucleus-core
|
173
|
+
homepage_uri: https://github.com/dodgerogers/nucleus-core
|
174
|
+
rubygems_mfa_required: 'true'
|
175
|
+
post_install_message:
|
176
|
+
rdoc_options: []
|
177
|
+
require_paths:
|
178
|
+
- lib
|
179
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '2.6'
|
184
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
185
|
+
requirements:
|
186
|
+
- - ">="
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: '0'
|
189
|
+
requirements: []
|
190
|
+
rubygems_version: 3.4.5
|
191
|
+
signing_key:
|
192
|
+
specification_version: 4
|
193
|
+
summary: A Ruby business logic framework
|
194
|
+
test_files: []
|