collectible 0.15.0 → 0.15.1

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: 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: