collectible 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/collectible.rb +14 -1
- data/lib/collectible/collection/core.rb +24 -0
- data/lib/collectible/collection/ensures_item_eligibility.rb +133 -0
- data/lib/collectible/collection/finder.rb +29 -0
- data/lib/collectible/collection/maintain_sort_order.rb +55 -0
- data/lib/collectible/collection/wraps_collection_methods.rb +69 -0
- data/lib/collectible/collection_base.rb +22 -0
- data/lib/collectible/version.rb +1 -1
- metadata +37 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3042afa746b23d3334c26f3e2d535fef1255f16d4d6c5282558e18a329212586
|
|
4
|
+
data.tar.gz: 3bfd916f142dc82e0ce02922b0291fbbf2c1aa3b6e81f5849ba9c7bfb4cc9056
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad7a87d8f7fba77e85faec60f7d73c0d03e462c1219b5926e7705a5c138b7476569263f3bcbbc9743aff626792f402a178fc6cec3a5b94d5450b3f74c7dbe6ed
|
|
7
|
+
data.tar.gz: 9882813663e2579a888c5dd7fcba670072cbf3d1b920ec988dc5bb8586db44767fd591d906c35500c687846222920f56864142af15f0919f927a4acd6f138046
|
data/lib/collectible.rb
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/core_ext/module"
|
|
5
|
+
|
|
6
|
+
require "short_circu_it"
|
|
7
|
+
|
|
8
|
+
require "tablesalt/stringable_object"
|
|
9
|
+
require "tablesalt/uses_hash_for_equality"
|
|
10
|
+
|
|
3
11
|
require "collectible/version"
|
|
4
12
|
|
|
13
|
+
require "collectible/collection_base"
|
|
14
|
+
|
|
5
15
|
module Collectible
|
|
6
|
-
|
|
16
|
+
class ItemNotAllowedError < StandardError; end
|
|
17
|
+
class ItemTypeMismatchError < ItemNotAllowedError; end
|
|
18
|
+
class TypeEnforcementAlreadyDefined < StandardError; end
|
|
19
|
+
class MethodNotAllowedError < StandardError; end
|
|
7
20
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collectible
|
|
4
|
+
module Collection
|
|
5
|
+
module Core
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
attr_reader :items
|
|
10
|
+
|
|
11
|
+
delegate :name, to: :class, prefix: true
|
|
12
|
+
delegate :to_a, :to_ary, :select, :map, :group_by, :partition, :as_json, to: :items
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(*items)
|
|
16
|
+
@items = items.flatten
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def hash
|
|
20
|
+
{ class_name: class_name, items: items }.hash
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collectible
|
|
4
|
+
module Collection
|
|
5
|
+
module EnsuresItemEligibility
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
ENSURE_TYPE_EQUALITY = :ENSURE_TYPE_EQUALITY
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
ensure_item_validity_before :initialize, :push, :<<, :unshift, :insert, :concat, :prepend
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
delegate :item_class, to: :class
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
delegate :ensure_item_validity_with, to: :class
|
|
19
|
+
|
|
20
|
+
# Raises an error if any items fail the class' item validity block/class
|
|
21
|
+
def ensure_allowed_in_collection!(*new_items)
|
|
22
|
+
ensure_type_equality!(*new_items) and return if ensures_type_equality?
|
|
23
|
+
|
|
24
|
+
invalid_items = new_items.flatten.reject { |item| allows_item?(item) }
|
|
25
|
+
return if invalid_items.empty?
|
|
26
|
+
|
|
27
|
+
raise Collectible::ItemNotAllowedError,
|
|
28
|
+
"not allowed: #{invalid_items.first(3).map(&:inspect).join(", ")}#{"..." if invalid_items.length > 3}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param item [*]
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def allows_item?(item)
|
|
34
|
+
if ensure_item_validity_with.respond_to?(:call)
|
|
35
|
+
instance_exec(item, &ensure_item_validity_with)
|
|
36
|
+
elsif ensure_item_validity_with.present?
|
|
37
|
+
item.class <= ensure_item_validity_with
|
|
38
|
+
else
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def ensures_type_equality?
|
|
45
|
+
ensure_item_validity_with == ENSURE_TYPE_EQUALITY
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ensure_type_equality!(*new_items)
|
|
49
|
+
new_items = new_items.flatten
|
|
50
|
+
first_item = items.blank? ? new_items.first : items.first
|
|
51
|
+
|
|
52
|
+
new_items.each do |item|
|
|
53
|
+
next if item.class == first_item.class
|
|
54
|
+
|
|
55
|
+
raise Collectible::ItemTypeMismatchError, "item mismatch: #{first_item.inspect}, #{item.inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class_methods do
|
|
60
|
+
# @return [Class] The class or superclass of all items in the collection
|
|
61
|
+
def item_class
|
|
62
|
+
item_enforcement.is_a?(Class) ? item_enforcement : name.gsub(%r{Collection.*}, "").safe_constantize
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Class, Proc] Either a class that all collection items must belong to,
|
|
66
|
+
# or a proc that validates each item as it is inserted.
|
|
67
|
+
def ensure_item_validity_with
|
|
68
|
+
item_enforcement ||
|
|
69
|
+
(superclass.ensure_item_validity_with if superclass.respond_to?(:ensure_item_validity_with, true))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
# @example
|
|
75
|
+
# # Allows any item that responds to :even? with a truthy value
|
|
76
|
+
# allow_item { |item| item.even? }
|
|
77
|
+
#
|
|
78
|
+
# for block { |item| ... }
|
|
79
|
+
# @yield [*] Yields each inserted item to the provided block. The item will only be allowed into
|
|
80
|
+
# the collection if the block resolves to a truthy value; otherwise, an error is raised.
|
|
81
|
+
# The block shares the context of the collection +instance+, not the class.
|
|
82
|
+
def allow_item(&block)
|
|
83
|
+
raise Collectible::TypeEnforcementAlreadyDefined if item_enforcement.present?
|
|
84
|
+
raise ArgumentError, "must provide a block" unless block_given?
|
|
85
|
+
|
|
86
|
+
self.item_enforcement = block
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @example
|
|
90
|
+
# # Allows any item where item.class <= TargetDate
|
|
91
|
+
# ensures_item_class TargetDate
|
|
92
|
+
#
|
|
93
|
+
# @param klass [Class] A specific class all items are expected to be. Items of this type will be
|
|
94
|
+
# allowed into collection; otherwise, an error is raised.
|
|
95
|
+
def ensures_item_class(klass)
|
|
96
|
+
raise Collectible::TypeEnforcementAlreadyDefined if item_enforcement.present?
|
|
97
|
+
|
|
98
|
+
self.item_enforcement = klass
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Allows any object initially, but any subsequent item must be of the same class
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# collection << Meal.first
|
|
105
|
+
# => #<ApplicationCollection items: [ #<Meal id: 1> ]
|
|
106
|
+
#
|
|
107
|
+
# collection << User.first
|
|
108
|
+
# => Collectible::ItemNotAllowedError: not allowed: #<User id: 1>
|
|
109
|
+
def ensures_type_equality
|
|
110
|
+
raise Collectible::TypeEnforcementAlreadyDefined if item_enforcement.present?
|
|
111
|
+
|
|
112
|
+
self.item_enforcement = ENSURE_TYPE_EQUALITY
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Inserts a check before each of the methods provided to validate inserted objects
|
|
116
|
+
#
|
|
117
|
+
# @param *methods [Array<Symbol>]
|
|
118
|
+
def ensure_item_validity_before(*methods)
|
|
119
|
+
methods.each do |method_name|
|
|
120
|
+
around_method(method_name, prevent_double_wrapping_for: "EnsureItemValidity") do |*items|
|
|
121
|
+
ensure_allowed_in_collection!(items)
|
|
122
|
+
|
|
123
|
+
super(*items)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
attr_internal_accessor :item_enforcement
|
|
129
|
+
private :item_enforcement, :item_enforcement=
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collectible
|
|
4
|
+
module Collection
|
|
5
|
+
module Finder
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
# @param attributes [Hash] A hash of arbitrary attributes to match against the collection
|
|
9
|
+
# @return [*] The first item in the collection that matches the specified attributes
|
|
10
|
+
def find_by(**attributes)
|
|
11
|
+
find do |item|
|
|
12
|
+
attributes.all? do |attribute, value|
|
|
13
|
+
item.public_send(attribute) == value
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param attributes [Hash] A hash of arbitrary attributes to match against the collection
|
|
19
|
+
# @return [ApplicationCollection] A collection of all items in the collection that match the specified attributes
|
|
20
|
+
def where(**attributes)
|
|
21
|
+
select do |item|
|
|
22
|
+
attributes.all? do |attribute, value|
|
|
23
|
+
item.public_send(attribute) == value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collectible
|
|
4
|
+
module Collection
|
|
5
|
+
module MaintainSortOrder
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
delegate :maintain_sort_order?, to: :class
|
|
10
|
+
|
|
11
|
+
disallow_when_sorted :insert, :unshift, :prepend
|
|
12
|
+
maintain_sorted_order_after :initialize, :push, :<<, :concat
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class_methods do
|
|
16
|
+
def inherited(base)
|
|
17
|
+
base.maintain_sort_order if maintain_sort_order?
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def maintain_sort_order?
|
|
22
|
+
@maintain_sort_order.present?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
def maintain_sort_order
|
|
28
|
+
@maintain_sort_order = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def maintain_sorted_order_after(*methods)
|
|
34
|
+
methods.each do |method_name|
|
|
35
|
+
around_method(method_name, prevent_double_wrapping_for: "MaintainSortingOrder") do |*items|
|
|
36
|
+
super(*items).tap do
|
|
37
|
+
sort! if maintain_sort_order?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def disallow_when_sorted(*methods)
|
|
44
|
+
methods.each do |method_name|
|
|
45
|
+
around_method(method_name, prevent_double_wrapping_for: "MaintainSortingOrder") do |*arguments, &block|
|
|
46
|
+
raise Collectible::MethodNotAllowedError, "cannot call #{method_name} when sorted" if maintain_sort_order?
|
|
47
|
+
|
|
48
|
+
super(*arguments, &block)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collectible
|
|
4
|
+
module Collection
|
|
5
|
+
module WrapsCollectionMethods
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
PROXY_MODULE_NAME = "WrapCollectionMethods"
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
collection_wrap_delegate :shift, :pop, :find, :index, :at, :[], :first, :last, :uniq, :uniq!, :sort!,
|
|
12
|
+
:unshift, :insert, :prepend, :push, :<<, :concat, :+, :-,
|
|
13
|
+
:any?, :empty?, :present?, :blank?, :length, :count,
|
|
14
|
+
:each, :reverse_each, :cycle
|
|
15
|
+
|
|
16
|
+
collection_wrap_on :collection_wrap, :select
|
|
17
|
+
collection_wrap_values_on :group_by, :partition
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
|
|
22
|
+
def collection_wrap
|
|
23
|
+
yield
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class_methods do
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def collection_wrap_delegate(*methods)
|
|
30
|
+
methods.each do |method_name|
|
|
31
|
+
next if method_defined?(method_name)
|
|
32
|
+
|
|
33
|
+
define_method(method_name) do |*arguments, &block|
|
|
34
|
+
collection_wrap { items.public_send(method_name, *arguments, &block) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def collection_wrap_on(*methods)
|
|
40
|
+
methods.each do |method_name|
|
|
41
|
+
around_method(method_name, prevent_double_wrapping_for: PROXY_MODULE_NAME) do |*args, &block|
|
|
42
|
+
result = super(*args, &block)
|
|
43
|
+
|
|
44
|
+
return self if result.equal?(items)
|
|
45
|
+
|
|
46
|
+
result.is_a?(Array) ? self.class.new(result) : result
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def collection_wrap_values_on(*methods)
|
|
52
|
+
methods.each do |method_name|
|
|
53
|
+
around_method(method_name, prevent_double_wrapping_for: PROXY_MODULE_NAME) do |*args, &block|
|
|
54
|
+
result = super(*args, &block)
|
|
55
|
+
|
|
56
|
+
if result.respond_to?(:transform_values)
|
|
57
|
+
result.transform_values { |collection| collection_wrap { collection } }
|
|
58
|
+
elsif result.respond_to?(:map)
|
|
59
|
+
result.map { |collection| collection_wrap { collection } }
|
|
60
|
+
else
|
|
61
|
+
collection_wrap { result }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "collection/core"
|
|
4
|
+
require_relative "collection/wraps_collection_methods"
|
|
5
|
+
require_relative "collection/ensures_item_eligibility"
|
|
6
|
+
require_relative "collection/maintain_sort_order"
|
|
7
|
+
require_relative "collection/finder"
|
|
8
|
+
|
|
9
|
+
module Collectible
|
|
10
|
+
class CollectionBase
|
|
11
|
+
include ShortCircuIt
|
|
12
|
+
|
|
13
|
+
include Tablesalt::StringableObject
|
|
14
|
+
include Tablesalt::UsesHashForEquality
|
|
15
|
+
|
|
16
|
+
include Collectible::Collection::Core
|
|
17
|
+
include Collectible::Collection::WrapsCollectionMethods
|
|
18
|
+
include Collectible::Collection::EnsuresItemEligibility
|
|
19
|
+
include Collectible::Collection::MaintainSortOrder
|
|
20
|
+
include Collectible::Collection::Finder
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/collectible/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: collectible
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.15.
|
|
4
|
+
version: 0.15.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Allen Rettberg
|
|
8
|
+
- Eric Garside
|
|
8
9
|
autorequire:
|
|
9
10
|
bindir: bin
|
|
10
11
|
cert_chain: []
|
|
@@ -24,9 +25,38 @@ dependencies:
|
|
|
24
25
|
- - "~>"
|
|
25
26
|
- !ruby/object:Gem::Version
|
|
26
27
|
version: 5.2.1
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: short_circu_it
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - '='
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: 0.15.1
|
|
35
|
+
type: :runtime
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - '='
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: 0.15.1
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: tablesalt
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - '='
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 0.15.1
|
|
49
|
+
type: :runtime
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - '='
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 0.15.1
|
|
27
56
|
description: Perform operations on and pass around explicit collections of objects
|
|
28
57
|
email:
|
|
29
58
|
- allen.rettberg@freshly.com
|
|
59
|
+
- eric.garside@freshly.com
|
|
30
60
|
executables: []
|
|
31
61
|
extensions: []
|
|
32
62
|
extra_rdoc_files: []
|
|
@@ -34,6 +64,12 @@ files:
|
|
|
34
64
|
- LICENSE.txt
|
|
35
65
|
- README.md
|
|
36
66
|
- lib/collectible.rb
|
|
67
|
+
- lib/collectible/collection/core.rb
|
|
68
|
+
- lib/collectible/collection/ensures_item_eligibility.rb
|
|
69
|
+
- lib/collectible/collection/finder.rb
|
|
70
|
+
- lib/collectible/collection/maintain_sort_order.rb
|
|
71
|
+
- lib/collectible/collection/wraps_collection_methods.rb
|
|
72
|
+
- lib/collectible/collection_base.rb
|
|
37
73
|
- lib/collectible/version.rb
|
|
38
74
|
homepage: https://github.com/Freshly/spicerack/tree/master/collectible
|
|
39
75
|
licenses:
|