nucleus-core 0.1.4 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cad40c45a637495932d264dfef04ca978e3f47dba177e8acbfbd59a37e0c2217
4
- data.tar.gz: 3389a5a8e8bb3d63c4afb7fb47ae900367295d86c7bb4ccf651773b1805ca9b2
3
+ metadata.gz: 05f1cb3a1cdbe91b800911bb9f62b22496be36e29d620847f955ea52e823b166
4
+ data.tar.gz: 95d2d4eeab4797441ddff2473f29a61c37b49adbec2168599cd67e3ef3803502
5
5
  SHA512:
6
- metadata.gz: 461315b12ca674353043961e25e49498878392b89d4aef410aa1e0d16e43938087e4eb377840dd4b815d4a176e3f8f09c296be096198dcd39022b647cee4b89a
7
- data.tar.gz: 6cbd15c2ce8e2d264ecdd849e1f723b60519c132e2b422f9fc9857f0fc9828649b712bed93bf90680d1a422e87d25e6277fb801dd457436e39667665f390b3c4
6
+ metadata.gz: 5d8948f0e23f0724b396d3888a06cd515b36427b540e706330de247a12d713407d1b053bad5c49fe85bed7cbe49756cec28c20f62bd51273a39c25f30d4535ff
7
+ data.tar.gz: 8e6ea5ea9ca756d24c73c11186651687bb5eea6acc817f13c5c44e2b7a023684a290972ca71b209c3ad19ef2dcc49ad5bb5bb8333ba2cfe6251758cc4e956c2c
data/README.md CHANGED
@@ -1,143 +1,170 @@
1
- # NucleusCore
1
+ # Nucleus Core
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/nucleus-core.svg)](https://rubygems.org/gems/nucleus-core)
4
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
5
  [![Code Climate](https://codeclimate.com/github/dodgerogers/nucleus-core/badges/gpa.svg)](https://codeclimate.com/github/dodgerogers/nucleus-core)
6
6
 
7
- NucleusCore Core is a framework to express and orchestrate business logic in a way that is agnostic to the framework.
7
+ - [Overview](#overview)
8
+ - [Components](#components)
9
+ - [Supported Frameworks](#supported-frameworks)
10
+ - [Quick start](#quick-start)
11
+ - [Best practices](#best-practices)
12
+ - [Support](#support)
13
+ - [License](#license)
14
+ - [Code of conduct](#code-of-conduct)
15
+ - [Contribution guide](#contribution-guide)
16
+
17
+ ## Overview
18
+
19
+ Nucleus Core defines a boundary between your business logic, and a framework.
8
20
 
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.
21
+ ## Components
10
22
 
11
- Here are the classes NucleusCore exposes, they have preordained responsibilities, can be composed together, and tested simply in isolation from the framework.
23
+ **Responder** - The boundary which passes request parameters to your business logic, then renders a response (requires framework request, and response adapaters).\
24
+ **Operations** - Service implementation that executes one side effect.\
25
+ **Workflows** - Service orchestration which composes complex, multi stage processes.\
26
+ **Views** - Presentation objects which render multiple formats.
12
27
 
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.
28
+ ## Supported Frameworks
19
29
 
20
- Below is an example using NucleusCore Core with Rails:
30
+ These packages implement request, and response adapters for their respective framework.
31
+
32
+ - [nucleus-rails](https://rubygems.org/gems/nucleus-rails).
33
+
34
+ ## Getting started
35
+
36
+ 1. Install the gem
37
+
38
+ ```
39
+ $ gem install nuclueus-core
40
+ ```
41
+
42
+ 2. Initialize, and configure `NucleusCore`
21
43
 
22
44
  ```ruby
23
- # controllers/payments_controller.rb
24
- class PaymentsController < ApplicationController
25
- include NucleusCore::Responder
45
+ require "nucleus-core"
26
46
 
27
- def create
28
- handle_response do
29
- policy.enforce!(:can_write?)
47
+ NucleusCore.configure do |config|
48
+ config.logger = Logger.new($stdout)
49
+ config.default_response_format = :json
50
+ config.exceptions = {
51
+ not_found: ActiveRecord::RecordNotFound,
52
+ unprocessible: [ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved],
53
+ bad_request: Apipie::ParamError,
54
+ unauthorized: Pundit::NotAuthorizedError
55
+ }
56
+ end
57
+ ```
30
58
 
31
- context, _process = HandleCheckoutWorkflow.call(invoice_params)
59
+ 3. Create a class that implements the rendering methods below. Class, or instance methods can be used, make sure to initialize the responder accordingly.
32
60
 
33
- return context if !context.success?
61
+ ```ruby
62
+ class ResponderAdapter
63
+ # entity: Nucleus::ResponseAdapter
34
64
 
35
- return PaymentView.new(cart: context.cart, paid: context.paid)
36
- end
65
+ def render_json(entity)
37
66
  end
38
67
 
39
- private
40
-
41
- def policy
42
- Policy.new(current_user)
68
+ def render_xml(entity)
43
69
  end
44
70
 
45
- def invoice_params
46
- params.slice(:cart_id)
71
+ def render_pdf(entity)
47
72
  end
48
- end
49
73
 
50
- # workflows/handle_checkout_workflow.rb
51
- class HandleCheckoutWorkflow < NucleusCore::Workflow
52
- def define
53
- start_node(continue: :calculate_amount)
54
- register_node(
55
- state: :calculate_amount,
56
- operation: FetchShoppingCart,
57
- determine_signal: ->(context) { context.cart.total > 10 ? :discount : :pay },
58
- signals: { discount: :apply_discount, pay: :take_payment }
59
- )
60
- register_node(
61
- state: :apply_discount,
62
- operation: ApplyDiscountToShoppingCart,
63
- signals: { continue: :take_payment }
64
- )
65
- register_node(
66
- state: :take_payment,
67
- operation: ->(context) { context.paid = context.cart.paid },
68
- determine_signal: ->(_) { :completed }
69
- )
70
- register_node(
71
- state: :completed,
72
- determine_signal: ->(_) { :wait }
73
- )
74
+ def render_csv(entity)
74
75
  end
75
- end
76
76
 
77
- # app/operations/fetch_shopping_cart.rb
78
- class FetchShoppingCart < NucleusCore::Operation
79
- def call
80
- cart = ShoppingCartRepository.find(context.cart_id)
77
+ def render_text(entity)
78
+ end
81
79
 
82
- context.cart = cart
83
- rescue NucleusCore::NotFound => e
84
- context.fail!(e.message, exception: e)
80
+ def render_nothing(entity)
85
81
  end
86
82
  end
83
+ ```
87
84
 
88
- # app/repositories/shopping_cart_repository.rb
89
- class ShoppingCartRepository < NucleusCore::Repository
90
- def self.find(cart_id)
91
- cart = ShoppingCart.find(cart_id)
85
+ 4. Create a class that implements `call` which returns a hash of request details. `format` and `parameters` are required, but there others can be returned.
92
86
 
93
- return ShoppingCart::Aggregate.new(cart)
94
- rescue ActiveRecord::RecordNotFound => e
95
- raise NucleusCore::NotFound, e.message
87
+ ```ruby
88
+ class RequestAdapter
89
+ def call(args={})
90
+ { format: args[:format], parameters: args[:params], ...}
96
91
  end
92
+ end
93
+ ```
97
94
 
98
- def self.discount(cart_id, percentage)
99
- cart = find(cart_id, percentage=0.5)
95
+ 4. Implement your business logic using Operations, and orchestrate more complex proceedures with Workflows.
100
96
 
101
- cart.update!(total: cart.total * percentage, paid: true)
97
+ `operations/fetch_order.rb`
102
98
 
103
- return ShoppingCart::Aggregate.new(cart)
99
+ ```ruby
100
+ class Operations::FetchOrder < NucleusCore::Operation
101
+ def call
102
+ context.order = find_order(context.id)
104
103
  rescue NucleusCore::NotFound => e
105
- raise e
104
+ context.fail!(e.message, exception: e)
106
105
  end
107
106
  end
107
+ ```
108
108
 
109
- # app/models/shopping_cart.rb
110
- class ShoppingCart < ActiveRecord::Base
111
- # ...
112
- end
109
+ `operations/apply_order_discount.rb`
110
+
111
+ ```ruby
112
+ class Operations::ApplyOrderDiscount < NucleusCore::Operation
113
+ def call
114
+ discount = context.discount || 0.25
115
+ order = update_order(context.order, discount: discount)
113
116
 
114
- # app/aggregates/shopping_cart.rb
115
- class ShoppingCart::Aggregate < NucleusCore::Aggregate
116
- def initialize(cart)
117
- super(id: cart.id, total: cart.total, paid: cart.paid, created_at: cart.created_at)
117
+ context.order = order
118
+ rescue NucleusCore::NotFound, NucleusCore::Unprocessable => e
119
+ context.fail!(e.message, exception: e)
118
120
  end
119
121
  end
122
+ ```
120
123
 
121
- # app/operations/apply_discount_to_shopping_cart.rb
122
- class ApplyDiscountToShoppingCart < NucleusCore::Operation
123
- def call
124
- cart = ShoppingCartRepository.discount(context.cart_id, 0.75)
124
+ `workflows/fulfill_order.rb`
125
125
 
126
- context.cart
127
- rescue NucleusCore::NotFound => e
128
- context.fail!(e.message, exception: e)
126
+ ```ruby
127
+ class Workflows::FulfillOrder < NucleusCore::Workflow
128
+ def define
129
+ start_node(continue: :apply_discount?)
130
+ register_node(
131
+ state: :apply_discount?,
132
+ operation: Operations::FetchOrder,
133
+ determine_signal: ->(context) { context.order.total > 10 ? :discount : :pay },
134
+ signals: { discount: :discount_order, pay: :take_payment }
135
+ )
136
+ register_node(
137
+ state: :discount_order,
138
+ operation: Operations::ApplyOrderDiscount,
139
+ signals: { continue: :take_payment }
140
+ )
141
+ register_node(
142
+ state: :take_payment,
143
+ operation: ->(context) { context.paid = context.order.pay! },
144
+ signals: { continue: :completed }
145
+ )
146
+ register_node(
147
+ state: :completed,
148
+ determine_signal: ->(_) { :wait }
149
+ )
129
150
  end
130
151
  end
152
+ ```
131
153
 
132
- # app/views/payments_view.rb
133
- class NucleusCore::PaymentView < NucleusCore::View
134
- def initialize(cart)
135
- super(total: "$#{cart.total}", paid: cart.paid, created_at: cart.created_at)
154
+ 5. Define your view, and it's responses.
155
+
156
+ `views/order.rb`
157
+
158
+ ```ruby
159
+ class Views::Order < NucleusCore::View
160
+ def initialize(order)
161
+ super(id: order.id, price: "$#{order.total}", paid: order.paid, created_at: order.created_at)
136
162
  end
137
163
 
138
164
  def json_response
139
165
  content = {
140
166
  payment: {
167
+ id: id,
141
168
  price: price,
142
169
  paid: paid,
143
170
  created_at: created_at,
@@ -149,38 +176,50 @@ class NucleusCore::PaymentView < NucleusCore::View
149
176
  end
150
177
 
151
178
  def pdf_response
152
- pdf_string = generate_pdf_string(price, paid)
179
+ pdf = generate_pdf(id, price, paid)
153
180
 
154
- NucleusCore::PdfResponse.new(content: pdf_string)
155
- end
156
-
157
- private def generate_pdf_string(price, paid)
158
- # pdf string genration...
181
+ NucleusCore::PdfResponse.new(content: pdf)
159
182
  end
160
183
  end
161
184
  ```
162
185
 
163
- ---
186
+ 4. Initialize the responder with your adapters, then call your business logic, and return a view.
164
187
 
165
- - [Quick start](#quick-start)
166
- - [Support](#support)
167
- - [License](#license)
168
- - [Code of conduct](#code-of-conduct)
169
- - [Contribution guide](#contribution-guide)
188
+ `controllers/orders_controller.rb`
170
189
 
171
- ## Quick start
190
+ ```ruby
191
+ class OrdersController
192
+ before_action do
193
+ @responder = Nucleus::Responder.new(
194
+ response_adapter: ResponseAdapter.new,
195
+ request_adapter: RequestAdapter.new
196
+ )
172
197
 
173
- ```
174
- $ gem install nucleus-core
175
- ```
198
+ @request = {
199
+ format: request.format,
200
+ parameters: request.params
201
+ }
202
+ end
176
203
 
177
- ```ruby
178
- require "nucleus-core"
204
+ def create
205
+ @responder.execute(@request) do |req|
206
+ context, _process = Workflows::FulfillOrder.call(context: req.parameters)
207
+
208
+ return Views::Order.new(order: context.order) if context.success?
209
+
210
+ return context
211
+ end
212
+ end
213
+ end
179
214
  ```
180
215
 
216
+ 5. Then tell us about it!
217
+
218
+ ---
219
+
181
220
  ## Support
182
221
 
183
- 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!
222
+ 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 we will do our best to provide a helpful answer.
184
223
 
185
224
  ## License
186
225
 
@@ -0,0 +1,3 @@
1
+ require "nucleus_core/exceptions/base"
2
+
3
+ class NucleusCore::BadRequest < NucleusCore::BaseException; end
@@ -0,0 +1 @@
1
+ class NucleusCore::BaseException < StandardError; end
@@ -0,0 +1,3 @@
1
+ require "nucleus_core/exceptions/base"
2
+
3
+ class NucleusCore::NotAuthorized < NucleusCore::BaseException; end
@@ -0,0 +1,3 @@
1
+ require "nucleus_core/exceptions/base"
2
+
3
+ class NucleusCore::NotFound < NucleusCore::BaseException; end
@@ -0,0 +1,3 @@
1
+ require "nucleus_core/exceptions/base"
2
+
3
+ class NucleusCore::Unprocessable < NucleusCore::BaseException; end
@@ -0,0 +1,88 @@
1
+ # Rack::Utils patch for status code
2
+ class Utils
3
+ HTTP_STATUS_CODES = {
4
+ 100 => "Continue",
5
+ 101 => "Switching Protocols",
6
+ 102 => "Processing",
7
+ 103 => "Early Hints",
8
+ 200 => "OK",
9
+ 201 => "Created",
10
+ 202 => "Accepted",
11
+ 203 => "Non-Authoritative Information",
12
+ 204 => "No Content",
13
+ 205 => "Reset Content",
14
+ 206 => "Partial Content",
15
+ 207 => "Multi-Status",
16
+ 208 => "Already Reported",
17
+ 226 => "IM Used",
18
+ 300 => "Multiple Choices",
19
+ 301 => "Moved Permanently",
20
+ 302 => "Found",
21
+ 303 => "See Other",
22
+ 304 => "Not Modified",
23
+ 305 => "Use Proxy",
24
+ 306 => "(Unused)",
25
+ 307 => "Temporary Redirect",
26
+ 308 => "Permanent Redirect",
27
+ 400 => "Bad Request",
28
+ 401 => "Unauthorized",
29
+ 402 => "Payment Required",
30
+ 403 => "Forbidden",
31
+ 404 => "Not Found",
32
+ 405 => "Method Not Allowed",
33
+ 406 => "Not Acceptable",
34
+ 407 => "Proxy Authentication Required",
35
+ 408 => "Request Timeout",
36
+ 409 => "Conflict",
37
+ 410 => "Gone",
38
+ 411 => "Length Required",
39
+ 412 => "Precondition Failed",
40
+ 413 => "Payload Too Large",
41
+ 414 => "URI Too Long",
42
+ 415 => "Unsupported Media Type",
43
+ 416 => "Range Not Satisfiable",
44
+ 417 => "Expectation Failed",
45
+ 421 => "Misdirected Request",
46
+ 422 => "Unprocessable Entity",
47
+ 423 => "Locked",
48
+ 424 => "Failed Dependency",
49
+ 425 => "Too Early",
50
+ 426 => "Upgrade Required",
51
+ 428 => "Precondition Required",
52
+ 429 => "Too Many Requests",
53
+ 431 => "Request Header Fields Too Large",
54
+ 451 => "Unavailable for Legal Reasons",
55
+ 500 => "Internal Server Error",
56
+ 501 => "Not Implemented",
57
+ 502 => "Bad Gateway",
58
+ 503 => "Service Unavailable",
59
+ 504 => "Gateway Timeout",
60
+ 505 => "HTTP Version Not Supported",
61
+ 506 => "Variant Also Negotiates",
62
+ 507 => "Insufficient Storage",
63
+ 508 => "Loop Detected",
64
+ 509 => "Bandwidth Limit Exceeded",
65
+ 510 => "Not Extended",
66
+ 511 => "Network Authentication Required"
67
+ }.freeze
68
+
69
+ SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map do |code, message|
70
+ [message.downcase.gsub(/\s|-|'/, "_").to_sym, code]
71
+ end.flatten]
72
+
73
+ def self.status_code(status)
74
+ if status.is_a?(Symbol)
75
+ return SYMBOL_TO_STATUS_CODE.fetch(status) do
76
+ raise ArgumentError, "Unrecognized status code #{status.inspect}"
77
+ end
78
+ end
79
+
80
+ status.to_i
81
+ end
82
+
83
+ def self.wrap(object)
84
+ return [] if object.nil?
85
+
86
+ object.is_a?(Array) ? object : [object]
87
+ end
88
+ end
@@ -0,0 +1,23 @@
1
+ require "nucleus_core/simple_object"
2
+
3
+ module NucleusCore
4
+ class RequestAdapter < NucleusCore::SimpleObject
5
+ def initialize(attrs=nil)
6
+ attrs ||= {}
7
+ attributes = defaults
8
+ .merge(attrs)
9
+ .slice(*defaults.keys)
10
+
11
+ super(attributes)
12
+ end
13
+
14
+ private
15
+
16
+ def defaults
17
+ {
18
+ format: NucleusCore.configuration.default_response_format,
19
+ parameters: {}
20
+ }
21
+ end
22
+ end
23
+ end
@@ -1,27 +1,22 @@
1
1
  require "set"
2
2
 
3
3
  module NucleusCore
4
- module Responder
5
- attr_reader :request_format, :response_adapter
4
+ class Responder
5
+ attr_accessor :response_adapter, :request_adapter, :request_context
6
6
 
7
- def init_responder(request_format: nil, response_adapter: nil)
8
- set_request_format(request_format)
9
- set_response_adapter(response_adapter)
10
- end
11
-
12
- def set_request_format(request=nil)
13
- @request_format = request&.to_sym || :json
14
- end
15
-
16
- # rubocop:disable Naming/AccessorMethodName
17
- def set_response_adapter(response_adapter)
7
+ def initialize(request_adapter: nil, response_adapter: nil)
8
+ @request_adapter = request_adapter
18
9
  @response_adapter = response_adapter
10
+ @request_context = nil
19
11
  end
20
- # rubocop:enable Naming/AccessorMethodName
21
12
 
22
13
  # rubocop:disable Lint/RescueException:
23
- def handle_response(&block)
24
- entity = proc_to_lambda(&block)
14
+ def execute(raw_request_context=nil, &block)
15
+ return if block.nil?
16
+
17
+ request_context_attrs = request_adapter&.call(raw_request_context) || {}
18
+ @request_context = NucleusCore::RequestAdapter.new(request_context_attrs)
19
+ entity = execute_block(@request_context, &block)
25
20
 
26
21
  render_entity(entity)
27
22
  rescue Exception => e
@@ -29,49 +24,36 @@ module NucleusCore
29
24
  end
30
25
  # rubocop:enable Lint/RescueException:
31
26
 
32
- def handle_exception(exception)
33
- logger(exception)
34
-
35
- status = exception_to_status(exception)
36
- attrs = { message: exception.message, status: status }
37
- error = NucleusCore::ErrorView.new(attrs)
38
-
39
- render_entity(error)
40
- end
41
-
42
27
  # Calling `return` in a block/proc returns from the outer calling scope as well.
43
28
  # Lambdas do not have this limitation. So we convert the proc returned
44
29
  # from a block method into a lambda to avoid 'return' exiting the method early.
45
30
  # https://stackoverflow.com/questions/2946603/ruby-convert-proc-to-lambda
46
- def proc_to_lambda(&block)
31
+ def execute_block(request, &block)
47
32
  define_singleton_method(:_proc_to_lambda_, &block)
48
33
 
49
- method(:_proc_to_lambda_).to_proc.call
34
+ method(:_proc_to_lambda_).to_proc.call(request)
50
35
  end
51
36
 
52
37
  def render_entity(entity)
53
38
  return handle_context(entity) if entity.is_a?(NucleusCore::Operation::Context)
54
- return render_response(entity) if subclass_of(entity, NucleusCore::ResponseAdapter)
55
39
  return render_view(entity) if subclass_of(entity, NucleusCore::View)
40
+ return render_response(entity) if subclass_of(entity, NucleusCore::ResponseAdapter)
56
41
  end
57
42
 
58
43
  def handle_context(context)
59
44
  return render_nothing(context) if context.success?
60
45
  return handle_exception(context.exception) if context.exception
61
46
 
62
- message = context.message
63
- attrs = { message: message, status: :unprocessable_entity }
64
- error_view = NucleusCore::ErrorView.new(attrs)
47
+ view = NucleusCore::ErrorView.new(message: context.message, status: :unprocessable_entity)
65
48
 
66
- render_view(error_view)
49
+ render_view(view)
67
50
  end
68
51
 
69
52
  def render_view(view)
70
- format_rendering = "#{request_format}_response".to_sym
71
- renders_format = view.respond_to?(format_rendering)
72
- format_response = view.send(format_rendering) if renders_format
53
+ render_to_format = "#{request_context.format}_response".to_sym
54
+ format_response = view.send(render_to_format) if view.respond_to?(render_to_format)
73
55
 
74
- raise NucleusCore::BadRequest, "#{request_format} is not supported" if format_response.nil?
56
+ raise NucleusCore::BadRequest, "`#{request_context.format}` is not supported" if format_response.nil?
75
57
 
76
58
  render_response(format_response)
77
59
  end
@@ -79,7 +61,7 @@ module NucleusCore
79
61
  def render_response(entity)
80
62
  render_headers(entity.headers)
81
63
 
82
- method_name = {
64
+ render_method = {
83
65
  NucleusCore::JsonResponse => :render_json,
84
66
  NucleusCore::XmlResponse => :render_xml,
85
67
  NucleusCore::PdfResponse => :render_pdf,
@@ -88,7 +70,16 @@ module NucleusCore
88
70
  NucleusCore::NoResponse => :render_nothing
89
71
  }.fetch(entity.class, nil)
90
72
 
91
- response_adapter&.send(method_name, entity)
73
+ response_adapter&.send(render_method, entity)
74
+ end
75
+
76
+ def handle_exception(exception)
77
+ logger(exception)
78
+
79
+ status = exception_to_status(exception)
80
+ view = NucleusCore::ErrorView.new(message: exception.message, status: status)
81
+
82
+ render_view(view)
92
83
  end
93
84
 
94
85
  def render_headers(headers={})
@@ -99,26 +90,22 @@ module NucleusCore
99
90
  end
100
91
  end
101
92
 
102
- # rubocop:disable Lint/DuplicateBranch
103
93
  def exception_to_status(exception)
104
- config = exception_map
94
+ exceptions = NucleusCore.configuration.exceptions
105
95
 
106
96
  case exception
107
- when NucleusCore::NotFound, *config.not_found
97
+ when NucleusCore::NotFound, *exceptions.not_found
108
98
  :not_found
109
- when NucleusCore::BadRequest, *config.bad_request
99
+ when NucleusCore::BadRequest, *exceptions.bad_request
110
100
  :bad_request
111
- when NucleusCore::NotAuthorized, *config.forbidden
101
+ when NucleusCore::NotAuthorized, *exceptions.forbidden
112
102
  :forbidden
113
- when NucleusCore::Unprocessable, *config.unprocessable
103
+ when NucleusCore::Unprocessable, *exceptions.unprocessable
114
104
  :unprocessable_entity
115
- when NucleusCore::BaseException, *config.server_error
116
- :internal_server_error
117
105
  else
118
106
  :internal_server_error
119
107
  end
120
108
  end
121
- # rubocop:enable Lint/DuplicateBranch
122
109
 
123
110
  def subclass_of(entity, *classes)
124
111
  Set[*entity.class.ancestors].intersect?(classes.to_set)
@@ -127,9 +114,5 @@ module NucleusCore
127
114
  def logger(object, log_level=:info)
128
115
  NucleusCore.configuration.logger&.send(log_level, object)
129
116
  end
130
-
131
- def exception_map
132
- NucleusCore.configuration.exceptions_map
133
- end
134
117
  end
135
118
  end
@@ -0,0 +1,13 @@
1
+ require "nucleus_core/response_adapters/response_adapter"
2
+
3
+ class NucleusCore::CsvResponse < NucleusCore::ResponseAdapter
4
+ def initialize(attrs={})
5
+ attrs = attrs.merge(
6
+ disposition: "attachment",
7
+ filename: attrs.fetch(:filename) { "response.csv" },
8
+ type: "text/csv; charset=UTF-8;"
9
+ )
10
+
11
+ super(attrs)
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ require "nucleus_core/response_adapters/response_adapter"
2
+
3
+ class NucleusCore::JsonResponse < NucleusCore::ResponseAdapter
4
+ def initialize(attrs={})
5
+ attrs = attrs.merge(type: "application/json")
6
+
7
+ super(attrs)
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require "nucleus_core/response_adapters/response_adapter"
2
+
3
+ class NucleusCore::NoResponse < NucleusCore::ResponseAdapter
4
+ def initialize(attrs={})
5
+ attrs = attrs.merge(content: nil, type: "text/html; charset=utf-8")
6
+
7
+ super(attrs)
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ require "nucleus_core/response_adapters/response_adapter"
2
+
3
+ class NucleusCore::PdfResponse < NucleusCore::ResponseAdapter
4
+ def initialize(attrs={})
5
+ attrs = attrs.merge(
6
+ disposition: "inline",
7
+ filename: attrs.fetch(:filename) { "response.pdf" },
8
+ type: "application/pdf"
9
+ )
10
+
11
+ super(attrs)
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ require "nucleus_core/simple_object"
2
+
3
+ class NucleusCore::ResponseAdapter < NucleusCore::SimpleObject
4
+ def initialize(attrs={})
5
+ attributes = defaults
6
+ .merge(attrs)
7
+ .slice(*defaults.keys)
8
+ .tap do |hash|
9
+ hash[:status] = status_code(hash[:status])
10
+ end
11
+
12
+ super(attributes)
13
+ end
14
+
15
+ private
16
+
17
+ def defaults
18
+ {
19
+ content: nil,
20
+ headers: nil,
21
+ status: nil,
22
+ location: nil
23
+ }
24
+ end
25
+
26
+ def status_code(status=nil)
27
+ status = Utils.status_code(status)
28
+ default_status = 200
29
+
30
+ status.zero? ? default_status : status
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ require "nucleus_core/response_adapters/response_adapter"
2
+
3
+ class NucleusCore::TextResponse < NucleusCore::ResponseAdapter
4
+ def initialize(attrs={})
5
+ attrs = attrs.merge(type: "application/text")
6
+
7
+ super(attrs)
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require "nucleus_core/response_adapters/response_adapter"
2
+
3
+ class NucleusCore::XmlResponse < NucleusCore::ResponseAdapter
4
+ def initialize(attrs={})
5
+ attrs = attrs.merge(type: "application/xml")
6
+
7
+ super(attrs)
8
+ end
9
+ end
@@ -1,6 +1,10 @@
1
1
  module NucleusCore
2
- class BasicObject
2
+ class SimpleObject
3
+ attr_reader :__attributes__
4
+
3
5
  def initialize(attrs={})
6
+ @__attributes__ = {}
7
+
4
8
  attrs.each_pair do |key, value|
5
9
  define_singleton_method(key.to_s) do
6
10
  instance_variable_get("@#{key}")
@@ -8,17 +12,16 @@ module NucleusCore
8
12
 
9
13
  define_singleton_method("#{key}=") do |val|
10
14
  instance_variable_set("@#{key}", val)
15
+ @__attributes__[key] = val
11
16
  end
12
17
 
13
18
  instance_variable_set("@#{key}", value)
19
+ @__attributes__[key] = value
14
20
  end
15
21
  end
16
22
 
17
23
  def to_h
18
- instance_variables
19
- .reduce({}) do |acc, var|
20
- acc.merge(var.to_s.delete("@") => instance_variable_get(var))
21
- end
24
+ __attributes__
22
25
  end
23
26
  end
24
27
  end
@@ -1,3 +1,3 @@
1
1
  module NucleusCore
2
- VERSION = "0.1.4".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
@@ -1,4 +1,5 @@
1
- require "nucleus_core/response_adapter"
1
+ require "nucleus_core/response_adapters/response_adapter"
2
+ require "nucleus_core/views/view"
2
3
 
3
4
  class NucleusCore::ErrorView < NucleusCore::View
4
5
  def initialize(attrs={})
@@ -1,4 +1,6 @@
1
- class NucleusCore::View < NucleusCore::BasicObject
1
+ require "nucleus_core/response_adapters/response_adapter"
2
+
3
+ class NucleusCore::View < NucleusCore::SimpleObject
2
4
  def json_response
3
5
  NucleusCore::JsonResponse.new(content: to_h, status: :ok)
4
6
  end
data/lib/nucleus_core.rb CHANGED
@@ -2,61 +2,48 @@ require "ostruct"
2
2
  require "json"
3
3
  require "set"
4
4
 
5
- Dir[File.join(__dir__, "nucleus_core", "extensions", "*.rb")].sort.each { |file| require file }
5
+ extensions = File.join(__dir__, "nucleus_core", "extensions", "*.rb")
6
+ exceptions = File.join(__dir__, "nucleus_core", "exceptions", "*.rb")
7
+ views = File.join(__dir__, "nucleus_core", "views", "*.rb")
8
+ response_adapters = File.join(__dir__, "nucleus_core", "response_adapters", "*.rb")
9
+ [extensions, exceptions, views, response_adapters].each do |dir|
10
+ Dir[dir].sort.each { |f| require f }
11
+ end
6
12
 
7
13
  module NucleusCore
8
14
  autoload :CLI, "nucleus_core/cli"
9
15
  autoload :VERSION, "nucleus_core/version"
10
- autoload :BasicObject, "nucleus_core/basic_object"
11
- autoload :View, "nucleus_core/views/view"
12
- autoload :ErrorView, "nucleus_core/views/error_view"
13
- autoload :ResponseAdapter, "nucleus_core/response_adapter"
14
- autoload :Aggregate, "nucleus_core/aggregate"
15
- autoload :Policy, "nucleus_core/policy"
16
16
  autoload :Operation, "nucleus_core/operation"
17
17
  autoload :Workflow, "nucleus_core/workflow"
18
18
  autoload :Responder, "nucleus_core/responder"
19
-
20
- class BaseException < StandardError; end
21
- class NotAuthorized < BaseException; end
22
- class NotFound < BaseException; end
23
- class Unprocessable < BaseException; end
24
- class BadRequest < BaseException; end
19
+ autoload :RequestAdapter, "nucleus_core/request_adapter"
20
+ autoload :SimpleObject, "nucleus_core/basic_object"
25
21
 
26
22
  class Configuration
27
- attr_reader :exceptions_map
28
- attr_accessor :logger
23
+ attr_accessor :default_response_format, :logger
24
+ attr_reader :exceptions
29
25
 
30
- RESPONSE_ADAPTER_METHODS = %i[
31
- render_json render_xml render_text render_pdf render_csv render_nothing set_header
32
- ].freeze
26
+ ERROR_STATUSES = %i[not_found bad_request unauthorized unprocessable].freeze
33
27
 
34
28
  def initialize
35
29
  @logger = nil
36
- @exceptions_map = nil
30
+ @exceptions = format_exceptions
31
+ @default_response_format = :json
37
32
  end
38
33
 
39
- def exceptions_map=(args={})
40
- @exceptions_map = format_exceptions(args)
34
+ def exceptions=(args={})
35
+ @exceptions = format_exceptions(args)
41
36
  end
42
37
 
43
38
  private
44
39
 
45
- def objectify(hash)
46
- OpenStruct.new(hash)
47
- end
48
-
49
- ERROR_STATUSES = %i[not_found bad_request unauthorized unprocessable server_error].freeze
50
-
51
- def format_exceptions(exceptions={})
52
- exception_defaults = ERROR_STATUSES.reduce({}) { |acc, ex| acc.merge(ex => nil) }
53
- exceptions_map = (exceptions || exception_defaults)
54
- .slice(*exception_defaults.keys)
55
- .reduce({}) do |acc, (key, value)|
56
- acc.merge(key => Array.wrap(value))
57
- end
40
+ def format_exceptions(args={})
41
+ exceptions = ERROR_STATUSES
42
+ .reduce({}) { |acc, name| acc.merge(name => nil) }
43
+ .merge(args)
44
+ .transform_values { |values| Utils.wrap(values) }
58
45
 
59
- objectify(exceptions_map)
46
+ OpenStruct.new(exceptions)
60
47
  end
61
48
  end
62
49
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nucleus-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dodgerogers
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-02 00:00:00.000000000 Z
11
+ date: 2023-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -70,16 +70,16 @@ dependencies:
70
70
  name: rubocop
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - '='
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 1.44.1
75
+ version: 1.45.1
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - '='
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 1.44.1
82
+ version: 1.45.1
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rubocop-minitest
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -114,14 +114,14 @@ dependencies:
114
114
  requirements:
115
115
  - - '='
116
116
  - !ruby/object:Gem::Version
117
- version: 1.15.2
117
+ version: 1.16.0
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - '='
123
123
  - !ruby/object:Gem::Version
124
- version: 1.15.2
124
+ version: 1.16.0
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: rubocop-rake
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -136,7 +136,7 @@ dependencies:
136
136
  - - '='
137
137
  - !ruby/object:Gem::Version
138
138
  version: 0.6.0
139
- description:
139
+ description:
140
140
  email:
141
141
  - dodgerogers@hotmail.com
142
142
  executables:
@@ -148,16 +148,24 @@ files:
148
148
  - README.md
149
149
  - exe/nucleus
150
150
  - lib/nucleus_core.rb
151
- - lib/nucleus_core/aggregate.rb
152
- - lib/nucleus_core/basic_object.rb
153
151
  - lib/nucleus_core/cli.rb
154
- - lib/nucleus_core/extensions/array.rb
155
- - lib/nucleus_core/extensions/rack.rb
152
+ - lib/nucleus_core/exceptions/bad_request.rb
153
+ - lib/nucleus_core/exceptions/base.rb
154
+ - lib/nucleus_core/exceptions/not_authorized.rb
155
+ - lib/nucleus_core/exceptions/not_found.rb
156
+ - lib/nucleus_core/exceptions/unprocessable.rb
157
+ - lib/nucleus_core/extensions/utils.rb
156
158
  - lib/nucleus_core/operation.rb
157
- - lib/nucleus_core/policy.rb
158
- - lib/nucleus_core/repository.rb
159
+ - lib/nucleus_core/request_adapter.rb
159
160
  - lib/nucleus_core/responder.rb
160
- - lib/nucleus_core/response_adapter.rb
161
+ - lib/nucleus_core/response_adapters/csv_response.rb
162
+ - lib/nucleus_core/response_adapters/json_response.rb
163
+ - lib/nucleus_core/response_adapters/no_response.rb
164
+ - lib/nucleus_core/response_adapters/pdf_response.rb
165
+ - lib/nucleus_core/response_adapters/response_adapter.rb
166
+ - lib/nucleus_core/response_adapters/text_response.rb
167
+ - lib/nucleus_core/response_adapters/xml_response.rb
168
+ - lib/nucleus_core/simple_object.rb
161
169
  - lib/nucleus_core/version.rb
162
170
  - lib/nucleus_core/views/error_view.rb
163
171
  - lib/nucleus_core/views/view.rb
@@ -171,7 +179,7 @@ metadata:
171
179
  source_code_uri: https://github.com/dodgerogers/nucleus-core
172
180
  homepage_uri: https://github.com/dodgerogers/nucleus-core
173
181
  rubygems_mfa_required: 'true'
174
- post_install_message:
182
+ post_install_message:
175
183
  rdoc_options: []
176
184
  require_paths:
177
185
  - lib
@@ -186,8 +194,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
194
  - !ruby/object:Gem::Version
187
195
  version: '0'
188
196
  requirements: []
189
- rubygems_version: 3.4.5
190
- signing_key:
197
+ rubygems_version: 3.0.3.1
198
+ signing_key:
191
199
  specification_version: 4
192
200
  summary: A Ruby business logic framework
193
201
  test_files: []
@@ -1 +0,0 @@
1
- class NucleusCore::Aggregate < NucleusCore::BasicObject; end
@@ -1,11 +0,0 @@
1
- class Array
2
- def self.wrap(object)
3
- if object.nil?
4
- []
5
- elsif object.respond_to?(:to_ary)
6
- object.to_ary || [object]
7
- else
8
- [object]
9
- end
10
- end
11
- end
@@ -1,86 +0,0 @@
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
@@ -1,21 +0,0 @@
1
- module NucleusCore
2
- class Policy
3
- attr_reader :user, :record
4
-
5
- def initialize(user, record=nil)
6
- @user = user
7
- @record = record
8
- end
9
-
10
- def enforce!(*policy_methods)
11
- policy_methods.each do |policy_method_and_args|
12
- next if send(*policy_method_and_args)
13
-
14
- name = Array.wrap(policy_method_and_args).first
15
- message = "You do not have access to: #{name}"
16
-
17
- raise NucleusCore::NotAuthorized, message
18
- end
19
- end
20
- end
21
- end
@@ -1,6 +0,0 @@
1
- module NucleusCore
2
- class Repository
3
- def self.execute
4
- end
5
- end
6
- end
@@ -1,81 +0,0 @@
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