conjunction 0.20.2 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +711 -4
- data/lib/conjunction/configuration.rb +12 -0
- data/lib/conjunction/conjunctive.rb +48 -0
- data/lib/conjunction/junction.rb +49 -0
- data/lib/conjunction/naming_convention.rb +44 -0
- data/lib/conjunction/nexus.rb +43 -0
- data/lib/conjunction/prototype.rb +26 -0
- data/lib/conjunction/rspec/custom_matchers/be_conjoined_to.rb +19 -0
- data/lib/conjunction/rspec/custom_matchers/be_conjugated_from.rb +23 -0
- data/lib/conjunction/rspec/custom_matchers/conjugate_into.rb +18 -0
- data/lib/conjunction/rspec/custom_matchers/define_prototype.rb +21 -0
- data/lib/conjunction/rspec/custom_matchers/have_conjunction_prefix.rb +23 -0
- data/lib/conjunction/rspec/custom_matchers/have_conjunction_suffix.rb +21 -0
- data/lib/conjunction/rspec/custom_matchers/have_junction_key.rb +21 -0
- data/lib/conjunction/rspec/custom_matchers/have_prototype.rb +21 -0
- data/lib/conjunction/rspec/custom_matchers/have_prototype_name.rb +21 -0
- data/lib/conjunction/rspec/custom_matchers.rb +11 -0
- data/lib/conjunction/spec_helper.rb +3 -0
- data/lib/conjunction/version.rb +1 -1
- data/lib/conjunction.rb +18 -0
- metadata +50 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29f3f2cc286036dc191c0e25fb9add5e66641c12c874d96726ac7edaec31ef38
|
4
|
+
data.tar.gz: 29d161b6a36913ea34e11e0d7ffd20f98d6144b50cdbad6303344919ee088ab6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1888c5f4669520ee7028216dfbbb2f944ec20fe2a97e1bfc4ce6d04f1d58c4f0295103fd7e5c67fa7427f79c56be7d4b80ce54f11d258a41d6f5e6a65d669fc
|
7
|
+
data.tar.gz: 6d12506c38b16fa38fce51759b0f78f6c611cc45c3cc8453cc0a60bb0526339e930595dd17f0be19a987497a6b23d40368082f5376287e5cf4e4dabc1d6a0f82
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Conjunction
|
2
2
|
|
3
|
-
|
3
|
+
Join together related concepts for a common purpose with Conjugation.
|
4
4
|
|
5
5
|
[![Gem Version](https://badge.fury.io/rb/conjunction.svg)](https://badge.fury.io/rb/conjunction)
|
6
6
|
[![Build Status](https://semaphoreci.com/api/v1/freshly/spicerack/branches/master/badge.svg)](https://semaphoreci.com/freshly/spicerack)
|
@@ -8,7 +8,18 @@ Create cacheable collections of filtered, sorted, and paginated ActiveRecord obj
|
|
8
8
|
[![Test Coverage](https://api.codeclimate.com/v1/badges/7e089c2617c530a85b17/test_coverage)](https://codeclimate.com/github/Freshly/spicerack/test_coverage)
|
9
9
|
|
10
10
|
* [Installation](#installation)
|
11
|
-
* [
|
11
|
+
* [QuickStart Guide](#quickstart-guide)
|
12
|
+
* [Gem Developer Usage](#gem-developer-usage)
|
13
|
+
* [On the Separation of Concerns](#on-the-separation-of-concerns)
|
14
|
+
* [Why does this exist?](#why-does-this-exist)
|
15
|
+
* [How's it Function?](#hows-it-function)
|
16
|
+
* [Digging In](#digging-in)
|
17
|
+
* [On Naming Conventions](#on-naming-conventions)
|
18
|
+
* [Backreference](#backreference)
|
19
|
+
* [Bi-direction Cross-Reference](#bi-direction-cross-reference)
|
20
|
+
* [Configuration](#configuration)
|
21
|
+
* [Explicit vs Override](#explicit-vs-override)
|
22
|
+
* [Nexus](#nexus)
|
12
23
|
* [Development](#development)
|
13
24
|
* [Contributing](#contributing)
|
14
25
|
* [License](#license)
|
@@ -29,9 +40,705 @@ Or install it yourself as:
|
|
29
40
|
|
30
41
|
$ gem install conjunction
|
31
42
|
|
32
|
-
##
|
43
|
+
## QuickStart Guide
|
33
44
|
|
34
|
-
|
45
|
+
Let's say you have an `Order`:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
class Order < ApplicationRecord
|
49
|
+
def self.service_class
|
50
|
+
OrderService
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
Which descends from an `ApplicationRecord`:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class ApplicationRecord
|
59
|
+
def to_service
|
60
|
+
self.class.service_class.new(self)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
And you have an `OrderService`:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class OrderService < ApplicationService
|
69
|
+
# ...software be here...
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
Which descends from an `ApplicationService`:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class ApplicationService
|
77
|
+
def initialize(object)
|
78
|
+
@object = object
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Use `Conjunction` to tell your Records they can be related (called a Conjunctive):
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
class ApplicationRecord < ActiveRecord::Base
|
87
|
+
include Conjunction::Conjunctive
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
And your services that they are a kind of relation with a naming convention (called a Junction):
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class ApplicationService
|
95
|
+
include Conjunction::Junction
|
96
|
+
prefixed_with "Service"
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
Now your can look up related objects (like services) implicitly from your objects:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
class ApplicationRecord
|
104
|
+
def to_service
|
105
|
+
conjugate(ApplicationService)&.new(self)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
And remove all the implicit boilerplate which could be assumed:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class Order < ApplicationRecord; end
|
114
|
+
|
115
|
+
Order.conjugate(ApplicationService) # => OrderService
|
116
|
+
```
|
117
|
+
|
118
|
+
And then in the future, any new objects you create which follow convention "just work":
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class Foo < ApplicationRecord; end
|
122
|
+
class FooService < ApplicationService; end
|
123
|
+
|
124
|
+
Foo.conjugate(ApplicationService) # => FooService
|
125
|
+
```
|
126
|
+
|
127
|
+
You can also quickly an easily configure relationships explicitly, either directly:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class Foo < ApplicationRecord
|
131
|
+
conjoins BarService
|
132
|
+
end
|
133
|
+
|
134
|
+
Foo.conjugate(ApplicationService) # => BarService
|
135
|
+
```
|
136
|
+
|
137
|
+
Or through a central routing file called a `Nexus`:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# config/initializers/conjunction_nexus.rb
|
141
|
+
class Conjunction::Nexus
|
142
|
+
couple Foo, to: GazService
|
143
|
+
end
|
144
|
+
|
145
|
+
Foo.conjugate(ApplicationService) # => GazService
|
146
|
+
```
|
147
|
+
|
148
|
+
You may also be interested in reading through the [Configuration](#configuration) docs.
|
149
|
+
|
150
|
+
## Gem Developer Usage
|
151
|
+
|
152
|
+
🚨 **Note**: This is a middleware gem designed to help gem developers or folks with lots of custom DSL objects build them in a cleaner and more standardized way. It is **NOT** expected that most application developers will need to be aware of this gem's existence or configuration!
|
153
|
+
|
154
|
+
### On the Separation of Concerns
|
155
|
+
|
156
|
+
Consider the following:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class User < ApplicationRecord
|
160
|
+
validates :first_name, :last_name, length: { minimum: 2 }, presence: true
|
161
|
+
|
162
|
+
def initials
|
163
|
+
"#{first_name.chr}#{last_name.chr}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def name
|
167
|
+
"#{first_name} #{last_name}"
|
168
|
+
end
|
169
|
+
|
170
|
+
# conventional name in application for displaying in subjects in views
|
171
|
+
def display_name
|
172
|
+
name
|
173
|
+
end
|
174
|
+
|
175
|
+
# name used when this user sends emails to others on platforms
|
176
|
+
def email_from_name
|
177
|
+
name
|
178
|
+
end
|
179
|
+
|
180
|
+
# name used when addressing this user in emails (obviously...)
|
181
|
+
def email_to_name
|
182
|
+
first_name
|
183
|
+
end
|
184
|
+
|
185
|
+
# vestigial code used in the legacy half of the app... smh
|
186
|
+
def nickname
|
187
|
+
last_name
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
This object has very poor [Separation of Concerns](https://en.wikipedia.org/wiki/Separation_of_concerns); it obligates much of the application while providing little value at the cost of extraneous code.
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
class User < ApplicationRecord
|
196
|
+
include PersonNameable
|
197
|
+
include NamedPresentable
|
198
|
+
include NamedPersonEmailAddressable
|
199
|
+
|
200
|
+
# vestigial code used in the legacy half of the app... smh
|
201
|
+
def nickname
|
202
|
+
last_name
|
203
|
+
end
|
204
|
+
end
|
205
|
+
```
|
206
|
+
|
207
|
+
This kinda looks nicer. Until you look at the consequential under-the-hood code:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
module PersonNameable
|
211
|
+
extend ActiveSupport::Concern
|
212
|
+
|
213
|
+
included do
|
214
|
+
validates :first_name, :last_name, length: { minimum: 2 }, presence: true
|
215
|
+
end
|
216
|
+
|
217
|
+
def initials
|
218
|
+
"#{first_name.chr}#{last_name.chr}"
|
219
|
+
end
|
220
|
+
|
221
|
+
def name
|
222
|
+
"#{first_name} #{last_name}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
module NamedPresentable
|
227
|
+
extend ActiveSupport::Concern
|
228
|
+
|
229
|
+
# conventional name in application for displaying in subjects in views, assume name
|
230
|
+
def display_name
|
231
|
+
name
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
module NamedPersonEmailAddressable
|
236
|
+
extend ActiveSupport::Concern
|
237
|
+
|
238
|
+
# name used when this user sends emails to others on platforms
|
239
|
+
def email_from_name
|
240
|
+
name
|
241
|
+
end
|
242
|
+
|
243
|
+
# name used when addressing this user in emails (obviously...)
|
244
|
+
def email_to_name
|
245
|
+
first_name
|
246
|
+
end
|
247
|
+
end
|
248
|
+
```
|
249
|
+
|
250
|
+
And recognize that `user.email_to_name` is still a valid method. This object STILL has very poor `Separation of Concerns` but in this form also suffers from a much higher [Connascence](https://en.wikipedia.org/wiki/Connascence)! OH NO! 😭
|
251
|
+
|
252
|
+
⭐️ That's because **Separation of Concerns** applies to your object architecture *NOT* your file system!
|
253
|
+
|
254
|
+
An object architecture with properly separated concerns would look more like this:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
class User
|
258
|
+
validates :first_name, :last_name, length: { minimum: 2 }, presence: true
|
259
|
+
|
260
|
+
def initials
|
261
|
+
"#{first_name.chr}#{last_name.chr}"
|
262
|
+
end
|
263
|
+
|
264
|
+
def name
|
265
|
+
"#{first_name} #{last_name}"
|
266
|
+
end
|
267
|
+
|
268
|
+
# vestigial code used in the legacy half of the app... smh
|
269
|
+
def nickname
|
270
|
+
last_name
|
271
|
+
end
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
class UserPresenter
|
277
|
+
def initialize(user)
|
278
|
+
@user = user
|
279
|
+
end
|
280
|
+
|
281
|
+
def display_name
|
282
|
+
user.name
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
class UserEmailSender
|
287
|
+
def initialize(user)
|
288
|
+
@user = user
|
289
|
+
end
|
290
|
+
|
291
|
+
def from_name
|
292
|
+
user.name
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
class UserEmailRecipient
|
297
|
+
def initialize(user)
|
298
|
+
@user = user
|
299
|
+
end
|
300
|
+
|
301
|
+
def to_name
|
302
|
+
user.first_name
|
303
|
+
end
|
304
|
+
end
|
305
|
+
```
|
306
|
+
|
307
|
+
Now, the `User` object has no need to know about email sending or receiving, nor whatever naming standards the application has adopted around presenting objects to users.
|
308
|
+
|
309
|
+
This is a nominal pattern adopted by several gems in the rails ecosystem: [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers/tree/0-10-stable), [Draper](https://github.com/drapergem/draper), [Pundit](https://github.com/varvet/pundit) to name a few.
|
310
|
+
|
311
|
+
Enter `Conjunction`, a gem for managing the coupling of properly separated object concerns.
|
312
|
+
|
313
|
+
### Why does this exist?
|
314
|
+
|
315
|
+
Let's imagine we're building a standardized presenter gem for displaying models:
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
class ApplicationPresenter
|
319
|
+
def initialize(model)
|
320
|
+
@model = model
|
321
|
+
end
|
322
|
+
|
323
|
+
def name
|
324
|
+
I18n.t("unknown")
|
325
|
+
end
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
And we want to create a specialized class for displaying a specific model:
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
class UserPresenter < ApplicationPresenter
|
333
|
+
def name
|
334
|
+
I18n.t("format_greetings.#{user.greeting_type}", user.name)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
```
|
338
|
+
|
339
|
+
So the first question is how a `User` know about it's presenter.
|
340
|
+
|
341
|
+
One option is direct reference:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
class User < ApplicationRecord
|
345
|
+
def to_presenter
|
346
|
+
UserPresenter.new(self)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
```
|
350
|
+
|
351
|
+
This leverages `Connascence of Name (CoN)` which is the weakest (and therefore most ideal) reference.
|
352
|
+
|
353
|
+
And this solution is nice, but it gets kind of messy at scale:
|
354
|
+
|
355
|
+
```ruby
|
356
|
+
class User < ApplicationRecord
|
357
|
+
def to_presenter
|
358
|
+
UserPresenter.new(self)
|
359
|
+
end
|
360
|
+
|
361
|
+
def to_serializer
|
362
|
+
UserSerializer.new(self)
|
363
|
+
end
|
364
|
+
|
365
|
+
def to_policy
|
366
|
+
UserPolicy.new(self)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
```
|
370
|
+
|
371
|
+
Especially when you add in the complexity of class AND/OR instance reference:
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
class User < ApplicationRecord
|
375
|
+
class << self
|
376
|
+
def to_presenter
|
377
|
+
UserList.new
|
378
|
+
end
|
379
|
+
|
380
|
+
def to_policy
|
381
|
+
UserPolicy.new
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
def to_presenter
|
386
|
+
UserPresenter.new(self)
|
387
|
+
end
|
388
|
+
|
389
|
+
def to_serializer
|
390
|
+
UserSerializer.new(self)
|
391
|
+
end
|
392
|
+
|
393
|
+
def to_policy
|
394
|
+
UserPolicy.new(self)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
```
|
398
|
+
|
399
|
+
For just three related objects we're at 20 lines of code added to potentially `N` models in the ecosystem.
|
400
|
+
|
401
|
+
This also creates other problems, such as dissimilarity in reference, a model without a presenter will not define a `to_presenter` method, so generic code:
|
402
|
+
|
403
|
+
```ruby
|
404
|
+
model.to_presenter # => raise NoMethodError ?!
|
405
|
+
```
|
406
|
+
|
407
|
+
To get around this, you now need to put some kind of generic handling in the base object:
|
408
|
+
|
409
|
+
```ruby
|
410
|
+
class ApplicationRecord
|
411
|
+
class << self
|
412
|
+
def to_presenter
|
413
|
+
nil
|
414
|
+
end
|
415
|
+
|
416
|
+
def to_policy
|
417
|
+
raise "all records must have a policy"
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def to_presenter
|
422
|
+
null
|
423
|
+
end
|
424
|
+
|
425
|
+
def to_serializer
|
426
|
+
raise "all records must have a serializer"
|
427
|
+
end
|
428
|
+
|
429
|
+
def to_policy
|
430
|
+
raise "all records must have a policy"
|
431
|
+
end
|
432
|
+
end
|
433
|
+
```
|
434
|
+
|
435
|
+
This also obviously isn't a possibility if you are writing a third party gem to introduce a "kind" of object into the system, and the example gems have all solved this problem differently:
|
436
|
+
|
437
|
+
`ActiveModelSerializers` conjured up the [LookupChain](https://github.com/rails-api/active_model_serializers/blob/0-10-stable/lib/active_model_serializers/lookup_chain.rb), `Draper` went the concern route with [decoratable](https://github.com/drapergem/draper/blob/master/lib/draper/decoratable.rb) and `Pundit` evolved the [PolicyFinder](https://github.com/varvet/pundit/blob/master/lib/pundit/policy_finder.rb).
|
438
|
+
|
439
|
+
All these are very disparate and feature-rich implementations of a solution to the problem. All of them offering at least some configurability to control how the lookup occurred to translate the kind of "root" model object into its related SoC object.
|
440
|
+
|
441
|
+
So ultimately, `Conjunction` exists because I thought it would be nice to create a generic solution to this object reference problem that can be utilized by other gems to create some kind of standardization and consistency to this hard problem.
|
442
|
+
|
443
|
+
It also selfishly helps cleanup a lot of duplicate code across several co-developed gems which I will shamelessly plug here: [Command](https://github.com/Freshly/command), [Facet](https://github.com/Freshly/spicerack/tree/develop/facet), [Flow](https://github.com/Freshly/flow), [Law](https://github.com/Freshly/law), [Material](https://github.com/Freshly/material).
|
444
|
+
|
445
|
+
### How's it Function?
|
446
|
+
|
447
|
+
In `Conjunction` there are two distinct concepts: **Prototypes** and **Conjugates**.
|
448
|
+
|
449
|
+
A **Prototype** is unitary (named without a prefix or suffix) and represents the core object around which concerns are being separated, in the above example `User`.
|
450
|
+
|
451
|
+
A **Conjugate** is the `SoC` object which encapsulates the other behaviors or information, in the above example `UserPresenter`; named after the linguistic process that gives the different forms of an verb as they vary according to voice, mood, tense, etc.
|
452
|
+
|
453
|
+
Generally speaking, the community assumption peddled by the ActiveModelSerializers, Drapers, and Pundits of the world seems to be a suffixed naming convention; `AuthorSerializer` for an `Author` class, `ArticleDecorator` for an `Article` class, `UserPolicy` for a `User` class.
|
454
|
+
|
455
|
+
`Conjunction` defines a `.conjugate` **Prototypes** class method to allow the lookup of **Conjugate** objects given their base classes:
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
Author.conjugate(ApplicationSerializer) # => AuthorSerializer
|
459
|
+
Article.conjugate(ApplicationDecorator) # => ArticleDecorator
|
460
|
+
User.conjugate(ApplicationPolicy) # => UserPolicy
|
461
|
+
```
|
462
|
+
|
463
|
+
### Digging In
|
464
|
+
|
465
|
+
`Conjunction` leverages two main concerns: **Conjunctives** and **Junctions**.
|
466
|
+
|
467
|
+
A **Conjunctive** is the generic base class from which your **Prototype** descends, in the above example `ApplicationRecord` is the **Conjunctive** for the `User` **Prototype**.
|
468
|
+
|
469
|
+
```ruby
|
470
|
+
class ApplicationRecord
|
471
|
+
include Conjunction::Conjunctive
|
472
|
+
end
|
473
|
+
```
|
474
|
+
|
475
|
+
This grants `ApplicationRecord` the ability to represent itself as a **Prototype**, the primary requirement of which is to define a `.prototype_name` method (which by default is simply the class name):
|
476
|
+
|
477
|
+
```ruby
|
478
|
+
User.prototype_name # => User
|
479
|
+
User.first.prototype_name # => User
|
480
|
+
```
|
481
|
+
|
482
|
+
A **Junction** is the generic base class from which any **Conjugate** object descends, in the above example `ApplicationPresenter` is the **Junction** for the `UserPresenter` **Conjugate**.
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
class ApplicationPresenter
|
486
|
+
include Conjunction::Junction
|
487
|
+
end
|
488
|
+
```
|
489
|
+
|
490
|
+
This grants `ApplicationPresenter` the ability to reference a **Prototype** and find a related **Conjugate**; also it allows you to write `Conjunction::Junction` and commit it into a production application for serious, which is a total bonus feature 🤩.
|
491
|
+
|
492
|
+
The primary requirement of a `Junction` is to define a `.junction_key` which should be unique among your `SoC` objects.
|
493
|
+
|
494
|
+
🚨 **WARNING**: By default, your Junctions **WILL NOT** define a junction key!
|
495
|
+
|
496
|
+
You can explicitly define a junction key:
|
497
|
+
|
498
|
+
```ruby
|
499
|
+
class ApplicationPresenter
|
500
|
+
include Conjunction::Junction
|
501
|
+
|
502
|
+
class << self
|
503
|
+
def junction_key
|
504
|
+
:any_key_u_want
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
508
|
+
```
|
509
|
+
😽 *Super Lazy??*: Good News, so am I! There are default junction keys!
|
510
|
+
|
511
|
+
### On Naming Conventions
|
512
|
+
|
513
|
+
Given the community standard of `FooBarThing` naming convention for `Things`, there is an assumption that most applications will attempt to keep a minimal `Connascence of Name (CoN)` approach; usually for any given `FooBar` you would expect it's `Thing` to be a `FooBarThing`.
|
514
|
+
|
515
|
+
You can **and should** define your expected naming convention on your Junctions:
|
516
|
+
|
517
|
+
```ruby
|
518
|
+
class ApplicationPresenter
|
519
|
+
include Conjunction::Junction
|
520
|
+
suffixed_with "Presenter"
|
521
|
+
end
|
522
|
+
```
|
523
|
+
|
524
|
+
This now gives the `Presenter` junction enough information to know that it's naming convention is `#{prototype_name}Presenter`. It also provides enough unique identifying information to assume the `junction_key` as an underscored version of the suffix:
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
ApplicationPresenter.junction_key # => presenter
|
528
|
+
```
|
529
|
+
|
530
|
+
You can also use `suffixed_with` if you want to do namespaces instead, ex:
|
531
|
+
|
532
|
+
```ruby
|
533
|
+
class ApplicationFleeb
|
534
|
+
include Conjunction::Junction
|
535
|
+
suffixed_with "Grundus::Fleeb::"
|
536
|
+
end
|
537
|
+
```
|
538
|
+
|
539
|
+
This now assumes that a `Foo` prototype will have a `Grundus::Fleeb::Foo` conjugate.
|
540
|
+
|
541
|
+
💁 *Note*: You can use both a `suffix` and `prefix` together; the junction key will be `#{suffix}_#{prefix}`.
|
542
|
+
|
543
|
+
You can also override the default by setting it to a blank string in a descendant class:
|
544
|
+
|
545
|
+
```ruby
|
546
|
+
class PresenterBase
|
547
|
+
include Conjunction::Junction
|
548
|
+
suffixed_with "Presenter"
|
549
|
+
end
|
550
|
+
|
551
|
+
class ApplicationPresenter < Base::Presenter
|
552
|
+
suffixed_with ""
|
553
|
+
prefixed_with "Presenter::"
|
554
|
+
end
|
555
|
+
```
|
556
|
+
|
557
|
+
#### Backreference
|
558
|
+
|
559
|
+
Naming Conventions are necessary to facilitate "best guess lookup" and therefore if a Conjugate name can be intuited by the naming convention, a Prototype name can be distilled from it:
|
560
|
+
|
561
|
+
```ruby
|
562
|
+
UserPresenter.prototype_class # => User
|
563
|
+
Grundus::Fleeb::User.prototype_class # => User
|
564
|
+
```
|
565
|
+
|
566
|
+
This allows valuable class level introspection into relationships which can be quickly and easily put under test:
|
567
|
+
|
568
|
+
```ruby
|
569
|
+
RSpec.describe User do
|
570
|
+
it { is_expected.to conjugate_into UserPresenter }
|
571
|
+
it { is_expected.to conjugate_into Grundus::Fleeb::User }
|
572
|
+
end
|
573
|
+
```
|
574
|
+
|
575
|
+
From either side of the equation (ideally, both!):
|
576
|
+
|
577
|
+
```ruby
|
578
|
+
RSpec.describe UserPresenter do
|
579
|
+
it { is_expected.to be_conjugated_from User }
|
580
|
+
end
|
581
|
+
```
|
582
|
+
|
583
|
+
```ruby
|
584
|
+
RSpec.describe Grundus::Fleeb::User do
|
585
|
+
it { is_expected.to be_conjugated_from User }
|
586
|
+
end
|
587
|
+
```
|
588
|
+
|
589
|
+
#### Bi-direction Cross-Reference
|
590
|
+
|
591
|
+
An even more interesting side-effect of backreference in the nominal case is Bi-direction Cross-Reference. Two Junctions which are conjugates of a given prototype can be directly conjugated into each other!
|
592
|
+
|
593
|
+
```ruby
|
594
|
+
UserPresenter.conjugate(ApplicationFleeb) # => Grundus::Fleeb::User
|
595
|
+
Grundus::Fleeb::User.conjugate(ApplicationPresenter) # => UserPresenter
|
596
|
+
```
|
597
|
+
|
598
|
+
What's *really* interesting is that you don't even *need* a user class to really exist for this kind of behavior to manifest! The above snippet works even when `defined?(User) == false`!!
|
599
|
+
|
600
|
+
## Configuration
|
601
|
+
|
602
|
+
`Conjunction` expects that a well-thought-out (or at least standardized) naming convention can get you a long way, but given software is hard there must be some mechanism for overrides.
|
603
|
+
|
604
|
+
So it might surprise you to find that no mechanism has been exposed to override `Conjunction`!
|
605
|
+
|
606
|
+
It will *always* run in implicit lookup mode where it attempts to use the naming convention of objects as it understands them to intuit the coupling relationships of your application.
|
607
|
+
|
608
|
+
It *can* be configured **NOT** to perform implicit lookup at all if you hate the idea of magic names "just working":
|
609
|
+
|
610
|
+
```ruby
|
611
|
+
# config/initializers/conjunction.rb
|
612
|
+
Conjunction.configure do |config|
|
613
|
+
config.disable_all_implicit_lookup = true
|
614
|
+
end
|
615
|
+
```
|
616
|
+
|
617
|
+
### Explicit vs Override
|
618
|
+
|
619
|
+
Instead of overriding implicit lookup, you can simply explicitly define conjunctions.
|
620
|
+
|
621
|
+
To directly tell any Conjunctive how it is related to its conjugates, use `.conjoins`:
|
622
|
+
|
623
|
+
```ruby
|
624
|
+
class User < ApplicationRecord
|
625
|
+
conjoins UserPresenter
|
626
|
+
conjoins Grundus::Fleeb::User
|
627
|
+
conjoins UserPolicy
|
628
|
+
end
|
629
|
+
```
|
630
|
+
|
631
|
+
⚠️ **WARNING**: You can only provide objects which are valid `Conjunction::Junctions` to the `.conjoins` method; any object without a valid `junction_key` will raise an exception.
|
632
|
+
|
633
|
+
It is this explicit reference mechanism that exists for you to "override" the default coupling rules of your application:
|
634
|
+
|
635
|
+
```ruby
|
636
|
+
class User < ApplicationRecord
|
637
|
+
conjoins GenericPresenter
|
638
|
+
conjoins Grundus::Fleeb::Dinglebop
|
639
|
+
conjoins AdminOnlyEditOpenViewPolicy
|
640
|
+
end
|
641
|
+
```
|
642
|
+
|
643
|
+
This has the direct and immediate effect of altering what conjugates are returned:
|
644
|
+
|
645
|
+
```ruby
|
646
|
+
User.conjugate(ApplicationPresenter) # => GenericPresenter
|
647
|
+
User.conjugate(ApplicationFleeb) # => Grundus::Fleeb::Dinglebop
|
648
|
+
User.conjugate(ApplicationPolicy) # => AdminOnlyEditOpenViewPolicy
|
649
|
+
```
|
650
|
+
|
651
|
+
This is true EVEN IF you have valid naming-convention-assumed objects that exist. For example, even if there was a `UserPresenter` the immediate example above would return a `GenericPresenter`. This can best be seen in this example:
|
652
|
+
|
653
|
+
```ruby
|
654
|
+
User.conjugate(UserPresenter) # => GenericPresenter
|
655
|
+
```
|
656
|
+
|
657
|
+
Even giving the object what you assume the destination presenter would be, it's conjugate follows the explicit naming rules!
|
658
|
+
|
659
|
+
💁 **Note**: You can use `.conjoins` only when you need to override the otherwise "default" implicit naming conventions. This "explicit as override" methodology is the recommended way to use `Conjunction`, as it calls attention to "what's different" while letting normal "ust work". It also prevents you from having to define an object AND remember to relate it.
|
660
|
+
|
661
|
+
```ruby
|
662
|
+
class User < ApplicationRecord
|
663
|
+
conjoins AdminOnlyEditOpenViewPolicy
|
664
|
+
end
|
665
|
+
```
|
666
|
+
|
667
|
+
Given an expected presence of other objects, this would work as follows:
|
668
|
+
|
669
|
+
```ruby
|
670
|
+
User.conjugate(ApplicationPresenter) # => UserPresenter
|
671
|
+
User.conjugate(ApplicationFleeb) # => Grundus::Fleeb::User
|
672
|
+
User.conjugate(ApplicationPolicy) # => AdminOnlyEditOpenViewPolicy
|
673
|
+
```
|
674
|
+
|
675
|
+
### Nexus
|
676
|
+
|
677
|
+
In what is arguably the coolest class name I've ever written, you can define a central routing nexus which acts as a central source of truth for all the object relationships in your application.
|
678
|
+
|
679
|
+
Create a `config/initializers/conjunction_nexus.rb` file like so:
|
680
|
+
|
681
|
+
```ruby
|
682
|
+
class Conjunction::Nexus
|
683
|
+
couple Foo, to: CommonMaterial
|
684
|
+
couple Bar, to: CommonMaterial
|
685
|
+
|
686
|
+
couple FooFlow, to: FooState, bidirectional: true
|
687
|
+
end
|
688
|
+
```
|
689
|
+
|
690
|
+
The `bidirectional: true` flag above is the DRY form of this equivalent assignment:
|
691
|
+
|
692
|
+
```ruby
|
693
|
+
class Conjunction::Nexus
|
694
|
+
couple FooFlow, to: FooState
|
695
|
+
couple FooState, to: FooFlow
|
696
|
+
end
|
697
|
+
```
|
698
|
+
|
699
|
+
The `Nexus` file is a "best of both worlds" approach if you want to keep your models (conjunctives) limited in what they outwardly need to know about their other conjunctive forms without bloating each model individually.
|
700
|
+
|
701
|
+
💁 **Note**: If you make use of the nexus file and want to enforce explicit lookup behavior in your application, there is a special configuration option to disable implicit lookup for any classes which are defined within the nexus using the `nexus_use_disables_implicit_lookup` flag:
|
702
|
+
|
703
|
+
```ruby
|
704
|
+
# config/initializers/conjunction.rb
|
705
|
+
Conjunction.configure do |config|
|
706
|
+
config.nexus_use_disables_implicit_lookup = true
|
707
|
+
end
|
708
|
+
```
|
709
|
+
|
710
|
+
Now, if you define something within the nexus for any given type of junction, ALL junctions need to follow suite:
|
711
|
+
|
712
|
+
```ruby
|
713
|
+
# Given `FooPresenter`, `BarPresenter`, `Foo`, and `Bar`:
|
714
|
+
class Conjunction::Nexus
|
715
|
+
couple Foo, to: FooPresenter
|
716
|
+
end
|
717
|
+
|
718
|
+
# With `nexus_use_disables_implicit_lookup` false:
|
719
|
+
Foo.conjoins(ApplicationPresenter) # => FooPresenter
|
720
|
+
Bar.conjoins(ApplicationPresenter) # => BarPresenter
|
721
|
+
|
722
|
+
# With `nexus_use_disables_implicit_lookup` true:
|
723
|
+
Foo.conjoins(ApplicationPresenter) # => FooPresenter
|
724
|
+
Bar.conjoins(ApplicationPresenter) # => nil
|
725
|
+
```
|
726
|
+
|
727
|
+
💁 **Note**: Nexus configuration is a "kind" of explicit configuration and shouldn't be mixed with conflicting information on the object itself:
|
728
|
+
|
729
|
+
```ruby
|
730
|
+
class Conjunction::Nexus
|
731
|
+
couple Foo, to: FooPresenter
|
732
|
+
end
|
733
|
+
|
734
|
+
class Foo < ApplicationRecord
|
735
|
+
conjoins BarPresenter
|
736
|
+
end
|
737
|
+
```
|
738
|
+
|
739
|
+
In the above example, `Foo.conjoins(ApplicationPresenter)` would be `BarPresenter` as the configuration closest to the object takes precedence, but this is a really weird and not very expected behavior that is left intact "in-case you really need it".
|
740
|
+
|
741
|
+
The overall recommendation here is to "pick a horse" and either use a Nexus file OR use explicit object level definitions for overrides.
|
35
742
|
|
36
743
|
## Development
|
37
744
|
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conjunction
|
4
|
+
module Configuration
|
5
|
+
extend Spicerack::Configurable
|
6
|
+
|
7
|
+
configuration_options do
|
8
|
+
option :nexus_use_disables_implicit_lookup, default: false
|
9
|
+
option :disable_all_implicit_lookup, default: false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conjunction
|
4
|
+
# **Conjunctives** are **Prototypes** that can be `conjugated` with a **Junction** into a specific **Conjunction**.
|
5
|
+
module Conjunctive
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
include Conjunction::Prototype
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :explicit_conjunctions, instance_writer: false, default: {}
|
12
|
+
|
13
|
+
delegate :conjugate, :conjugate!, to: :class
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
def conjugate(junction)
|
18
|
+
conjugate_with(junction, :conjunction_for)
|
19
|
+
end
|
20
|
+
|
21
|
+
def conjugate!(junction)
|
22
|
+
conjugate_with(junction, :conjunction_for!)
|
23
|
+
end
|
24
|
+
|
25
|
+
def inherited(base)
|
26
|
+
base.explicit_conjunctions = {}
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def conjugate_with(junction, method_name)
|
33
|
+
conjunction = explicit_conjunctions[junction.try(:junction_key)] || Nexus.conjugate(self, junction: junction)
|
34
|
+
return conjunction if conjunction.present?
|
35
|
+
|
36
|
+
return if Conjunction.config.nexus_use_disables_implicit_lookup && Nexus.couples?(junction)
|
37
|
+
|
38
|
+
junction.try(method_name, prototype, prototype_name) unless Conjunction.config.disable_all_implicit_lookup
|
39
|
+
end
|
40
|
+
|
41
|
+
def conjoins(junction)
|
42
|
+
raise TypeError, "#{junction} is not a valid junction" unless junction.respond_to?(:junction_key)
|
43
|
+
|
44
|
+
explicit_conjunctions[junction.junction_key] = junction
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conjunction
|
4
|
+
# A **Junction** is an encapsulation of behavior which has been abstracted out of a **Conjunctive**.
|
5
|
+
module Junction
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
include Conjunction::Conjunctive
|
9
|
+
include Conjunction::NamingConvention
|
10
|
+
|
11
|
+
included do
|
12
|
+
delegate :conjunction_for!, :conjunction_for, :conjunction_name_for, to: :class
|
13
|
+
end
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
def junction_key
|
17
|
+
key_parts.join.underscore.parameterize(separator: "_").to_sym if conjunctive?
|
18
|
+
end
|
19
|
+
|
20
|
+
def prototype_name
|
21
|
+
output = name
|
22
|
+
output.slice!(conjunction_prefix) if conjunction_prefix?
|
23
|
+
output.chomp!(conjunction_suffix) if conjunction_suffix?
|
24
|
+
output unless output == name
|
25
|
+
end
|
26
|
+
|
27
|
+
def conjunction_for!(other_prototype, prototype_name)
|
28
|
+
conjunction_for(other_prototype, prototype_name) or raise DisjointedError, "#{other_prototype} #{name} unknown"
|
29
|
+
end
|
30
|
+
|
31
|
+
def conjunction_for(other_prototype, prototype_name)
|
32
|
+
conjunction_name_for(other_prototype, prototype_name)&.safe_constantize
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def key_parts
|
38
|
+
[ conjunction_prefix, conjunction_suffix ].compact
|
39
|
+
end
|
40
|
+
|
41
|
+
def conjunction_name_for(other_prototype, other_prototype_name)
|
42
|
+
other_prototype_name = other_prototype.prototype_name if other_prototype.respond_to?(:prototype_name)
|
43
|
+
return if other_prototype_name.blank?
|
44
|
+
|
45
|
+
[ conjunction_prefix, other_prototype_name, conjunction_suffix ].compact.join if conjunctive?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conjunction
|
4
|
+
# A **NamingConvention** defines the name formatting of a given **Junction**.
|
5
|
+
module NamingConvention
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
attr_reader :conjunction_prefix, :conjunction_suffix
|
10
|
+
|
11
|
+
def conjunctive?
|
12
|
+
conjunction_prefix? || conjunction_suffix?
|
13
|
+
end
|
14
|
+
|
15
|
+
def conjunction_prefix?
|
16
|
+
conjunction_prefix.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def conjunction_suffix?
|
20
|
+
conjunction_suffix.present?
|
21
|
+
end
|
22
|
+
|
23
|
+
def inherited(base)
|
24
|
+
base.prefixed_with(conjunction_prefix) if conjunction_prefix?
|
25
|
+
base.suffixed_with(conjunction_suffix) if conjunction_suffix?
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def prefixed_with(prefix)
|
32
|
+
raise TypeError, "prefix must be a string" if prefix.present? && !prefix.is_a?(String)
|
33
|
+
|
34
|
+
@conjunction_prefix = prefix
|
35
|
+
end
|
36
|
+
|
37
|
+
def suffixed_with(suffix)
|
38
|
+
raise TypeError, "suffix must be a string" if suffix.present? && !suffix.is_a?(String)
|
39
|
+
|
40
|
+
@conjunction_suffix = suffix
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conjunction
|
4
|
+
# The Nexus provides a central source to couple objects together explicitly:
|
5
|
+
#
|
6
|
+
# class Conjunction::Nexus
|
7
|
+
# couple Foo, to: CommonMaterial
|
8
|
+
# couple Bar, to: CommonMaterial
|
9
|
+
#
|
10
|
+
# couple FooFlow, to: FooState, bidirectional: true
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class Nexus
|
14
|
+
include Singleton
|
15
|
+
|
16
|
+
class_attribute :_couplings, instance_writer: false, default: Hash.new { |hash, key| hash[key] = {} }
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def conjugate(conjunctive, junction:)
|
20
|
+
_couplings[junction.try(:junction_key)][conjunctive] if couples?(junction)
|
21
|
+
end
|
22
|
+
|
23
|
+
def couples?(junction)
|
24
|
+
_couplings.key?(junction.try(:junction_key))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def couple(conjunctive, to:, bidirectional: false)
|
30
|
+
raise TypeError, "#{conjunctive} is not a valid conjunctive" unless conjunctive.respond_to?(:conjugate)
|
31
|
+
raise TypeError, "#{to} is not a valid junction" unless to.respond_to?(:junction_key)
|
32
|
+
|
33
|
+
if bidirectional
|
34
|
+
raise TypeError, "#{conjunctive} is not a valid junction" unless conjunctive.respond_to?(:junction_key)
|
35
|
+
|
36
|
+
_couplings[conjunctive.junction_key][to] = conjunctive
|
37
|
+
end
|
38
|
+
|
39
|
+
_couplings[to.junction_key][conjunctive] = to
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conjunction
|
4
|
+
# A **Prototype** is a uniquely named Object with behavior which has been abstracted out into **Junction** classes.
|
5
|
+
module Prototype
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
delegate :prototype_name, :prototype, :prototype!, to: :class
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def prototype!
|
14
|
+
prototype or raise NameError, "#{prototype_name} is not defined"
|
15
|
+
end
|
16
|
+
|
17
|
+
def prototype
|
18
|
+
prototype_name&.safe_constantize
|
19
|
+
end
|
20
|
+
|
21
|
+
def prototype_name
|
22
|
+
try(:model_name)&.name || name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `.conjoins`
|
4
|
+
#
|
5
|
+
# class Foo < ApplicationRecord
|
6
|
+
# conjoins GenericFleeb
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# class GenericFleeb < ApplicationFleeb; end
|
10
|
+
#
|
11
|
+
# RSpec.describe Foo, type: :model do
|
12
|
+
# it { is_expected.to be_conjoined_to GenericFleeb }
|
13
|
+
# end
|
14
|
+
|
15
|
+
RSpec::Matchers.define :be_conjoined_to do |junction|
|
16
|
+
match { |subject| expect(subject.explicit_conjunctions[junction.junction_key]).to eq junction }
|
17
|
+
description { "be conjoined to #{junction}" }
|
18
|
+
failure_message { |subject| "expected #{subject} to be conjoined to #{junction} but wasn't" }
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `.conjugate`
|
4
|
+
#
|
5
|
+
# class Foo < ApplicationRecord
|
6
|
+
# conjoins GenericFleeb
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# class GenericFleeb < ApplicationFleeb; end
|
10
|
+
#
|
11
|
+
# RSpec.describe GenericFleeb, type: :fleeb do
|
12
|
+
# it { is_expected.to be_conjugated_from Foo }
|
13
|
+
# end
|
14
|
+
|
15
|
+
RSpec::Matchers.define :be_conjugated_from do |conjunctive|
|
16
|
+
match { expect(conjunctive.conjugate(test_subject)).to eq test_subject }
|
17
|
+
description { "be conjugated from #{conjunctive}" }
|
18
|
+
failure_message { "expected #{test_subject} to be conjugated from #{conjunctive} but wasn't" }
|
19
|
+
|
20
|
+
def test_subject
|
21
|
+
subject.is_a?(Class) ? subject : subject.class
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `.conjoins`
|
4
|
+
#
|
5
|
+
# class Foo < ApplicationRecord
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# class FooFleeb < ApplicationFleeb; end
|
9
|
+
#
|
10
|
+
# RSpec.describe Foo, type: :model do
|
11
|
+
# it { is_expected.to conjugate_into FooFleeb }
|
12
|
+
# end
|
13
|
+
|
14
|
+
RSpec::Matchers.define :conjugate_into do |junction|
|
15
|
+
match { |subject| expect(subject.conjugate(junction)).to eq junction }
|
16
|
+
description { "conjugate into #{junction}" }
|
17
|
+
failure_message { |subject| "expected #{subject} to conjugate into #{junction} but didn't" }
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests the presence of `.prototype`
|
4
|
+
#
|
5
|
+
# class ApplicationFleeb
|
6
|
+
# include Conjunction::Junction
|
7
|
+
#
|
8
|
+
# prefixed_with "Fleeb"
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# class GenericFleeb < ApplicationFleeb; end
|
12
|
+
#
|
13
|
+
# RSpec.describe GenericFleeb, type: :fleeb do
|
14
|
+
# it { is_expected.not_to define_prototype }
|
15
|
+
# end
|
16
|
+
|
17
|
+
RSpec::Matchers.define :define_prototype do
|
18
|
+
match { |subject| expect(subject.prototype).not_to be_nil }
|
19
|
+
description { "define prototype" }
|
20
|
+
failure_message { |subject| "expected #{subject} to define prototype but didn't" }
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `.prefixed_with`
|
4
|
+
#
|
5
|
+
# class ApplicationGrodus
|
6
|
+
# include Conjunction::Junction
|
7
|
+
#
|
8
|
+
# prefixed_with "Grodus::"
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# class Grodus::Example < ApplicationGrodus; end
|
12
|
+
#
|
13
|
+
# RSpec.describe Grodus::Example, type: :grodus do
|
14
|
+
# it { is_expected.to have_conjunction_prefix "Grodus::" }
|
15
|
+
# end
|
16
|
+
|
17
|
+
RSpec::Matchers.define :have_conjunction_prefix do |prefix|
|
18
|
+
match { |subject| expect(subject.conjunction_prefix).to eq prefix }
|
19
|
+
description { "have conjunction prefix `#{prefix}'" }
|
20
|
+
failure_message do |subject|
|
21
|
+
"expected #{subject} to have conjunction prefix `#{prefix}' but had `#{subject.conjunction_prefix}'"
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `.suffixed_with`
|
4
|
+
#
|
5
|
+
# class ApplicationLaw < Law::LawBase
|
6
|
+
# include Conjunction::Junction
|
7
|
+
#
|
8
|
+
# suffixed_with "Law"
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# RSpec.describe ApplicationLaw, type: :law do
|
12
|
+
# it { is_expected.to have_conjunction_suffix "Law" }
|
13
|
+
# end
|
14
|
+
|
15
|
+
RSpec::Matchers.define :have_conjunction_suffix do |suffix|
|
16
|
+
match { |subject| expect(subject.conjunction_suffix).to eq suffix }
|
17
|
+
description { "have conjunction suffix `#{suffix}'" }
|
18
|
+
failure_message do |subject|
|
19
|
+
"expected #{subject} to have conjunction suffix `#{suffix}' but had `#{subject.conjunction_prefix}'"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `.suffixed_with`
|
4
|
+
#
|
5
|
+
# class ApplicationDingleBop
|
6
|
+
# include Conjunction::Junction
|
7
|
+
#
|
8
|
+
# suffixed_with "DingleBop"
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# RSpec.describe ApplicationDingleBop, type: :dingle_bop do
|
12
|
+
# it { is_expected.to have_junction_key :dingle_bop }
|
13
|
+
# end
|
14
|
+
|
15
|
+
RSpec::Matchers.define :have_junction_key do |key|
|
16
|
+
match { |subject| expect(subject.junction_key).to eq key }
|
17
|
+
description { "have junction key `#{key}'" }
|
18
|
+
failure_message do |subject|
|
19
|
+
"expected #{subject} to have junction key `#{key}' but had `#{subject.junction_key}'"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests the value of `.prototype`
|
4
|
+
#
|
5
|
+
# class ApplicationFleeb < ActiveRecord::Base
|
6
|
+
# include Conjunction::Junction
|
7
|
+
#
|
8
|
+
# prefixed_with "Fleeb"
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# class FooFleeb < ApplicationFleeb; end
|
12
|
+
#
|
13
|
+
# RSpec.describe FooFleeb, type: :fleeb do
|
14
|
+
# it { is_expected.to have_prototype Foo }
|
15
|
+
# end
|
16
|
+
|
17
|
+
RSpec::Matchers.define :have_prototype do |prototype|
|
18
|
+
match { |subject| expect(subject.prototype).to eq prototype }
|
19
|
+
description { "have prototype name `#{prototype}'" }
|
20
|
+
failure_message { |subject| "expected #{subject} to have prototype `#{prototype}' but had `#{subject.prototype}'" }
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests the value of `.prototype_name`
|
4
|
+
#
|
5
|
+
# class ApplicationRecord < ActiveRecord::Base
|
6
|
+
# include Conjunction::Conjunctive
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# class ShippingAddress < ApplicationRecord; end
|
10
|
+
#
|
11
|
+
# RSpec.describe ShippingAddress, type: :model do
|
12
|
+
# it { is_expected.to have_prototype_name "ShippingAddress" }
|
13
|
+
# end
|
14
|
+
|
15
|
+
RSpec::Matchers.define :have_prototype_name do |prototype_name|
|
16
|
+
match { |subject| expect(subject.prototype_name).to eq prototype_name }
|
17
|
+
description { "have prototype name `#{prototype_name}'" }
|
18
|
+
failure_message do |subject|
|
19
|
+
"expected #{subject} to have prototype name `#{prototype_name}' but had `#{subject.prototype_name}'"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "custom_matchers/be_conjoined_to"
|
4
|
+
require_relative "custom_matchers/be_conjugated_from"
|
5
|
+
require_relative "custom_matchers/conjugate_into"
|
6
|
+
require_relative "custom_matchers/define_prototype"
|
7
|
+
require_relative "custom_matchers/have_conjunction_prefix"
|
8
|
+
require_relative "custom_matchers/have_conjunction_suffix"
|
9
|
+
require_relative "custom_matchers/have_junction_key"
|
10
|
+
require_relative "custom_matchers/have_prototype"
|
11
|
+
require_relative "custom_matchers/have_prototype_name"
|
data/lib/conjunction/version.rb
CHANGED
data/lib/conjunction.rb
CHANGED
@@ -1,6 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_support"
|
4
|
+
|
5
|
+
require "spicerack"
|
6
|
+
|
3
7
|
require "conjunction/version"
|
4
8
|
|
9
|
+
require "conjunction/configuration"
|
10
|
+
|
11
|
+
require "conjunction/nexus"
|
12
|
+
|
13
|
+
require "conjunction/prototype"
|
14
|
+
require "conjunction/conjunctive"
|
15
|
+
require "conjunction/naming_convention"
|
16
|
+
require "conjunction/junction"
|
17
|
+
|
5
18
|
module Conjunction
|
19
|
+
include Spicerack::Configurable::ConfigDelegation
|
20
|
+
delegates_to_configuration
|
21
|
+
|
22
|
+
class Error < StandardError; end
|
23
|
+
class DisjointedError < Error; end
|
6
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: conjunction
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.21.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Garside
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-01-
|
11
|
+
date: 2020-01-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 5.2.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: spicerack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.21.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.21.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activemodel
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 5.2.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 5.2.1
|
27
55
|
description: Join together related concepts for a common purpose with Conjugation
|
28
56
|
email:
|
29
57
|
- garside@gmail.com
|
@@ -35,6 +63,23 @@ files:
|
|
35
63
|
- LICENSE.txt
|
36
64
|
- README.md
|
37
65
|
- lib/conjunction.rb
|
66
|
+
- lib/conjunction/configuration.rb
|
67
|
+
- lib/conjunction/conjunctive.rb
|
68
|
+
- lib/conjunction/junction.rb
|
69
|
+
- lib/conjunction/naming_convention.rb
|
70
|
+
- lib/conjunction/nexus.rb
|
71
|
+
- lib/conjunction/prototype.rb
|
72
|
+
- lib/conjunction/rspec/custom_matchers.rb
|
73
|
+
- lib/conjunction/rspec/custom_matchers/be_conjoined_to.rb
|
74
|
+
- lib/conjunction/rspec/custom_matchers/be_conjugated_from.rb
|
75
|
+
- lib/conjunction/rspec/custom_matchers/conjugate_into.rb
|
76
|
+
- lib/conjunction/rspec/custom_matchers/define_prototype.rb
|
77
|
+
- lib/conjunction/rspec/custom_matchers/have_conjunction_prefix.rb
|
78
|
+
- lib/conjunction/rspec/custom_matchers/have_conjunction_suffix.rb
|
79
|
+
- lib/conjunction/rspec/custom_matchers/have_junction_key.rb
|
80
|
+
- lib/conjunction/rspec/custom_matchers/have_prototype.rb
|
81
|
+
- lib/conjunction/rspec/custom_matchers/have_prototype_name.rb
|
82
|
+
- lib/conjunction/spec_helper.rb
|
38
83
|
- lib/conjunction/version.rb
|
39
84
|
homepage: https://github.com/Freshly/spicerack/tree/master/conjunction
|
40
85
|
licenses:
|
@@ -43,7 +88,7 @@ metadata:
|
|
43
88
|
homepage_uri: https://github.com/Freshly/spicerack/tree/master/conjunction
|
44
89
|
source_code_uri: https://github.com/Freshly/spicerack/tree/master/conjunction
|
45
90
|
changelog_uri: https://github.com/Freshly/spicerack/blob/master/conjunction/CHANGELOG.md
|
46
|
-
documentation_uri: https://www.rubydoc.info/gems/conjunction/0.
|
91
|
+
documentation_uri: https://www.rubydoc.info/gems/conjunction/0.21.0
|
47
92
|
post_install_message:
|
48
93
|
rdoc_options: []
|
49
94
|
require_paths:
|
@@ -59,7 +104,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
104
|
- !ruby/object:Gem::Version
|
60
105
|
version: '0'
|
61
106
|
requirements: []
|
62
|
-
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.7.6
|
63
109
|
signing_key:
|
64
110
|
specification_version: 4
|
65
111
|
summary: Provides a mechanism to loosely coupled a suite of cross-referenced objects
|