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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d86d4d1188c1b5dbaddc82a818e07eb7f4de752315982da7beed7eb1a7415e0
4
- data.tar.gz: 4ec1b035fca482873ae814a3e1d2f69962294155dd4266fe76eadb5d724f23aa
3
+ metadata.gz: 3042afa746b23d3334c26f3e2d535fef1255f16d4d6c5282558e18a329212586
4
+ data.tar.gz: 3bfd916f142dc82e0ce02922b0291fbbf2c1aa3b6e81f5849ba9c7bfb4cc9056
5
5
  SHA512:
6
- metadata.gz: 469459b0353175d294e496d98145402d5f5323981287fc5e8f093c8726276e722db15eacb990ad7709c49057ef01624dab4ceed65c309b62dbf378b584d1791a
7
- data.tar.gz: ff43527d2bcff46488c7513387f88f60e69a813ebbb28d25b6b1993023f45c1d3910c432bc07af23bc4f3de3a1bb1cb4968b2acaa6363c67edbd8c080918693b
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
- # Your code goes here...
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Collectible
4
4
  # This constant is managed by spicerack
5
- VERSION = "0.15.0"
5
+ VERSION = "0.15.1"
6
6
  end
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.0
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: