navigable 1.5.1 → 1.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +49 -92
- data/lib/navigable/command.rb +5 -5
- data/lib/navigable/command_builder.rb +42 -0
- data/lib/navigable/errors.rb +17 -0
- data/lib/navigable/immutable_object.rb +41 -0
- data/lib/navigable/immutable_struct.rb +17 -0
- data/lib/navigable/input.rb +42 -0
- data/lib/navigable/observable.rb +29 -12
- data/lib/navigable/observer.rb +4 -3
- data/lib/navigable/observer_interface.rb +1 -1
- data/lib/navigable/observer_map.rb +0 -1
- data/lib/navigable/result.rb +49 -0
- data/lib/navigable/version.rb +1 -1
- data/lib/navigable.rb +1 -1
- data/navigable.gemspec +2 -2
- metadata +14 -11
- data/lib/navigable/dispatcher.rb +0 -39
- data/lib/navigable/null_resolver.rb +0 -19
- data/lib/navigable/resolver.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa68142c1f4409d279fc63286903134018efc36b11325184012ca55a335a998a
|
4
|
+
data.tar.gz: 9426d5938d355c4f47dfba1970c8e835635c2cfd0511fd064bee4abb8bfa04a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d9e4e3f63dab110af1fd28d0592f00d5c4b1bbc4e9c8347e78b9cbe35f979c3ee8974061a5a90c49c8f2242fa39771564ebed84e7d14b11697ccbcbcc3ec7ef7
|
7
|
+
data.tar.gz: f78fd62ce3dc5cf6602bf2b6d1fbadc46593ead0e3a967669f808f03206447d11d50a5e47c18d3e4c971ecb3ed541a884be0573f1161a50927fae501741beb2a
|
data/README.md
CHANGED
@@ -2,97 +2,39 @@
|
|
2
2
|
|
3
3
|
# Navigable
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
Navigable is a family of gems that together provide all the tools you need to build fast, testable, and reliable JSON and/or GraphQL based APIs with isolated, composable business logic. The gems include:
|
8
|
-
|
9
|
-
<table style="margin: 20px 0">
|
10
|
-
<tr height="140">
|
11
|
-
<td width="130"><img alt="Clipper Ship" src="https://raw.githubusercontent.com/first-try-software/navigable/main/assets/clipper.png"></td>
|
12
|
-
<td>
|
13
|
-
|
14
|
-
**[Navigable][navigable]**<br>
|
15
|
-
A stand-alone tool for isolating business logic from external interfaces and cross-cutting concerns. Navigable composes self-configured command and observer objects to allow you to extend your business logic without modifying it. Navigable is compatible with any Ruby-based application development framework, including Rails, Hanami, and Sinatra.
|
16
|
-
|
17
|
-
</td>
|
18
|
-
</tr>
|
19
|
-
<tr height="140">
|
20
|
-
<td width="130"><img alt="Compass" src="https://raw.githubusercontent.com/first-try-software/navigable/main/assets/sextant.png"></td>
|
21
|
-
<td>
|
22
|
-
|
23
|
-
**[Navigable Router][router]** *(coming soon)*<br>
|
24
|
-
A simple, highly-performant, Rack-based router.
|
25
|
-
|
26
|
-
</td>
|
27
|
-
</tr>
|
28
|
-
<tr height="140">
|
29
|
-
<td width="130"><img alt="Compass" src="https://raw.githubusercontent.com/first-try-software/navigable/main/assets/compass.png"></td>
|
30
|
-
<td>
|
31
|
-
|
32
|
-
**[Navigable Server][server]** *(coming soon)*<br>
|
33
|
-
A Rack-based server for building Ruby and Navigable web applications.
|
34
|
-
|
35
|
-
</td>
|
36
|
-
</tr>
|
37
|
-
<tr height="140">
|
38
|
-
<td width="130"><img alt="Map" src="https://raw.githubusercontent.com/first-try-software/navigable/main/assets/map.png"></td>
|
39
|
-
<td>
|
40
|
-
|
41
|
-
**[Navigable GraphQL][graphql]** *(coming soon)*<br>
|
42
|
-
An extension of Navigable Server for building GraphQL APIs.
|
43
|
-
|
44
|
-
</td>
|
45
|
-
</tr>
|
46
|
-
</table>
|
47
|
-
|
48
|
-
<br><br>
|
49
|
-
|
50
|
-
<img style="width: 600px; display: block; margin: 0 auto;" alt="Lighthouse" src="https://raw.githubusercontent.com/first-try-software/navigable/main/assets/lighthouse.png">
|
51
|
-
|
52
|
-
# The Navigable Charter
|
5
|
+
Navigable is a stand-alone tool for isolating business logic from external interfaces and cross-cutting concerns. Navigable composes self-configured command and observer objects to allow you to extend your business logic without modifying it. Navigable is compatible with any Ruby-based application development framework, including Rails, Hanami, and Sinatra.
|
53
6
|
|
54
|
-
|
7
|
+
<br>
|
55
8
|
|
56
|
-
|
57
|
-
|
58
|
-
## Who We Are
|
59
|
-
|
60
|
-
We are professional Rubyists. We could write software in any language, but we choose to work in Ruby because it is so beautiful and expressive. We love Ruby.
|
61
|
-
|
62
|
-
We are test-oriented developers. We always write tests for our code, often before we've written the code. And, despite the conventional wisdom that investing in tests produces diminishing returns as you approach 100% coverage, we prefer the confidence we get with full coverage.
|
63
|
-
|
64
|
-
We are also students of software architecture. We apply SOLID object-oriented design principles like the [Single Responsibility][srp] and [Open/Closed][ocp] Principles to everything we build. And, we follow [Sandi Metz's Rules][sandi] as much as possible. This leads us to write small, loosely coupled, highly cohesive classes.
|
65
|
-
|
66
|
-
## Why We Wrote Navigable
|
67
|
-
|
68
|
-
Besides being Rubyists, we are also seasoned Rails developers. Most of our experience with Ruby has involved Rails. We've also built applications in Sinatra, and dabbled with Hanami. And, while they all have strengths, we're not completely satisfied with any of them.
|
69
|
-
|
70
|
-
### Rails
|
9
|
+
[![Gem Version](https://badge.fury.io/rb/navigable.svg)](https://badge.fury.io/rb/navigable) [![Build Status](https://travis-ci.org/first-try-software/navigable.svg?branch=main)](https://travis-ci.org/first-try-software/navigable) [![Maintainability](https://api.codeclimate.com/v1/badges/33ca28cb17e1b512e006/maintainability)](https://codeclimate.com/github/first-try-software/navigable/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/33ca28cb17e1b512e006/test_coverage)](https://codeclimate.com/github/first-try-software/navigable/test_coverage)
|
71
10
|
|
72
|
-
|
11
|
+
## Installation
|
73
12
|
|
74
|
-
|
13
|
+
Add this line to your application's Gemfile:
|
75
14
|
|
76
|
-
|
15
|
+
```ruby
|
16
|
+
gem 'navigable'
|
17
|
+
```
|
77
18
|
|
78
|
-
|
19
|
+
And then execute:
|
79
20
|
|
80
|
-
|
21
|
+
$ bundle install
|
81
22
|
|
82
|
-
|
23
|
+
Or install it yourself as:
|
83
24
|
|
84
|
-
|
25
|
+
$ gem install navigable
|
85
26
|
|
86
|
-
##
|
27
|
+
## Usage
|
87
28
|
|
88
|
-
|
29
|
+
We built Navigable to help separate the web adapter and persistence layer from your actual business logic. And, we did it in a composable manner that allows for incredible flexibility. Here's a peek:
|
89
30
|
|
90
31
|
```ruby
|
32
|
+
# CreateAlert command
|
91
33
|
class CreateAlert
|
92
34
|
extend Navigable::Command
|
93
35
|
|
94
36
|
corresponds_to :create_alert
|
95
|
-
corresponds_to :
|
37
|
+
corresponds_to :create_alert_and_notify
|
96
38
|
|
97
39
|
def execute
|
98
40
|
return failed_to_validate(new_alert) unless new_alert.valid?
|
@@ -101,13 +43,22 @@ class CreateAlert
|
|
101
43
|
successfully created_alert
|
102
44
|
end
|
103
45
|
|
104
|
-
|
46
|
+
private
|
47
|
+
|
48
|
+
def new_alert
|
49
|
+
@new_alert ||= Alert.new(params)
|
50
|
+
end
|
51
|
+
|
52
|
+
def created_alert
|
53
|
+
@created_alert ||= @new_alert.save
|
54
|
+
end
|
105
55
|
end
|
106
56
|
|
57
|
+
# AllRecipientsNotifier observer
|
107
58
|
class AllRecipientsNotifier
|
108
59
|
extend Navigable::Observer
|
109
60
|
|
110
|
-
observes :
|
61
|
+
observes :create_alert_and_notify
|
111
62
|
|
112
63
|
def on_success(alert)
|
113
64
|
NotifyAllRecipientsWorker.perform_async(alert_id: alert.id)
|
@@ -124,7 +75,7 @@ Navigable::Dispatcher.dispatch(:create_alert, params: alert_params)
|
|
124
75
|
Or, create an alert and notify all recipients:
|
125
76
|
|
126
77
|
```ruby
|
127
|
-
Navigable::Dispatcher.dispatch(:
|
78
|
+
Navigable::Dispatcher.dispatch(:create_alert_and_notify, params: alert_params)
|
128
79
|
```
|
129
80
|
|
130
81
|
All without having to add conditional logic about the notifications to the `CreateAlert` class.
|
@@ -132,6 +83,7 @@ All without having to add conditional logic about the notifications to the `Crea
|
|
132
83
|
Similarly, you can add cross-cutting concerns to an application just as easily:
|
133
84
|
|
134
85
|
```ruby
|
86
|
+
# Monitor observer
|
135
87
|
class Monitor
|
136
88
|
extend Navigable::Observer
|
137
89
|
|
@@ -164,27 +116,33 @@ Here are a few things to look for in the code above:
|
|
164
116
|
|
165
117
|
For a deeper look at the core concepts introduced by Navigable, please have a look at our [wiki][wiki].
|
166
118
|
|
167
|
-
##
|
168
|
-
|
169
|
-
We are really excited about Navigable! We think it solves the problem of seperating business logic from the web interface, persistence layer, and even cross-cutting concerns in an elegant and simple way.
|
170
|
-
|
171
|
-
We're thrilled you're checking out Navigable! If you have any questions or comments, please feel free to reach out to [navigable@firsttry.software][mail].
|
119
|
+
## Rails Usage
|
172
120
|
|
173
|
-
|
174
|
-
|
175
|
-
Add this line to your application's Gemfile:
|
121
|
+
The code above can be integrated into Rails like this:
|
176
122
|
|
177
123
|
```ruby
|
178
|
-
|
179
|
-
|
124
|
+
# AlertsController
|
125
|
+
class AlertsController < ApplicationController
|
126
|
+
# ...
|
180
127
|
|
181
|
-
|
128
|
+
def create
|
129
|
+
@alert = Navigable::Dispatcher.dispatch(:create_alert, params: alert_params)
|
182
130
|
|
183
|
-
|
131
|
+
if @alert.persisted?
|
132
|
+
redirect_to @alert, notice: 'Alert successfully created.'
|
133
|
+
else
|
134
|
+
render :new
|
135
|
+
end
|
136
|
+
end
|
184
137
|
|
185
|
-
|
138
|
+
# ...
|
139
|
+
end
|
186
140
|
|
187
|
-
|
141
|
+
# Alert model
|
142
|
+
class Alert < ApplicationRecord
|
143
|
+
validates :title, presence: true
|
144
|
+
end
|
145
|
+
```
|
188
146
|
|
189
147
|
## Contributing
|
190
148
|
|
@@ -198,7 +156,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
198
156
|
|
199
157
|
Everyone interacting in the Navigable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/first-try-software/navigable/blob/main/CODE_OF_CONDUCT.md).
|
200
158
|
|
201
|
-
|
202
159
|
[sandi]: https://thoughtbot.com/blog/sandi-metz-rules-for-developers
|
203
160
|
[srp]: https://en.wikipedia.org/wiki/Single-responsibility_principle
|
204
161
|
[ocp]: https://en.wikipedia.org/wiki/Open–closed_principle
|
data/lib/navigable/command.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
3
|
require 'navigable/observable'
|
4
|
+
require 'navigable/input'
|
4
5
|
|
5
6
|
module Navigable
|
6
7
|
module Command
|
@@ -11,8 +12,8 @@ module Navigable
|
|
11
12
|
|
12
13
|
def self.extended(base)
|
13
14
|
base.extend(Manufacturable::Item)
|
15
|
+
base.extend(Observable)
|
14
16
|
base.extend(ClassMethods)
|
15
|
-
base.include(Observable)
|
16
17
|
base.include(InstanceMethods)
|
17
18
|
end
|
18
19
|
|
@@ -23,12 +24,11 @@ module Navigable
|
|
23
24
|
end
|
24
25
|
|
25
26
|
module InstanceMethods
|
26
|
-
attr_reader :
|
27
|
+
attr_reader :input, :observers
|
27
28
|
|
28
|
-
def inject(
|
29
|
-
@
|
29
|
+
def inject(input: Input.new, observers: [])
|
30
|
+
@input = input
|
30
31
|
@observers = observers
|
31
|
-
@resolver = resolver
|
32
32
|
end
|
33
33
|
|
34
34
|
def execute
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'navigable/observer'
|
4
|
+
require 'navigable/command'
|
5
|
+
|
6
|
+
module Navigable
|
7
|
+
class CommandBuilder
|
8
|
+
attr_reader :key, :input
|
9
|
+
|
10
|
+
def initialize(key, input:)
|
11
|
+
@key = key
|
12
|
+
@input = input
|
13
|
+
end
|
14
|
+
|
15
|
+
def observed_by(observer)
|
16
|
+
observers << observer
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute
|
21
|
+
command.execute
|
22
|
+
end
|
23
|
+
|
24
|
+
def observers
|
25
|
+
@observers ||= registered_observers
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def registered_observers
|
31
|
+
Manufacturable.build_all(Observer::TYPE, key) { |observer| observer.inject(input: input) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def command
|
35
|
+
build_command.tap { |command| raise Navigable::Command::NotFoundError unless command }
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_command
|
39
|
+
Manufacturable.build_one(Command::TYPE, key) { |command| command.inject(input: input, observers: observers) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Navigable
|
4
|
+
class Errors
|
5
|
+
def initialize()
|
6
|
+
@errors = Hash.new { |h, k| h[k] = [] }
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(field, message)
|
10
|
+
@errors[field] << message
|
11
|
+
end
|
12
|
+
|
13
|
+
def empty?
|
14
|
+
@errors.empty?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Navigable
|
4
|
+
class ImmutableObject
|
5
|
+
@attribute_names = []
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :attribute_names
|
9
|
+
|
10
|
+
def attribute(name)
|
11
|
+
@attribute_names << name
|
12
|
+
attr_reader(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def inherited(subclass)
|
18
|
+
super
|
19
|
+
subclass.instance_variable_set(:@attribute_names, @attribute_names.dup)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(**args)
|
24
|
+
absent_attributes = args.keys - self.class.attribute_names
|
25
|
+
|
26
|
+
if absent_attributes.any?
|
27
|
+
raise ArgumentError, "Unknown attribute #{absent_attributes}"
|
28
|
+
end
|
29
|
+
|
30
|
+
args.each do |name, value|
|
31
|
+
instance_variable_set("@#{name}", value)
|
32
|
+
end
|
33
|
+
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def attributes
|
38
|
+
self.class.attribute_names.to_h { |name| [name, public_send(name)] }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Navigable
|
4
|
+
class ImmutableStruct < Struct
|
5
|
+
class << self
|
6
|
+
def new(*args, &block)
|
7
|
+
super(*args, keyword_init: true, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(**args)
|
12
|
+
members.each { |key| args.fetch(key) }
|
13
|
+
super(**args)
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'navigable/immutable_object'
|
4
|
+
require 'navigable/errors'
|
5
|
+
|
6
|
+
module Navigable
|
7
|
+
class Input < ImmutableObject
|
8
|
+
class << self
|
9
|
+
def validates_presence_of(field)
|
10
|
+
validations << ->(subject) do
|
11
|
+
if subject.public_send(field).nil?
|
12
|
+
subject.errors.add(field, "not present")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def validates_max_length_of(field, max_length)
|
18
|
+
validations << ->(subject) do
|
19
|
+
if (subject.public_send(field) && subject.public_send(field).length > max_length)
|
20
|
+
subject.errors.add(field, "longer than #{max_length}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def validations
|
26
|
+
@validations ||= []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :errors
|
31
|
+
|
32
|
+
def initialize(attributes = {})
|
33
|
+
@errors = Errors.new
|
34
|
+
super(**attributes.to_hash.transform_keys(&:to_sym))
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid?
|
38
|
+
self.class.validations.each { |validation| validation.call(self) }
|
39
|
+
@errors.empty?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/navigable/observable.rb
CHANGED
@@ -2,27 +2,44 @@
|
|
2
2
|
|
3
3
|
require 'navigable/observer_map'
|
4
4
|
require 'navigable/executor'
|
5
|
+
require 'navigable/result'
|
5
6
|
|
6
7
|
module Navigable
|
7
8
|
module Observable
|
8
9
|
OBSERVERS_NOT_IMPLEMENTED_MESSAGE = 'Class must implement `observers` method.'
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
|
11
|
+
def self.extended(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
base.include(InstanceMethods)
|
13
14
|
end
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
module ClassMethods
|
17
|
+
def result(*args)
|
18
|
+
@result_class = Navigable::Result.new(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def result_class
|
22
|
+
@result_class ||= Navigable::Result.new
|
23
|
+
end
|
17
24
|
end
|
25
|
+
|
26
|
+
module InstanceMethods
|
27
|
+
def observers
|
28
|
+
raise NotImplementedError.new(OBSERVERS_NOT_IMPLEMENTED_MESSAGE)
|
29
|
+
end
|
18
30
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
Navigable::Executor.execute { observer.send(observer_method, *args) }
|
23
|
-
end
|
31
|
+
def result
|
32
|
+
self.class.result_class
|
33
|
+
end
|
24
34
|
|
25
|
-
|
35
|
+
ObserverMap::METHODS.each_pair do |observable_method, observer_method|
|
36
|
+
define_method(observable_method) do |*args|
|
37
|
+
observers.each do |observer|
|
38
|
+
Navigable::Executor.execute { observer.send(observer_method, *args) }
|
39
|
+
end
|
40
|
+
|
41
|
+
result.send(observer_method, *args)
|
42
|
+
end
|
26
43
|
end
|
27
44
|
end
|
28
45
|
end
|
data/lib/navigable/observer.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
3
|
require 'navigable/observer_interface'
|
4
|
+
require 'navigable/input'
|
4
5
|
|
5
6
|
module Navigable
|
6
7
|
module Observer
|
@@ -24,10 +25,10 @@ module Navigable
|
|
24
25
|
end
|
25
26
|
|
26
27
|
module InstanceMethods
|
27
|
-
attr_reader :
|
28
|
+
attr_reader :input
|
28
29
|
|
29
|
-
def inject(
|
30
|
-
@
|
30
|
+
def inject(input: Navigable::Input.new)
|
31
|
+
@input = input
|
31
32
|
end
|
32
33
|
|
33
34
|
def observed_command_key
|
@@ -5,7 +5,7 @@ require 'navigable/observer_map'
|
|
5
5
|
module Navigable
|
6
6
|
module ObserverInterface
|
7
7
|
ObserverMap::METHODS.values.each do |observer_method|
|
8
|
-
define_method(observer_method) { |*args| }
|
8
|
+
define_method(observer_method) { |*args| } unless method_defined?(observer_method)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'navigable/observer_interface'
|
4
|
+
require 'navigable/immutable_struct'
|
5
|
+
|
6
|
+
module Navigable
|
7
|
+
class Result < ImmutableStruct
|
8
|
+
extend ObserverInterface
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def new(*members)
|
12
|
+
super(*members, :errors)
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_success(*values)
|
16
|
+
new(*values)
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_failure(errors)
|
20
|
+
values = members.to_h { |member| [member, nil] }
|
21
|
+
new(**values.merge(errors: errors))
|
22
|
+
end
|
23
|
+
|
24
|
+
alias_method :on_failure_to_validate, :on_failure
|
25
|
+
alias_method :on_failure_to_find, :on_failure
|
26
|
+
alias_method :on_failure_to_create, :on_failure
|
27
|
+
alias_method :on_failure_to_update, :on_failure
|
28
|
+
alias_method :on_failure_to_delete, :on_failure
|
29
|
+
end
|
30
|
+
|
31
|
+
def and_then(&block)
|
32
|
+
attributes = self.to_h.tap { |attributes| attributes.delete(:errors) }
|
33
|
+
block.call(**attributes) if errors.none?
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def or_else(&block)
|
38
|
+
block.call(errors) if errors.any?
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def initialize(*args)
|
45
|
+
values = { errors: [] }.merge(args.first || {})
|
46
|
+
super(**values)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/navigable/version.rb
CHANGED
data/lib/navigable.rb
CHANGED
data/navigable.gemspec
CHANGED
@@ -21,11 +21,11 @@ Gem::Specification.new do |spec|
|
|
21
21
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|assets)/}) }
|
22
22
|
end
|
23
23
|
spec.bindir = "exe"
|
24
|
-
spec.executables =
|
24
|
+
spec.executables = []
|
25
25
|
spec.require_paths = ["lib"]
|
26
26
|
|
27
27
|
spec.add_dependency "concurrent-ruby", "~> 1.1.7"
|
28
|
-
spec.add_dependency "manufacturable", "~>
|
28
|
+
spec.add_dependency "manufacturable", "~> 2"
|
29
29
|
|
30
30
|
spec.add_development_dependency "bundler", "~> 2.0"
|
31
31
|
spec.add_development_dependency "rake", "~> 12.0"
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: navigable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alan Ridlehoover
|
8
8
|
- Fito von Zastrow
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2022-04-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: concurrent-ruby
|
@@ -31,14 +31,14 @@ dependencies:
|
|
31
31
|
requirements:
|
32
32
|
- - "~>"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '
|
34
|
+
version: '2'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '
|
41
|
+
version: '2'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: bundler
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -132,14 +132,17 @@ files:
|
|
132
132
|
- bin/setup
|
133
133
|
- lib/navigable.rb
|
134
134
|
- lib/navigable/command.rb
|
135
|
-
- lib/navigable/
|
135
|
+
- lib/navigable/command_builder.rb
|
136
|
+
- lib/navigable/errors.rb
|
136
137
|
- lib/navigable/executor.rb
|
137
|
-
- lib/navigable/
|
138
|
+
- lib/navigable/immutable_object.rb
|
139
|
+
- lib/navigable/immutable_struct.rb
|
140
|
+
- lib/navigable/input.rb
|
138
141
|
- lib/navigable/observable.rb
|
139
142
|
- lib/navigable/observer.rb
|
140
143
|
- lib/navigable/observer_interface.rb
|
141
144
|
- lib/navigable/observer_map.rb
|
142
|
-
- lib/navigable/
|
145
|
+
- lib/navigable/result.rb
|
143
146
|
- lib/navigable/version.rb
|
144
147
|
- navigable.gemspec
|
145
148
|
homepage: https://firsttry.software
|
@@ -149,7 +152,7 @@ metadata:
|
|
149
152
|
homepage_uri: https://firsttry.software
|
150
153
|
source_code_uri: https://github.com/first-try-software/navigable
|
151
154
|
bug_tracker_uri: https://github.com/first-try-software/navigable/issues
|
152
|
-
post_install_message:
|
155
|
+
post_install_message:
|
153
156
|
rdoc_options: []
|
154
157
|
require_paths:
|
155
158
|
- lib
|
@@ -164,8 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
167
|
- !ruby/object:Gem::Version
|
165
168
|
version: '0'
|
166
169
|
requirements: []
|
167
|
-
rubygems_version: 3.
|
168
|
-
signing_key:
|
170
|
+
rubygems_version: 3.3.11
|
171
|
+
signing_key:
|
169
172
|
specification_version: 4
|
170
173
|
summary: Ahoy! Welcome aboard Navigable!
|
171
174
|
test_files: []
|
data/lib/navigable/dispatcher.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen-string-literal: true
|
2
|
-
|
3
|
-
require 'navigable/null_resolver'
|
4
|
-
require 'navigable/observer'
|
5
|
-
require 'navigable/command'
|
6
|
-
|
7
|
-
module Navigable
|
8
|
-
class Dispatcher
|
9
|
-
def self.dispatch(key, params: {}, resolver: NullResolver.new)
|
10
|
-
self.new(key, params: params, resolver: resolver).dispatch
|
11
|
-
end
|
12
|
-
|
13
|
-
def dispatch
|
14
|
-
command.execute
|
15
|
-
resolver.resolve
|
16
|
-
end
|
17
|
-
|
18
|
-
attr_reader :key, :params, :resolver
|
19
|
-
private :key, :params, :resolver
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def initialize(key, params:, resolver:)
|
24
|
-
@key, @params, @resolver = key, params, resolver
|
25
|
-
end
|
26
|
-
|
27
|
-
def observers
|
28
|
-
Manufacturable.build_all(Observer::TYPE, key) { |observer| observer.inject(params: params) }
|
29
|
-
end
|
30
|
-
|
31
|
-
def command
|
32
|
-
build_command.tap { |command| raise Navigable::Command::NotFoundError unless command }
|
33
|
-
end
|
34
|
-
|
35
|
-
def build_command
|
36
|
-
Manufacturable.build_one(Command::TYPE, key) { |command| command.inject(params: params, observers: observers, resolver: resolver) }
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen-string-literal: true
|
2
|
-
|
3
|
-
require 'navigable/resolver'
|
4
|
-
|
5
|
-
module Navigable
|
6
|
-
class NullResolver
|
7
|
-
extend Navigable::Resolver
|
8
|
-
|
9
|
-
def resolve
|
10
|
-
@result
|
11
|
-
end
|
12
|
-
|
13
|
-
ObserverMap::METHODS.values.each do |observer_method|
|
14
|
-
define_method(observer_method) do |result|
|
15
|
-
@result = result
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
data/lib/navigable/resolver.rb
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
# frozen-string-literal: true
|
2
|
-
|
3
|
-
require 'navigable/observer_interface'
|
4
|
-
|
5
|
-
module Navigable
|
6
|
-
module Resolver
|
7
|
-
TYPE = :__resolver__
|
8
|
-
RESOLVE_NOT_IMPLEMENTED_MESSAGE = 'Resolver classes must implement a `resolve` method.'
|
9
|
-
|
10
|
-
def self.extended(base)
|
11
|
-
base.extend(Manufacturable::Item)
|
12
|
-
base.extend(ClassMethods)
|
13
|
-
base.include(ObserverInterface)
|
14
|
-
base.include(InstanceMethods)
|
15
|
-
end
|
16
|
-
|
17
|
-
module ClassMethods
|
18
|
-
def default_resolver
|
19
|
-
default_manufacturable(TYPE)
|
20
|
-
end
|
21
|
-
|
22
|
-
def resolves(key)
|
23
|
-
corresponds_to(key, TYPE)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
module InstanceMethods
|
28
|
-
def resolve
|
29
|
-
raise NotImplementedError.new(RESOLVE_NOT_IMPLEMENTED_MESSAGE)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|