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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6d5c16ce29ac6addec81af2221485e531d9efffd99f9ec059672063190c84c0
4
- data.tar.gz: a2ce500f63fd7bd00c200e4181f5e92a4371884e0a49b79787ffabbde95186fa
3
+ metadata.gz: 29f3f2cc286036dc191c0e25fb9add5e66641c12c874d96726ac7edaec31ef38
4
+ data.tar.gz: 29d161b6a36913ea34e11e0d7ffd20f98d6144b50cdbad6303344919ee088ab6
5
5
  SHA512:
6
- metadata.gz: 2ae90365cd6d8f2d7d0958da89d7296a674097c9b2554e55373301ea5a82e2019b11291097ce6068f0c31a6ba6a6537e1760f09829bfa273e0e221982722f3f3
7
- data.tar.gz: c2276b3def46f6134c74a3182fb14ed7d305a8f32ab77b92c0bdd7fb8f6c744ae4c4d35642681c93e2dbbbf9ffadb2220646d4617592ee425ece6fa6c466606e
6
+ metadata.gz: c1888c5f4669520ee7028216dfbbb2f944ec20fe2a97e1bfc4ce6d04f1d58c4f0295103fd7e5c67fa7427f79c56be7d4b80ce54f11d258a41d6f5e6a65d669fc
7
+ data.tar.gz: 6d12506c38b16fa38fce51759b0f78f6c611cc45c3cc8453cc0a60bb0526339e930595dd17f0be19a987497a6b23d40368082f5376287e5cf4e4dabc1d6a0f82
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Conjunction
2
2
 
3
- Create cacheable collections of filtered, sorted, and paginated ActiveRecord objects.
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
- * [Usage](#usage)
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
- ## Usage
43
+ ## QuickStart Guide
33
44
 
34
- TODO: Write usage instructions here
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"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rspec/custom_matchers"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Conjunction
4
4
  # This constant is managed by spicerack
5
- VERSION = "0.20.2"
5
+ VERSION = "0.21.0"
6
6
  end
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.20.2
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-06 00:00:00.000000000 Z
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.20.2
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
- rubygems_version: 3.0.3
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