graphql-filters 1.0.0

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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +1635 -0
  4. data/CHANGELOG.md +13 -0
  5. data/Gemfile +10 -0
  6. data/Gemfile.lock +77 -0
  7. data/LICENSE +21 -0
  8. data/README.md +265 -0
  9. data/Rakefile +10 -0
  10. data/lib/graphql/filters/activerecord_patch/arel/nodes/contained.rb +9 -0
  11. data/lib/graphql/filters/activerecord_patch/arel/predications.rb +9 -0
  12. data/lib/graphql/filters/activerecord_patch.rb +3 -0
  13. data/lib/graphql/filters/dsl/graphql/schema/enum.rb +16 -0
  14. data/lib/graphql/filters/dsl/graphql/schema/field.rb +64 -0
  15. data/lib/graphql/filters/dsl/graphql/schema/list.rb +28 -0
  16. data/lib/graphql/filters/dsl/graphql/schema/member.rb +25 -0
  17. data/lib/graphql/filters/dsl/graphql/schema/non_null.rb +15 -0
  18. data/lib/graphql/filters/dsl/graphql/schema/object.rb +16 -0
  19. data/lib/graphql/filters/dsl/graphql/schema/scalar.rb +16 -0
  20. data/lib/graphql/filters/dsl/graphql/types/numeric.rb +20 -0
  21. data/lib/graphql/filters/dsl/graphql/types/string.rb +16 -0
  22. data/lib/graphql/filters/dsl.rb +3 -0
  23. data/lib/graphql/filters/filterable.rb +42 -0
  24. data/lib/graphql/filters/input_types/base_comparison_input_type.rb +12 -0
  25. data/lib/graphql/filters/input_types/base_list_comparison_input_type.rb +23 -0
  26. data/lib/graphql/filters/input_types/base_scalar_comparison_input_type.rb +63 -0
  27. data/lib/graphql/filters/input_types/fields_comparison_input_type.rb +58 -0
  28. data/lib/graphql/filters/input_types/list_object_comparison_input_type.rb +97 -0
  29. data/lib/graphql/filters/input_types/list_scalar_comparison_input_type.rb +134 -0
  30. data/lib/graphql/filters/input_types/numeric_comparison_input_type.rb +45 -0
  31. data/lib/graphql/filters/input_types/object_comparison_input_type.rb +71 -0
  32. data/lib/graphql/filters/input_types/string_comparison_input_type.rb +64 -0
  33. data/lib/graphql/filters/utility/cached_class.rb +74 -0
  34. data/lib/graphql/filters/version.rb +5 -0
  35. data/lib/graphql/filters.rb +26 -0
  36. data/lib/graphql/models_connect/dsl/graphql/schema/object.rb +27 -0
  37. data/lib/graphql/models_connect/dsl.rb +3 -0
  38. data/lib/graphql/models_connect.rb +15 -0
  39. metadata +126 -0
@@ -0,0 +1,42 @@
1
+ module GraphQL
2
+ module Filters
3
+ module Filterable
4
+ extend ActiveSupport::Concern
5
+
6
+ # We need the two inner_type chunks of code because type.list? is true if any of the types in the of_type chain
7
+ # is a list, but type.unwrap returns the raw type. I want the filter to be based on the of_type of the list,
8
+ # which could be the unwrapped type, but it could also be a non nullable version of that type. The non
9
+ # nullability is relevant.
10
+
11
+ included do
12
+ raise 'You can only apply a filter to a list field' unless type.list?
13
+
14
+ unless defined?(SearchObject::Base) && include?(SearchObject::Base)
15
+ raise 'If you don\'t use SearchObject, you must *prepend* Filterable, not *include* it.'
16
+ end
17
+
18
+ inner_type = type
19
+ inner_type = type.of_type until inner_type.kind == GraphQL::TypeKinds::LIST
20
+ inner_type = inner_type.of_type
21
+
22
+ option :filter, type: inner_type.comparison_input_type do |scope, filter|
23
+ filter.call scope
24
+ end
25
+ end
26
+
27
+ prepended do
28
+ raise 'You can only apply a filter to a list field' unless type.list?
29
+
30
+ inner_type = type
31
+ inner_type = type.of_type while inner_type.kind == GraphQL::TypeKinds::LIST
32
+ inner_type = inner_type.of_type
33
+
34
+ argument :filter, inner_type.comparison_input_type, required: false
35
+
36
+ def resolve filter: nil, **kwargs
37
+ filter.call super(**kwargs)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ module GraphQL
2
+ module Filters
3
+ module InputTypes
4
+ class BaseComparisonInputType < Filters.base_input_object_class
5
+ def self.argument *args, **kwargs, &block
6
+ kwargs[:required] = false
7
+ super(*args, **kwargs, &block)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'base_comparison_input_type'
2
+
3
+ module GraphQL
4
+ module Filters
5
+ module InputTypes
6
+ class BaseListComparisonInputType
7
+ include CachedClass
8
+
9
+ resolve_cache_miss do |value_type, klass|
10
+ klass.new BaseComparisonInputType do
11
+ graphql_name "#{value_type.unwrap.graphql_name}ListComparisonInput"
12
+
13
+ one_of
14
+
15
+ def prepare
16
+ values.sole
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ require_relative 'base_comparison_input_type'
2
+
3
+ module GraphQL
4
+ module Filters
5
+ module InputTypes
6
+ class BaseScalarComparisonInputType
7
+ include CachedClass
8
+
9
+ resolve_cache_miss do |value_type, klass|
10
+ klass.new BaseComparisonInputType do
11
+ graphql_name "#{value_type.graphql_name}ComparisonInput"
12
+
13
+ one_of
14
+
15
+ argument :constant,
16
+ Types::Boolean,
17
+ prepare: lambda { |value, _context|
18
+ lambda { |scope, _column_name|
19
+ if value
20
+ scope.all
21
+ else
22
+ scope.where false
23
+ end
24
+ }
25
+ }
26
+ argument :equals,
27
+ value_type,
28
+ prepare: lambda { |value, _context|
29
+ lambda { |scope, column_name|
30
+ scope.where(column_name => value)
31
+ }
32
+ }
33
+ argument :not_equals,
34
+ value_type,
35
+ prepare: lambda { |value, _context|
36
+ lambda { |scope, column_name|
37
+ scope.where.not(column_name => value)
38
+ }
39
+ }
40
+ argument :in,
41
+ [value_type],
42
+ prepare: lambda { |value, _context|
43
+ lambda { |scope, column_name|
44
+ scope.where(column_name => value)
45
+ }
46
+ }
47
+ argument :not_in,
48
+ [value_type],
49
+ prepare: lambda { |value, _context|
50
+ lambda { |scope, column_name|
51
+ scope.where.not(column_name => value)
52
+ }
53
+ }
54
+
55
+ def prepare
56
+ values.sole
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ require_relative 'base_comparison_input_type'
2
+
3
+ module GraphQL
4
+ module Filters
5
+ module InputTypes
6
+ class FieldsComparisonInputType
7
+ include CachedClass
8
+
9
+ resolve_cache_miss do |object_type, klass|
10
+ klass.new BaseComparisonInputType do
11
+ graphql_name "#{object_type.graphql_name}ComparisonInput"
12
+
13
+ object_type.fields.each_value do |field_object|
14
+ next unless field_object.filter_options[:enabled]
15
+
16
+ type = field_object.type
17
+ filter_options = field_object.filter_options
18
+
19
+ argument field_object.name,
20
+ type.comparison_input_type,
21
+ required: false,
22
+ prepare: lambda { |field_comparator, _context|
23
+ lambda { |scope|
24
+ if scope.klass.attribute_names.include? filter_options[:attribute_name]
25
+ field_comparator.call scope, filter_options[:attribute_name]
26
+ else
27
+ field_comparator.call scope, filter_options[:association_name]
28
+ end
29
+ }
30
+ }
31
+ end
32
+
33
+ define_method :prepare do
34
+ lambda do |scope, association_name=nil|
35
+ model_class = object_type.model_class
36
+
37
+ nested_query = values.reduce model_class.all do |acc, val|
38
+ val.call acc
39
+ end
40
+
41
+ if association_name.nil?
42
+ unless scope.structurally_compatible? nested_query
43
+ raise "the produced nested query on model #{model_class}"\
44
+ 'is not compatible with the provided scope'
45
+ end
46
+
47
+ scope.and nested_query
48
+ else
49
+ scope.where association_name => nested_query
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,97 @@
1
+ require_relative 'base_list_comparison_input_type'
2
+
3
+ module GraphQL
4
+ module Filters
5
+ module InputTypes
6
+ class ListObjectComparisonInputType
7
+ include CachedClass
8
+
9
+ resolve_cache_miss do |value_type, klass|
10
+ klass.new BaseListComparisonInputType[value_type] do
11
+ raw_type = value_type.unwrap
12
+
13
+ argument :constant,
14
+ Types::Boolean,
15
+ prepare: lambda { |value, _context|
16
+ lambda { |scope, _column_name=nil|
17
+ if value
18
+ scope.all
19
+ else
20
+ scope.where false
21
+ end
22
+ }
23
+ }
24
+ argument :any,
25
+ value_type.of_type.comparison_input_type,
26
+ prepare: lambda { |field_comparator, _context|
27
+ lambda { |scope, association_name|
28
+ subquery = field_comparator.call raw_type.model_class.all
29
+
30
+ reflection = scope.reflect_on_association association_name
31
+
32
+ finished_subquery = apply_reflection_to_subquery reflection, scope, subquery
33
+
34
+ scope.where finished_subquery.arel.exists
35
+ }
36
+ }
37
+ argument :all,
38
+ value_type.of_type.comparison_input_type,
39
+ prepare: lambda { |field_comparator, _context|
40
+ lambda { |scope, association_name|
41
+ subquery = field_comparator.call raw_type.model_class.all
42
+
43
+ reflection = scope.reflect_on_association association_name
44
+
45
+ finished_subquery = apply_reflection_to_subquery reflection, scope, subquery.invert_where
46
+
47
+ scope.where.not finished_subquery.arel.exists
48
+ }
49
+ }
50
+ argument :none,
51
+ value_type.of_type.comparison_input_type,
52
+ prepare: lambda { |field_comparator, _context|
53
+ lambda { |scope, association_name|
54
+ subquery = field_comparator.call raw_type.model_class.all
55
+
56
+ reflection = scope.reflect_on_association association_name
57
+
58
+ finished_subquery = apply_reflection_to_subquery reflection, scope, subquery
59
+
60
+ scope.where.not finished_subquery.arel.exists
61
+ }
62
+ }
63
+
64
+ class << self
65
+ private
66
+
67
+ def apply_reflection_to_subquery reflection, scope, subquery
68
+ unless reflection.is_a? ActiveRecord::Reflection::AbstractReflection
69
+ raise 'Lists of objects are only supported through associations at the moment'
70
+ end
71
+
72
+ scope_klass = scope.klass
73
+ scope_table = scope.table
74
+ scope_aliased_table = scope_table.alias "other_#{scope_table.name}"
75
+
76
+ table_metadata = ActiveRecord::TableMetadata.new scope_klass, scope_aliased_table
77
+ predicate_builder = ActiveRecord::PredicateBuilder.new table_metadata
78
+ aliased_relation = ActiveRecord::Relation.new scope_klass,
79
+ table: scope_aliased_table,
80
+ predicate_builder: predicate_builder
81
+
82
+ aliased_primary_key_node = scope_aliased_table[scope.primary_key]
83
+ scope_primary_key_node = scope_table[scope.primary_key]
84
+
85
+ aliased_relation
86
+ .joins(reflection.name)
87
+ .merge(subquery)
88
+ .where(aliased_primary_key_node.eq(scope_primary_key_node))
89
+ .select(:id) # Improves performance
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,134 @@
1
+ require_relative 'base_list_comparison_input_type'
2
+
3
+ module GraphQL
4
+ module Filters
5
+ module InputTypes
6
+ class ListScalarComparisonInputType
7
+ include CachedClass
8
+
9
+ resolve_cache_miss do |value_type, klass|
10
+ klass.new BaseListComparisonInputType[value_type] do
11
+ argument :equals,
12
+ value_type,
13
+ prepare: lambda { |value, _context|
14
+ lambda { |scope, column_name|
15
+ scope.where(column_name => value)
16
+ }
17
+ }
18
+ argument :not_equals,
19
+ value_type,
20
+ prepare: lambda { |value, _context|
21
+ lambda { |scope, column_name|
22
+ scope.where.not(column_name => value)
23
+ }
24
+ }
25
+ argument :in,
26
+ [value_type],
27
+ prepare: lambda { |value, _context|
28
+ lambda { |scope, column_name|
29
+ scope.where(column_name => value)
30
+ }
31
+ }
32
+ argument :not_in,
33
+ [value_type],
34
+ prepare: lambda { |value, _context|
35
+ lambda { |scope, column_name|
36
+ scope.where.not(column_name => value)
37
+ }
38
+ }
39
+ argument :subset_of,
40
+ value_type,
41
+ prepare: lambda { |value, _context|
42
+ lambda { |scope, column_name|
43
+ table = scope.table[column_name]
44
+ attribute = scope.predicate_builder.build_bind_attribute column_name, value
45
+ scope.where table.contained(attribute)
46
+ }
47
+ }
48
+ argument :not_subset_of,
49
+ value_type,
50
+ prepare: lambda { |value, _context|
51
+ lambda { |scope, column_name|
52
+ table = scope.table[column_name]
53
+ attribute = scope.predicate_builder.build_bind_attribute column_name, value
54
+ scope.where.not table.contained(attribute)
55
+ }
56
+ }
57
+ argument :superset_of,
58
+ value_type,
59
+ prepare: lambda { |value, _context|
60
+ lambda { |scope, column_name|
61
+ table = scope.table[column_name]
62
+ attribute = scope.predicate_builder.build_bind_attribute column_name, value
63
+ scope.where table.contains(attribute)
64
+ }
65
+ }
66
+ argument :not_superset_of,
67
+ value_type,
68
+ prepare: lambda { |value, _context|
69
+ lambda { |scope, column_name|
70
+ table = scope.table[column_name]
71
+ attribute = scope.predicate_builder.build_bind_attribute column_name, value
72
+ scope.where.not table.contains(attribute)
73
+ }
74
+ }
75
+ argument :any,
76
+ value_type.of_type.comparison_input_type,
77
+ prepare: lambda { |field_comparator, _context|
78
+ lambda { |scope, column_name|
79
+ sub_scope = create_sub_scope scope, column_name, field_comparator
80
+ scope.where sub_scope.arel.exists
81
+ }
82
+ }
83
+ argument :all,
84
+ value_type.of_type.comparison_input_type,
85
+ prepare: lambda { |field_comparator, _context|
86
+ lambda { |scope, column_name|
87
+ sub_scope = create_sub_scope scope, column_name, field_comparator
88
+ scope.where.not sub_scope.invert_where.arel.exists
89
+ }
90
+ }
91
+ argument :none,
92
+ value_type.of_type.comparison_input_type,
93
+ prepare: lambda { |field_comparator, _context|
94
+ lambda { |scope, column_name|
95
+ sub_scope = create_sub_scope scope, column_name, field_comparator
96
+ scope.where.not sub_scope.arel.exists
97
+ }
98
+ }
99
+
100
+ class << self
101
+ private
102
+
103
+ def create_sub_scope scope, column_name, field_comparator
104
+ type_caster = Object.new
105
+
106
+ type_caster.define_singleton_method :type_for_attribute do |_name|
107
+ scope.klass.type_for_attribute(column_name).subtype
108
+ end
109
+
110
+ type_caster.define_singleton_method :type_cast_for_database do |attr_name, value|
111
+ type = type_for_attribute attr_name
112
+ type.serialize value
113
+ end
114
+
115
+ table = Arel::Table.new 'temp_table', type_caster: type_caster
116
+ predicate_builder = ActiveRecord::PredicateBuilder.new ActiveRecord::TableMetadata.new(
117
+ ActiveRecord::Base, table
118
+ )
119
+ temp_relation = ActiveRecord::Relation.new(ActiveRecord::Base,
120
+ table: table,
121
+ predicate_builder: predicate_builder)
122
+ .from(Arel::Nodes::NamedFunction.new 'UNNEST',
123
+ [scope.arel_table[column_name]],
124
+ 'temp_table(value)')
125
+
126
+ field_comparator.call temp_relation, 'value'
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,45 @@
1
+ require_relative 'base_scalar_comparison_input_type'
2
+ require 'arel'
3
+
4
+ module GraphQL
5
+ module Filters
6
+ module InputTypes
7
+ class NumericComparisonInputType
8
+ include CachedClass
9
+
10
+ resolve_cache_miss do |value_type, klass|
11
+ klass.new BaseScalarComparisonInputType[value_type] do
12
+ argument :greater_than,
13
+ value_type,
14
+ prepare: lambda { |value, _context|
15
+ lambda { |scope, column_name|
16
+ scope.where.not(column_name => (..value))
17
+ }
18
+ }
19
+ argument :greater_than_or_equals_to,
20
+ value_type,
21
+ prepare: lambda { |value, _context|
22
+ lambda { |scope, column_name|
23
+ scope.where(column_name => (value..))
24
+ }
25
+ }
26
+ argument :less_than,
27
+ value_type,
28
+ prepare: lambda { |value, _context|
29
+ lambda { |scope, column_name|
30
+ scope.where(column_name => (...value))
31
+ }
32
+ }
33
+ argument :less_than_or_equals_to,
34
+ value_type,
35
+ prepare: lambda { |value, _context|
36
+ lambda { |scope, column_name|
37
+ scope.where(column_name => (..value))
38
+ }
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ require_relative 'base_comparison_input_type'
2
+ require_relative 'fields_comparison_input_type'
3
+
4
+ module GraphQL
5
+ module Filters
6
+ module InputTypes
7
+ class ObjectComparisonInputType
8
+ include CachedClass
9
+
10
+ resolve_cache_miss do |value_type, klass|
11
+ klass.new BaseComparisonInputType do
12
+ graphql_name "#{value_type.graphql_name}ComplexFilterInput"
13
+
14
+ fields_comparison_input_type = FieldsComparisonInputType[value_type]
15
+
16
+ define_singleton_method :fields_comparison_input_type do
17
+ fields_comparison_input_type
18
+ end
19
+
20
+ one_of
21
+
22
+ argument :constant,
23
+ Types::Boolean,
24
+ prepare: lambda { |value, _context|
25
+ lambda { |scope, _association_name=nil|
26
+ if value
27
+ scope.all
28
+ else
29
+ scope.where false
30
+ end
31
+ }
32
+ }
33
+ argument :and,
34
+ [self],
35
+ required: false,
36
+ prepare: lambda { |and_arg, _context|
37
+ lambda { |scope, association_name=nil|
38
+ and_arg.reduce scope do |acc, val|
39
+ acc.and val.call(scope, association_name)
40
+ end
41
+ }
42
+ }
43
+ argument :or,
44
+ [self],
45
+ required: false,
46
+ prepare: lambda { |or_arg, _context|
47
+ lambda { |scope, association_name=nil|
48
+ or_arg.reduce scope.none do |acc, val|
49
+ acc.or val.call(scope, association_name)
50
+ end
51
+ }
52
+ }
53
+ argument :not,
54
+ self,
55
+ required: false,
56
+ prepare: lambda { |not_arg, _context|
57
+ lambda { |scope, association_name=nil|
58
+ scope.and(not_arg.call(scope.unscope(:where), association_name).invert_where)
59
+ }
60
+ }
61
+ argument :fields, fields_comparison_input_type, required: false
62
+
63
+ def prepare
64
+ values.sole
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,64 @@
1
+ require_relative 'base_scalar_comparison_input_type'
2
+
3
+ module GraphQL
4
+ module Filters
5
+ module InputTypes
6
+ class StringComparisonInputType < BaseScalarComparisonInputType[GraphQL::Types::String]
7
+ argument :match,
8
+ String,
9
+ prepare: lambda { |value, _context|
10
+ lambda { |scope, column_name|
11
+ column_node = scope.table[column_name]
12
+ scope.where resolve_pattern(column_node, value)
13
+ }
14
+ }
15
+
16
+ class << self
17
+ alias inspect to_s
18
+
19
+ private
20
+
21
+ def resolve_pattern column_node, expression
22
+ expression.match %r{v(?<version>\d)+/(?<full_pattern>.*)} do |match_data|
23
+ raise 'The only supported version of pattern is v1' if match_data[:version].to_i != 1
24
+
25
+ resolve_v1 column_node, match_data[:full_pattern]
26
+ end
27
+ end
28
+
29
+ def resolve_v1 column_node, full_pattern
30
+ full_pattern.match %r{(?<pattern>.*?)/(?<options>.*)} do |match_data|
31
+ options = match_data[:options].chars.map(&:to_sym)
32
+
33
+ if options.present? && options != [:i]
34
+ raise 'The only supported option is \'i\' for case insensitive matching'
35
+ end
36
+
37
+ case_sensitive = !options.include?(:i)
38
+
39
+ characters = match_data[:pattern].chars
40
+ pattern = ''
41
+
42
+ while characters.present?
43
+ char = characters.shift
44
+
45
+ pattern << case char
46
+ when '\\'
47
+ ActiveRecord::Base.sanitize_sql_like characters.shift
48
+ when '*'
49
+ '%'
50
+ when '.'
51
+ '_'
52
+ else
53
+ ActiveRecord::Base.sanitize_sql_like char
54
+ end
55
+ end
56
+
57
+ column_node.matches pattern, nil, case_sensitive
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,74 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'active_support/core_ext/class/attribute'
4
+
5
+ module GraphQL
6
+ module Filters
7
+ module Utility
8
+ module CachedClass
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ next unless is_a? Class
13
+
14
+ class_attribute :cache,
15
+ default: Hash.new { |h, k|
16
+ h[k] = Class.new
17
+ },
18
+ instance_accessor: false,
19
+ instance_predicate: false
20
+
21
+ singleton_class.delegate :[], :[]=, to: :cache
22
+ singleton_class.undef_method :new
23
+ end
24
+
25
+ class CacheClassConstructor
26
+ attr_reader :key
27
+ attr_reader :owner
28
+
29
+ delegate_missing_to :owner
30
+
31
+ def initialize key, owner
32
+ @key = key
33
+ @owner = owner
34
+ end
35
+
36
+ def new base_class, &block
37
+ klass = Class.new base_class
38
+ owner[key] = klass
39
+
40
+ outer_self = self
41
+
42
+ klass.define_singleton_method :inspect do
43
+ "#{outer_self.owner}<#{outer_self.key}>"
44
+ end
45
+
46
+ klass.class_exec(&block)
47
+ klass
48
+ end
49
+ end
50
+ private_constant :CacheClassConstructor
51
+
52
+ class_methods do
53
+ # Pass this method the block that will be called on a cache miss. The block will receive the `key` that caused
54
+ # the miss, and a class constructor `klass`. Use `klass.new` instead of `Class.new` to create the class that
55
+ # you want to associate with `key` in the cache. This prevents loops in the case you reference the same key
56
+ # during the definition of the class. Notice that the class is first created with `Class.new`, then it is
57
+ # associated with `key` in the cache, and only then it is properly defined using `class_exec` and the block
58
+ # you passed to `new`, so be careful about what assumptions you make on the classes you pull from the cache
59
+ # during a cache miss resolution.
60
+ def resolve_cache_miss &block
61
+ cache.default_proc = proc do |h, k|
62
+ class_constructor = CacheClassConstructor.new k, self
63
+ result = block.call k, class_constructor
64
+
65
+ h[k] = result unless h.key? k
66
+
67
+ result
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end