Skip to content

Commit 1448c4e

Browse files
committed
Add ability to declare some attributes as loosly validated
Allows skipping certain attributes when the loose argument is set
1 parent 4471d4a commit 1448c4e

File tree

8 files changed

+71
-25
lines changed

8 files changed

+71
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## 2.2.0
44

5-
- Add ability to define multiple versions using one block
5+
- Add ability to define multiple versions using one block.
6+
- Added ability to mark certain attributes as optional when validating with `loose: true` and required otherwise.
67

78
## 2.1.1
89

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class Venue
8181

8282
version 1 do
8383
attribute :name, String
84-
attribute :coords, String
84+
attribute :coords, String, optional: :loose
8585
attribute :updated_at, String
8686

8787
link :self
@@ -467,8 +467,8 @@ class MyMedia
467467
{ "foo": { "bar": "string" } }
468468
FIXTURE
469469
assert_fail '{"foo": {}}'
470-
assert_fail '{"foo": null}'
471-
assert_fail '{"foo": [42]}'
470+
assert_fail '{"foo": null}', loose: true
471+
assert_fail '{"foo": [42]}', loose: false
472472
end
473473
end
474474

lib/media_types/scheme.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ def message
5151
end
5252

5353
class FixtureData
54-
def initialize(caller:, fixture:, expect_to_pass:)
54+
def initialize(caller:, fixture:, expect_to_pass:, loose:)
5555
self.caller = caller
5656
self.fixture = fixture
5757
self.expect_to_pass = expect_to_pass
58+
self.loose = loose
5859
end
5960

60-
attr_accessor :caller, :fixture, :expect_to_pass
61+
attr_accessor :caller, :fixture, :expect_to_pass, :loose
6162

6263
alias expect_to_pass? expect_to_pass
6364
end
@@ -431,14 +432,14 @@ def inspect(indentation = 0)
431432
].join("\n")
432433
end
433434

434-
def assert_pass(fixture)
435+
def assert_pass(fixture, loose: false)
435436
reduced_stack = remove_current_dir_from_stack(caller_locations)
436-
@fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: true)
437+
@fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: true, loose: loose)
437438
end
438439

439-
def assert_fail(fixture)
440+
def assert_fail(fixture, loose: false)
440441
reduced_stack = remove_current_dir_from_stack(caller_locations)
441-
@fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: false)
442+
@fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: false, loose: loose)
442443
end
443444

444445
# Removes all calls originating in current dir from given stack
@@ -496,7 +497,7 @@ def validate_fixture(fixture_data, expect_symbol_keys, backtrace = [])
496497
expected_key_type = expect_symbol_keys ? Symbol : String
497498

498499
begin
499-
validate(json, expected_key_type: expected_key_type, backtrace: backtrace)
500+
validate(json, expected_key_type: expected_key_type, backtrace: backtrace, loose: fixture_data.loose)
500501
unless fixture_data.expect_to_pass?
501502
raise UnexpectedValidationResultError.new(fixture_data.caller, 'No error encounterd whilst expecting to')
502503
end

lib/media_types/scheme/output_empty_guard.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ def call
2929
attr_accessor :output, :options, :rules
3030

3131
def allow_empty?
32-
rules.allow_empty? || rules.required.empty?
32+
rules.allow_empty? || rules.required(loose: options.loose).empty?
3333
end
3434

3535
def raise_empty!(backtrace:, found:)
3636
raise EmptyOutputError, format(
37-
'Expected output, got empty at %<backtrace>s. Required are: %<required>s. Found: %<found>s',
37+
'The object at %<backtrace>s was empty but I expected contents. Required keys are: %<required>s.',
3838
backtrace: backtrace.join('->'),
39-
required: rules.required.keys,
40-
found: (found.is_a? Hash) ? found.keys : found.class.name,
39+
required: rules.required(loose: options.loose).keys,
40+
found: (found.respond_to? :keys) ? found.keys : found.class.name,
4141
)
4242
end
4343
end

lib/media_types/scheme/rules.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def initialize(allow_empty:, expected_type:)
1414
self.allow_empty = allow_empty
1515
self.expected_type = expected_type
1616
self.optional_keys = []
17+
self.strict_keys = []
1718
self.original_key_type = {}
1819

1920
self.default = MissingValidation.new
@@ -32,7 +33,11 @@ def add(key, val, optional: false)
3233

3334
normalized_key = normalize_key(key)
3435
__getobj__[normalized_key] = val
35-
optional_keys << normalized_key if optional
36+
if optional == :loose
37+
strict_keys << normalized_key
38+
else
39+
optional_keys << normalized_key if optional
40+
end
3641
original_key_type[normalized_key] = key.class
3742

3843
self
@@ -87,11 +92,16 @@ def delete(key)
8792
#
8893
# @return [Array<Symbol>] required keys
8994
#
90-
def required
95+
def required(loose:)
9196
clone.tap do |cloned|
9297
optional_keys.each do |key|
9398
cloned.delete(key)
9499
end
100+
if loose
101+
strict_keys.each do |key|
102+
cloned.delete(key)
103+
end
104+
end
95105
end
96106
end
97107

@@ -113,6 +123,9 @@ def merge(rules)
113123
if rules.respond_to?(:optional_keys, true)
114124
optional_keys.push(*rules.send(:optional_keys))
115125
end
126+
if rules.respond_to?(:strict_keys, true)
127+
strict_keys.push(*rules.send(:strict_keys))
128+
end
116129

117130
self
118131
end
@@ -123,7 +136,7 @@ def inspect(indent = 0)
123136
return "#{prefix}[Error]Depth limit reached[/Error]" if indent > 5_000
124137

125138
[
126-
"#{prefix}[Rules n=#{keys.length} optional=#{optional_keys.length} allow_empty=#{allow_empty?}]",
139+
"#{prefix}[Rules n=#{keys.length} optional=#{optional_keys.length} strict=#{strict_keys.length} allow_empty=#{allow_empty?}]",
127140
"#{prefix} #{inspect_format_attribute(indent, '*', default)}",
128141
*keys.map { |key| "#{prefix} #{inspect_format_attribute(indent, key)}" },
129142
"#{prefix}[/Rules]"
@@ -162,7 +175,7 @@ def default=(input_default)
162175

163176
private
164177

165-
attr_accessor :allow_empty, :optional_keys, :original_key_type
178+
attr_accessor :allow_empty, :strict_keys, :optional_keys, :original_key_type
166179
attr_writer :expected_type
167180

168181
def normalize_key(key)

lib/media_types/scheme/rules_exhausted_guard.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def call
2626
return iterate(EMPTY_MARK)
2727
end
2828

29-
required_rules = rules.required
29+
required_rules = rules.required(loose: options.loose)
3030
# noinspection RubyScope
3131
result = iterate(->(key) { required_rules.remove(key) })
3232
return result if required_rules.empty?

lib/media_types/scheme/validation_options.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@
33
module MediaTypes
44
class Scheme
55
class ValidationOptions
6-
attr_accessor :exhaustive, :strict, :backtrace, :context, :expected_key_type
6+
attr_accessor :exhaustive, :strict, :backtrace, :context, :expected_key_type, :loose
77

8-
def initialize(context = {}, exhaustive: true, strict: true, backtrace: [], expected_key_type:)
8+
def initialize(context = {}, exhaustive: true, strict: true, backtrace: [], loose: false, expected_key_type:)
99
self.exhaustive = exhaustive
1010
self.strict = strict
1111
self.backtrace = backtrace
1212
self.context = context
1313
self.expected_key_type = expected_key_type
14+
self.loose = loose
1415
end
1516

1617
def inspect
17-
"backtrack: #{backtrace.inspect}, strict: #{strict.inspect}, exhaustive: #{exhaustive}, current_obj: #{scoped_output.to_json}"
18+
"backtrack: #{backtrace.inspect}, strict: #{strict.inspect}, loose: #{loose}, exhaustive: #{exhaustive}, current_obj: #{scoped_output.to_json}"
1819
end
1920

2021
def scoped_output
@@ -28,15 +29,15 @@ def scoped_output
2829
end
2930

3031
def with_backtrace(backtrace)
31-
ValidationOptions.new(context, exhaustive: exhaustive, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type)
32+
ValidationOptions.new(context, exhaustive: exhaustive, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type, loose: loose)
3233
end
3334

3435
def trace(*traces)
3536
with_backtrace(backtrace.dup.concat(traces))
3637
end
3738

3839
def exhaustive!
39-
ValidationOptions.new(context, exhaustive: true, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type)
40+
ValidationOptions.new(context, exhaustive: true, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type, loose: loose)
4041
end
4142
end
4243
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../test_helper'
4+
5+
class LooseValidationTest < Minitest::Test
6+
### Attribute ###
7+
8+
class TestLooseAttributes
9+
include MediaTypes::Dsl
10+
11+
def self.organisation
12+
'domain.test'
13+
end
14+
15+
use_name 'TestThatWholeContextOfBlockIsUsedAttribute'
16+
17+
# default attribute (=hash object)
18+
validations do
19+
attribute :foo do
20+
attribute :bar, Numeric, optional: :loose
21+
end
22+
assert_pass '{"foo":{}}', loose: true
23+
assert_fail '{"foo":{}}', loose: false
24+
end
25+
end
26+
27+
[TestLooseAttributes].each do |type|
28+
create_specification_tests_for type
29+
end
30+
end

0 commit comments

Comments
 (0)