wcc-contentful 1.1.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f99e076627c9facfff117f6d9df87dcf593d8a07bb6d3351b159172207aff81a
4
- data.tar.gz: 463c6b34cbf4a5718736e04365a9705795fd1f3435167cc1028d5c217596fb34
3
+ metadata.gz: f05ea585d54f7ca83c96e018021f88776ebb148cc40b7d719d6199b3a3cd9d0c
4
+ data.tar.gz: 0da490d12beb9938ec51c0ac40e140420966667b72afebc5c9895cf197308ea8
5
5
  SHA512:
6
- metadata.gz: 4be52c0f698f59b61f91d547ae7a2588b07bc9d184cd09a78730f7a09b290d005770b4f1ab3306d7e9cbc9ca0e5a20c7dc3c63fa39417a7c3219bf4c28ecd8a8
7
- data.tar.gz: a1e93027a2a81655f9dbe82ee54c4f77061ef757c2dee9e3fe12369ce9f8d698fd01a7ab9a2510244273f567dbbdb05084eeb3b333abcafce9ccb05c6e62c4bc
6
+ metadata.gz: 979360a35356b041dbe0f8bbd4f0e5bfcaf91909ccc567322ceef8493de25300ea09a081e755142fe23bf9cfd5ea86ca7f919d3218d5dec44a98bcab5921af7d
7
+ data.tar.gz: 9e5f29e70fdcbccffd72aeb8b896e3518d2236cd699704289e8a3fdf1392201dc49671bab0eb83948e79ee079366de90b46add4ce2027e065db2da28b2be4e85
@@ -11,20 +11,19 @@ module WCC::Contentful
11
11
  args = default_configuration.merge!(args)
12
12
 
13
13
  client = WCC::Contentful::SimpleClient::Management.new(
14
- args
14
+ **args
15
15
  )
16
- enable_webhook(client, args.slice(:app_url, :webhook_username, :webhook_password))
16
+ enable_webhook(client, args.slice(:receive_url, :webhook_username, :webhook_password))
17
17
  end
18
18
 
19
- def enable_webhook(client, app_url:, webhook_username: nil, webhook_password: nil)
20
- expected_url = URI.join(app_url, 'webhook/receive').to_s
21
- webhook = client.webhook_definitions.items.find { |w| w['url'] == expected_url }
19
+ def enable_webhook(client, receive_url:, webhook_username: nil, webhook_password: nil)
20
+ webhook = client.webhook_definitions.items.find { |w| w['url'] == receive_url }
22
21
  logger.debug "existing webhook: #{webhook.inspect}" if webhook
23
22
  return if webhook
24
23
 
25
24
  body = {
26
25
  'name' => 'WCC::Contentful webhook',
27
- 'url' => expected_url,
26
+ 'url' => receive_url,
28
27
  'topics' => [
29
28
  '*.publish',
30
29
  '*.unpublish'
@@ -50,13 +49,14 @@ module WCC::Contentful
50
49
 
51
50
  {
52
51
  management_token: config.management_token,
53
- app_url: config.app_url,
54
52
  space: config.space,
55
53
  environment: config.environment,
56
54
  default_locale: config.default_locale,
57
55
  connection: config.connection,
58
56
  webhook_username: config.webhook_username,
59
- webhook_password: config.webhook_password
57
+ webhook_password: config.webhook_password,
58
+
59
+ receive_url: URI.join(config.app_url, 'webhook/receive').to_s
60
60
  }
61
61
  end
62
62
 
@@ -64,13 +64,13 @@ module WCC::Contentful::ActiveRecordShim
64
64
  }
65
65
  }
66
66
 
67
- find_all(filter).each_slice(batch_size, &block)
67
+ find_all(**filter).each_slice(batch_size, &block)
68
68
  end
69
69
 
70
70
  def where(**conditions)
71
71
  # TODO: return a Query object that implements more of the ActiveRecord query interface
72
72
  # https://guides.rubyonrails.org/active_record_querying.html#conditions
73
- find_all(conditions)
73
+ find_all(**conditions)
74
74
  end
75
75
  end
76
76
  end
@@ -137,6 +137,8 @@ module WCC::Contentful
137
137
  :Json
138
138
  when 'Location'
139
139
  :Coordinates
140
+ when 'RichText'
141
+ :RichText
140
142
  when 'Array'
141
143
  find_field_type(field.try(:items) || field['items'])
142
144
  when 'Link'
@@ -7,10 +7,10 @@ module WCC::Contentful::Event
7
7
 
8
8
  # Creates an Event out of a raw value received by a webhook or given from
9
9
  # the Contentful Sync API.
10
- def self.from_raw(raw, context = nil)
10
+ def self.from_raw(raw, context = nil, source: nil)
11
11
  const = Registry.instance.get(raw.dig('sys', 'type'))
12
12
 
13
- const.new(raw, context)
13
+ const.new(raw, context, source: source)
14
14
  end
15
15
 
16
16
  class Registry
@@ -134,12 +134,6 @@ class WCC::Contentful::Event::SyncComplete
134
134
  include WCC::Contentful::Event
135
135
 
136
136
  def initialize(items, context = nil, source: nil)
137
- items =
138
- items.map do |item|
139
- next item if item.is_a? WCC::Contentful::Event
140
-
141
- WCC::Contentful::Event.from_raw(item, context, source: source)
142
- end
143
137
  @items = items.freeze
144
138
  @source = source
145
139
  @sys = WCC::Contentful::Sys.new(
@@ -103,6 +103,7 @@ module WCC::Contentful
103
103
  Boolean
104
104
  Json
105
105
  Coordinates
106
+ RichText
106
107
  Link
107
108
  Asset
108
109
  ].freeze
@@ -20,7 +20,7 @@ module WCC::Contentful::Middleware::Store
20
20
  event = 'miss'
21
21
  # if it's not a contentful ID don't hit the API.
22
22
  # Store a nil object if we can't find the object on the CDN.
23
- (store.find(key, options) || nil_obj(key)) if key =~ /^\w+$/
23
+ (store.find(key, **options) || nil_obj(key)) if key =~ /^\w+$/
24
24
  end
25
25
  _instrument(event, key: key, options: options)
26
26
 
@@ -41,10 +41,6 @@ require_relative './model_api'
41
41
  class WCC::Contentful::Model
42
42
  include WCC::Contentful::ModelAPI
43
43
 
44
- # The Model base class maintains a registry which is best expressed as a
45
- # class var.
46
- # rubocop:disable Style/ClassVars
47
-
48
44
  class << self
49
45
  def const_missing(name)
50
46
  type = WCC::Contentful::Helpers.content_type_from_constant(name)
@@ -53,5 +49,3 @@ class WCC::Contentful::Model
53
49
  end
54
50
  end
55
51
  end
56
-
57
- # rubocop:enable Style/ClassVars
@@ -13,9 +13,8 @@ module WCC::Contentful::ModelAPI
13
13
  services.instrumentation
14
14
  end
15
15
 
16
- # We use a class var here because this is a global registry for all subclasses
17
- # of this namespace
18
- @@registry = {} # rubocop:disable Style/ClassVars
16
+ # Set the registry at the top of the namespace
17
+ @registry = {}
19
18
  end
20
19
 
21
20
  class_methods do
@@ -69,7 +68,7 @@ module WCC::Contentful::ModelAPI
69
68
  store = options[:preview] ? services.preview_store : services.store
70
69
  raw =
71
70
  _instrumentation.instrument 'find.model.contentful.wcc', id: id, options: options do
72
- store.find(id, options.except(*WCC::Contentful::ModelMethods::MODEL_LAYER_CONTEXT_KEYS))
71
+ store.find(id, **options.except(*WCC::Contentful::ModelMethods::MODEL_LAYER_CONTEXT_KEYS))
73
72
  end
74
73
 
75
74
  new_from_raw(raw, options) if raw.present?
@@ -89,7 +88,7 @@ module WCC::Contentful::ModelAPI
89
88
  def resolve_constant(content_type)
90
89
  raise ArgumentError, 'content_type cannot be nil' unless content_type
91
90
 
92
- const = @@registry[content_type]
91
+ const = _registry[content_type]
93
92
  return const if const
94
93
 
95
94
  const_name = WCC::Contentful::Helpers.constant_from_content_type(content_type).to_s
@@ -143,14 +142,14 @@ module WCC::Contentful::ModelAPI
143
142
 
144
143
  content_type ||= WCC::Contentful::Helpers.content_type_from_constant(klass)
145
144
 
146
- @@registry[content_type] = klass
145
+ _registry[content_type] = klass
147
146
  end
148
147
 
149
148
  # Returns the current registry of content type names to constants.
150
149
  def registry
151
- return {} unless @@registry
150
+ return {} unless _registry
152
151
 
153
- @@registry.dup.freeze
152
+ _registry.dup.freeze
154
153
  end
155
154
 
156
155
  def reload!
@@ -176,7 +175,15 @@ module WCC::Contentful::ModelAPI
176
175
  # that class. If nil, the generated WCC::Contentful::Model::{content_type} class
177
176
  # will be resolved for this content type.
178
177
  def registered?(content_type)
179
- @@registry[content_type]
178
+ _registry[content_type]
179
+ end
180
+
181
+ protected
182
+
183
+ def _registry
184
+ # If calling register_for_content_type in a subclass, look up the superclass
185
+ # chain until we get to the model namespace which defines the registry
186
+ @registry || superclass._registry
180
187
  end
181
188
  end
182
189
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative './link'
4
4
  require_relative './sys'
5
+ require_relative './rich_text'
5
6
 
6
7
  module WCC::Contentful
7
8
  class ModelBuilder
@@ -95,6 +96,8 @@ module WCC::Contentful
95
96
  #
96
97
  # when :DateTime
97
98
  # raw_value = Time.parse(raw_value).localtime
99
+ when :RichText
100
+ raw_value = WCC::Contentful::RichText.tokenize(raw_value)
98
101
  when :Int
99
102
  raw_value = Integer(raw_value)
100
103
  when :Float
@@ -16,7 +16,7 @@ module WCC::Contentful::ModelSingletonMethods
16
16
  raw =
17
17
  _instrumentation.instrument 'find.model.contentful.wcc',
18
18
  content_type: content_type, id: id, options: options do
19
- store.find(id, { hint: type }.merge!(options.except(:preview)))
19
+ store.find(id, **{ hint: type }.merge!(options.except(:preview)))
20
20
  end
21
21
  new(raw, options) if raw.present?
22
22
  end
@@ -69,9 +69,9 @@ module WCC::Contentful::ModelSingletonMethods
69
69
  def inherited(subclass)
70
70
  # If another different class is already registered for this content type,
71
71
  # don't auto-register this one.
72
- return if WCC::Contentful::Model.registered?(content_type)
72
+ return if model_namespace.registered?(content_type)
73
73
 
74
- WCC::Contentful::Model.register_for_content_type(content_type, klass: subclass)
74
+ model_namespace.register_for_content_type(content_type, klass: subclass)
75
75
  end
76
76
 
77
77
  class ModelQuery
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful::RichText
4
+ module Node
5
+ extend ActiveSupport::Concern
6
+
7
+ def keys
8
+ members.map(&:to_s)
9
+ end
10
+
11
+ included do
12
+ include Enumerable
13
+
14
+ alias_method :node_type, :nodeType
15
+
16
+ # Make the nodes read-only
17
+ undef_method :[]=
18
+ members.each do |member|
19
+ undef_method("#{member}=")
20
+ end
21
+
22
+ # Override each so it has a Hash-like interface rather than Struct-like.
23
+ # The goal being to mimic a JSON-parsed hash representation of the raw
24
+ def each
25
+ members.map do |key|
26
+ tuple = [key.to_s, self.[](key)]
27
+ yield tuple if block_given?
28
+ tuple
29
+ end
30
+ end
31
+ end
32
+
33
+ class_methods do
34
+ # Default value for node_type covers most cases
35
+ def node_type
36
+ name.demodulize.underscore.dasherize
37
+ end
38
+
39
+ def tokenize(raw, context = nil)
40
+ unless raw['nodeType'] == node_type
41
+ raise ArgumentError, "Expected '#{node_type}', got '#{raw['nodeType']}'"
42
+ end
43
+
44
+ values =
45
+ members.map do |symbol|
46
+ val = raw[symbol.to_s]
47
+
48
+ case symbol
49
+ when :content
50
+ WCC::Contentful::RichText.tokenize(val, context)
51
+ # when :data
52
+ # TODO: resolve links...
53
+ else
54
+ val
55
+ end
56
+ end
57
+
58
+ new(*values)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './rich_text/node'
4
+
5
+ ##
6
+ # This module contains a number of structs representing nodes in a Contentful
7
+ # rich text field. When the Model layer parses a Rich Text field from
8
+ # Contentful, it is turned into a WCC::Contentful::RichText::Document node.
9
+ # The {WCC::Contentful::RichText::Document#content content} method of this
10
+ # node is an Array containing paragraph, blockquote, entry, and other nodes.
11
+ #
12
+ # The various structs in the RichText object model are designed to mimic the
13
+ # Hash interface, so that the indexing operator `#[]` and the `#dig` method
14
+ # can be used to traverse the data. The data can also be accessed by the
15
+ # attribute reader methods defined on the structs. Both of these are considered
16
+ # part of the public API of the model and will not change.
17
+ #
18
+ # In a future release we plan to implement automatic link resolution. When that
19
+ # happens, the `.data` attribute of embedded entries and assets will return a
20
+ # new class that is able to resolve the `.target` automatically into a full
21
+ # entry or asset. This future class will still respect the hash accessor methods
22
+ # `#[]`, `#dig`, `#keys`, and `#each`, so it is safe to use those.
23
+ module WCC::Contentful::RichText
24
+ ##
25
+ # Recursively converts a raw JSON-parsed hash into the RichText object model.
26
+ def self.tokenize(raw, context = nil)
27
+ return unless raw
28
+ return raw.map { |c| tokenize(c, context) } if raw.is_a?(Array)
29
+
30
+ klass =
31
+ case raw['nodeType']
32
+ when 'document'
33
+ Document
34
+ when 'paragraph'
35
+ Paragraph
36
+ when 'blockquote'
37
+ Blockquote
38
+ when 'text'
39
+ Text
40
+ when 'embedded-entry-inline'
41
+ EmbeddedEntryInline
42
+ when 'embedded-entry-block'
43
+ EmbeddedEntryBlock
44
+ when 'embedded-asset-block'
45
+ EmbeddedAssetBlock
46
+ when /heading\-(\d+)/
47
+ size = Regexp.last_match(1)
48
+ const_get("Heading#{size}")
49
+ else
50
+ Unknown
51
+ end
52
+
53
+ klass.tokenize(raw, context)
54
+ end
55
+
56
+ Document =
57
+ Struct.new(:nodeType, :data, :content) do
58
+ include WCC::Contentful::RichText::Node
59
+ end
60
+
61
+ Paragraph =
62
+ Struct.new(:nodeType, :data, :content) do
63
+ include WCC::Contentful::RichText::Node
64
+ end
65
+
66
+ Blockquote =
67
+ Struct.new(:nodeType, :data, :content) do
68
+ include WCC::Contentful::RichText::Node
69
+ end
70
+
71
+ Text =
72
+ Struct.new(:nodeType, :value, :marks, :data) do
73
+ include WCC::Contentful::RichText::Node
74
+ end
75
+
76
+ EmbeddedEntryInline =
77
+ Struct.new(:nodeType, :data, :content) do
78
+ include WCC::Contentful::RichText::Node
79
+ end
80
+
81
+ EmbeddedEntryBlock =
82
+ Struct.new(:nodeType, :data, :content) do
83
+ include WCC::Contentful::RichText::Node
84
+ end
85
+
86
+ EmbeddedAssetBlock =
87
+ Struct.new(:nodeType, :data, :content) do
88
+ include WCC::Contentful::RichText::Node
89
+ end
90
+
91
+ (1..5).each do |i|
92
+ struct =
93
+ Struct.new(:nodeType, :data, :content) do
94
+ include WCC::Contentful::RichText::Node
95
+ end
96
+ sz = i
97
+ struct.define_singleton_method(:node_type) { "heading-#{sz}" }
98
+ const_set("Heading#{sz}", struct)
99
+ end
100
+
101
+ Unknown =
102
+ Struct.new(:nodeType, :data, :content) do
103
+ include WCC::Contentful::RichText::Node
104
+ end
105
+ end
@@ -49,7 +49,6 @@ class WCC::Contentful::SimpleClient
49
49
 
50
50
  def next_page
51
51
  return unless next_page?
52
- return @next_page if @next_page
53
52
 
54
53
  query = (@request[:query] || {}).merge({
55
54
  skip: page_items.length + skip
@@ -58,7 +57,7 @@ class WCC::Contentful::SimpleClient
58
57
  _instrument 'page', url: @request[:url], query: query do
59
58
  @client.get(@request[:url], query)
60
59
  end
61
- @next_page = np.assert_ok!
60
+ np.assert_ok!
62
61
  end
63
62
 
64
63
  def initialize(client, request, raw_response)
@@ -77,16 +76,7 @@ class WCC::Contentful::SimpleClient
77
76
  def each_page(&block)
78
77
  raise ArgumentError, 'Not a collection response' unless page_items
79
78
 
80
- ret =
81
- Enumerator.new do |y|
82
- y << self
83
-
84
- if next_page?
85
- next_page.each_page.each do |page|
86
- y << page
87
- end
88
- end
89
- end
79
+ ret = PaginatingEnumerable.new(self)
90
80
 
91
81
  if block_given?
92
82
  ret.map(&block)
@@ -118,11 +108,6 @@ class WCC::Contentful::SimpleClient
118
108
  raw.dig('includes')&.each_with_object({}) do |(_t, entries), h|
119
109
  entries.each { |e| h[e.dig('sys', 'id')] = e }
120
110
  end || {}
121
-
122
- return @includes unless @next_page
123
-
124
- # This could be more efficient - maybe not worth worrying about
125
- @includes.merge(@next_page.includes)
126
111
  end
127
112
  end
128
113
 
@@ -144,8 +129,8 @@ class WCC::Contentful::SimpleClient
144
129
  @client.get(url)
145
130
  end
146
131
 
147
- @next_page ||= SyncResponse.new(next_page)
148
- @next_page.assert_ok!
132
+ next_page = SyncResponse.new(next_page)
133
+ next_page.assert_ok!
149
134
  end
150
135
 
151
136
  def next_sync_token
@@ -160,16 +145,7 @@ class WCC::Contentful::SimpleClient
160
145
  def each_page
161
146
  raise ArgumentError, 'Not a collection response' unless page_items
162
147
 
163
- ret =
164
- Enumerator.new do |y|
165
- y << self
166
-
167
- if next_page?
168
- next_page.each_page.each do |page|
169
- y << page
170
- end
171
- end
172
- end
148
+ ret = PaginatingEnumerable.new(self)
173
149
 
174
150
  if block_given?
175
151
  ret.map(&block)
@@ -190,6 +166,26 @@ class WCC::Contentful::SimpleClient
190
166
  end
191
167
  end
192
168
 
169
+ class PaginatingEnumerable
170
+ include Enumerable
171
+
172
+ def initialize(initial_page)
173
+ raise ArgumentError, 'Must provide initial page' unless initial_page.present?
174
+
175
+ @initial_page = initial_page
176
+ end
177
+
178
+ def each
179
+ page = @initial_page
180
+ yield page
181
+
182
+ while page.next_page?
183
+ page = page.next_page
184
+ yield page
185
+ end
186
+ end
187
+ end
188
+
193
189
  class ApiError < StandardError
194
190
  attr_reader :response
195
191
 
@@ -72,9 +72,13 @@ module WCC::Contentful::Store
72
72
  delegate :count, to: :response
73
73
 
74
74
  def to_enum
75
- return response.items unless @options[:include]
75
+ return response.each_page.flat_map(&:page_items) unless @options[:include]
76
76
 
77
- response.items.map { |e| resolve_includes(e, @options[:include]) }
77
+ response.each_page
78
+ .flat_map { |page| page.page_items.each_with_object(page).to_a }
79
+ .map do |e, page|
80
+ resolve_includes(e, page.includes, depth: @options[:include])
81
+ end
78
82
  end
79
83
 
80
84
  def initialize(store, client:, relation:, options: nil, **extra)
@@ -105,6 +109,11 @@ module WCC::Contentful::Store
105
109
 
106
110
  def apply_operator(operator, field, expected, context = nil)
107
111
  op = operator == :eq ? nil : operator
112
+ if expected.is_a?(Array)
113
+ expected = expected.join(',')
114
+ op = :in if op.nil?
115
+ end
116
+
108
117
  param = parameter(field, operator: op, context: context, locale: true)
109
118
 
110
119
  self.class.new(
@@ -155,18 +164,18 @@ module WCC::Contentful::Store
155
164
  end
156
165
  end
157
166
 
158
- def resolve_includes(entry, depth)
167
+ def resolve_includes(entry, includes, depth:)
159
168
  return entry unless entry && depth && depth > 0
160
169
 
161
170
  # Dig links out of response.includes and insert them into the entry
162
171
  WCC::Contentful::LinkVisitor.new(entry, :Link, depth: depth - 1).map! do |val|
163
- resolve_link(val)
172
+ resolve_link(val, includes)
164
173
  end
165
174
  end
166
175
 
167
- def resolve_link(val)
176
+ def resolve_link(val, includes)
168
177
  return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link'
169
- return val unless included = response.includes[val.dig('sys', 'id')]
178
+ return val unless included = includes[val.dig('sys', 'id')]
170
179
 
171
180
  included
172
181
  end
@@ -38,7 +38,13 @@ module WCC::Contentful::Store
38
38
  end
39
39
  end
40
40
 
41
+ SUPPORTED_OPS = %i[eq ne in nin].freeze
42
+
41
43
  def execute(query)
44
+ (query.conditions.map(&:op) - SUPPORTED_OPS).each do |op|
45
+ raise ArgumentError, "Operator :#{op} not supported"
46
+ end
47
+
42
48
  relation = mutex.with_read_lock { @hash.values }
43
49
 
44
50
  # relation is an enumerable that we apply conditions to in the form of
@@ -56,21 +62,49 @@ module WCC::Contentful::Store
56
62
  # For each condition, we apply a new Enumerable#select with a block that
57
63
  # enforces the condition.
58
64
  query.conditions.reduce(relation) do |memo, condition|
59
- memo.select do |entry|
60
- # Our naiive implementation only supports equality operator
61
- raise ArgumentError, "Operator #{condition.op} not supported" unless condition.op == :eq
65
+ __send__("apply_#{condition.op}", memo, condition)
66
+ end
67
+ end
62
68
 
63
- # The condition's path tells us where to find the value in the JSON object
64
- val = entry.dig(*condition.path)
69
+ private
65
70
 
66
- # For arrays, equality is defined as does the array include the expected value.
67
- # See https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/array-equality-inequality
68
- if val.is_a? Array
69
- val.include?(condition.expected)
70
- else
71
- val == condition.expected
72
- end
73
- end
71
+ def apply_eq(memo, condition)
72
+ memo.select { |entry| eq?(entry, condition) }
73
+ end
74
+
75
+ def apply_ne(memo, condition)
76
+ memo.reject { |entry| eq?(entry, condition) }
77
+ end
78
+
79
+ def eq?(entry, condition)
80
+ # The condition's path tells us where to find the value in the JSON object
81
+ val = entry.dig(*condition.path)
82
+
83
+ # For arrays, equality is defined as does the array include the expected value.
84
+ # See https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/array-equality-inequality
85
+ if val.is_a? Array
86
+ val.include?(condition.expected)
87
+ else
88
+ val == condition.expected
89
+ end
90
+ end
91
+
92
+ def apply_in(memo, condition)
93
+ memo.select { |entry| in?(entry, condition) }
94
+ end
95
+
96
+ def apply_nin(memo, condition)
97
+ memo.reject { |entry| in?(entry, condition) }
98
+ end
99
+
100
+ def in?(entry, condition)
101
+ val = entry.dig(*condition.path)
102
+
103
+ if val.is_a? Array
104
+ # TODO: detect if in ruby 3.1 and use val.intersect?(condition.expected)
105
+ val.any? { |item| condition.expected.include?(item) }
106
+ else
107
+ condition.expected.include?(val)
74
108
  end
75
109
  end
76
110
  end