nucleus-core 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/nucleus-core.svg)](https://rubygems.org/gems/nucleus-core)
|
4
|
+
[![Circle](https://circleci.com/gh/dodgerogers/nucleus-core/tree/main.svg?style=shield)](https://app.circleci.com/pipelines/github/dodgerogers/nucleus-core?branch=main)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/dodgerogers/nucleus-core/badges/gpa.svg)](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: []
|