nucleus-core 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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