media_types 2.0.1 → 2.1.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.
data/lib/media_types.rb CHANGED
@@ -7,6 +7,7 @@ require 'media_types/hash'
7
7
  require 'media_types/object'
8
8
  require 'media_types/scheme'
9
9
  require 'media_types/dsl'
10
+ require 'media_types/errors'
10
11
 
11
12
  require 'media_types/views'
12
13
 
@@ -16,14 +17,57 @@ module MediaTypes
16
17
  @organisation_prefixes[mod.name] = organisation
17
18
  end
18
19
 
20
+ def self.expect_string_keys(mod)
21
+ set_key_expectation(mod, false)
22
+ end
23
+
24
+ def self.expect_symbol_keys(mod)
25
+ set_key_expectation(mod, true)
26
+ end
27
+
28
+ # Keep track of modules setting their key expectations
29
+ def self.set_key_expectation(mod, expect_symbol_keys)
30
+ @key_expectations ||= {}
31
+ @key_expectations_used ||= {}
32
+
33
+ raise KeyExpectationSetError.new(mod: mod) unless @key_expectations[mod.name].nil?
34
+ raise KeyExpectationUsedError.new(mod: mod) if @key_expectations_used[mod.name]
35
+
36
+ @key_expectations[mod.name] = expect_symbol_keys
37
+ end
38
+
39
+ SYMBOL_KEYS_DEFAULT = true
40
+
41
+ def self.get_key_expectation(mod)
42
+ @key_expectations ||= {}
43
+ @key_expectations_used ||= {}
44
+
45
+ expect_symbol = find_key_expectation(mod)
46
+
47
+ expect_symbol.nil? ? SYMBOL_KEYS_DEFAULT : expect_symbol
48
+ end
49
+
50
+ def self.find_key_expectation(mod)
51
+ modules = mod.name.split('::')
52
+ expect_symbol = nil
53
+
54
+ while modules.any? && expect_symbol.nil?
55
+ current_module = modules.join('::')
56
+ expect_symbol = @key_expectations[current_module]
57
+ @key_expectations_used[current_module] = true
58
+ modules.pop
59
+ end
60
+
61
+ expect_symbol
62
+ end
63
+
19
64
  def self.get_organisation(mod)
20
65
  name = mod.name
21
66
  prefixes = @organisation_prefixes.keys.select { |p| name.start_with? p }
22
67
  return nil unless prefixes.any?
68
+
23
69
  best = prefixes.max_by { |p| p.length }
24
70
 
25
71
  @organisation_prefixes[best]
26
72
  end
27
73
  end
28
-
29
-
@@ -76,7 +76,13 @@ module MediaTypes
76
76
  as_key.hash
77
77
  end
78
78
 
79
+ def override_suffix(suffix)
80
+ with(suffix: suffix)
81
+ end
82
+
79
83
  def suffix
84
+ return opts[:suffix] if opts.key?(:suffix)
85
+
80
86
  schema = schema_for(self)
81
87
  schema.type_attributes.fetch(:suffix, 'json')
82
88
  end
@@ -92,7 +98,7 @@ module MediaTypes
92
98
  )
93
99
  )
94
100
  end
95
-
101
+
96
102
  def available_validations
97
103
  return [] if !validatable?
98
104
  [self]
@@ -120,7 +126,7 @@ module MediaTypes
120
126
 
121
127
  def validatable?
122
128
  return false unless media_type_combinations.include? as_key
123
-
129
+
124
130
  __getobj__.validatable?(self)
125
131
  end
126
132
 
@@ -3,44 +3,61 @@
3
3
  require 'media_types/constructable'
4
4
  require 'media_types/validations'
5
5
 
6
+ require 'media_types/dsl/errors'
7
+
6
8
  module MediaTypes
7
9
  module Dsl
8
-
9
10
  def self.included(base)
10
11
  base.extend ClassMethods
11
12
  base.class_eval do
12
13
  class << self
13
- attr_accessor :media_type_name_for, :media_type_combinations
14
+ attr_accessor :media_type_name_for, :media_type_combinations, :media_type_validations, :symbol_keys
14
15
 
15
16
  private
16
17
 
17
- attr_accessor :media_type_constructable, :symbol_base, :media_type_registrar, :media_type_validations
18
+ attr_accessor :media_type_constructable, :symbol_base, :media_type_registrar
18
19
  end
19
20
  base.media_type_combinations = Set.new
20
21
  end
21
22
  end
22
23
 
23
24
  module ClassMethods
24
-
25
25
  def to_constructable
26
+ raise UninitializedConstructable if media_type_constructable.nil?
27
+
26
28
  media_type_constructable.dup.tap do |constructable|
27
29
  constructable.__setobj__(self)
28
30
  end
29
31
  end
30
32
 
33
+ def symbol_keys?
34
+ if symbol_keys.nil?
35
+ MediaTypes.get_key_expectation(self)
36
+ else
37
+ symbol_keys
38
+ end
39
+ end
40
+
41
+ def string_keys?
42
+ !symbol_keys?
43
+ end
44
+
31
45
  def valid?(output, **opts)
32
46
  to_constructable.valid?(output, **opts)
33
47
  end
34
48
 
35
49
  def valid_unsafe?(output, media_type = to_constructable, **opts)
50
+ opts[:expected_key_type] = string_keys? ? String : Symbol
36
51
  validations.find(media_type).valid?(output, backtrace: ['.'], **opts)
37
52
  end
38
-
53
+
39
54
  def validate!(output, **opts)
55
+ assert_sane!
40
56
  to_constructable.validate!(output, **opts)
41
57
  end
42
58
 
43
59
  def validate_unsafe!(output, media_type = to_constructable, **opts)
60
+ opts[:expected_key_type] = string_keys? ? String : Symbol
44
61
  validations.find(media_type).validate(output, backtrace: ['.'], **opts)
45
62
  end
46
63
 
@@ -58,16 +75,17 @@ module MediaTypes
58
75
  registerable
59
76
  end
60
77
  end
61
-
78
+
62
79
  def view(v)
63
80
  to_constructable.view(v)
64
81
  end
82
+
65
83
  def version(v)
66
84
  to_constructable.version(v)
67
85
  end
68
-
86
+
69
87
  def identifier_format
70
- self.media_type_name_for = Proc.new do |type:, view:, version:, suffix:|
88
+ self.media_type_name_for = proc do |type:, view:, version:, suffix:|
71
89
  yield(type: type, view: view, version: version, suffix: suffix)
72
90
  end
73
91
  end
@@ -77,7 +95,7 @@ module MediaTypes
77
95
  end
78
96
 
79
97
  def available_validations
80
- self.media_type_combinations.map do |a|
98
+ media_type_combinations.map do |a|
81
99
  _, view, version = a
82
100
  view(view).version(version)
83
101
  end
@@ -87,18 +105,27 @@ module MediaTypes
87
105
  validations.find(constructable)
88
106
  end
89
107
 
108
+ def assert_sane!
109
+ return if media_type_validations.scheme.asserted_sane?
110
+
111
+ media_type_validations.run_fixture_validations(symbol_keys?)
112
+ end
113
+
90
114
  private
91
115
 
92
116
  def use_name(name)
93
- if self.media_type_name_for.nil?
94
- self.media_type_name_for = Proc.new do |type:, view:, version:, suffix:|
117
+ if media_type_name_for.nil?
118
+ self.media_type_name_for = proc do |type:, view:, version:, suffix:|
95
119
  resolved_org = nil
96
120
  if defined?(organisation)
97
121
  resolved_org = organisation
98
122
  else
99
- resolved_org = MediaTypes::get_organisation(self)
123
+ resolved_org = MediaTypes.get_organisation(self)
100
124
 
101
- raise format('Implement the class method "organisation" in %<klass>s or specify a global organisation using MediaTypes::set_organisation', klass: self) if resolved_org.nil?
125
+ if resolved_org.nil?
126
+ raise OrganisationNotSetError,
127
+ format('Implement the class method "organisation" in %<klass>s or specify a global organisation using MediaTypes::set_organisation', klass: self)
128
+ end
102
129
  end
103
130
  raise ArgumentError, 'Unable to create a name for a schema with a nil name.' if type.nil?
104
131
  raise ArgumentError, 'Unable to create a name for a schema with a nil organisation.' if resolved_org.nil?
@@ -113,15 +140,31 @@ module MediaTypes
113
140
  self.media_type_constructable = Constructable.new(self, type: name)
114
141
  end
115
142
 
143
+ def expect_string_keys
144
+ raise KeyTypeExpectationError, 'Key expectation already set' unless symbol_keys.nil?
145
+
146
+ self.symbol_keys = false
147
+ end
148
+
149
+ def expect_symbol_keys
150
+ raise KeyTypeExpectationError, 'Key expectation already set' unless symbol_keys.nil?
151
+
152
+ self.symbol_keys = true
153
+ end
154
+
116
155
  def validations(&block)
117
- unless block_given?
118
- raise "No validations defined for #{self.name}" if media_type_validations.nil?
119
- return media_type_validations
120
- end
156
+ return lookup_validations unless block_given?
157
+
121
158
  self.media_type_validations = Validations.new(to_constructable, &block)
122
159
 
123
160
  self
124
161
  end
162
+
163
+ def lookup_validations
164
+ raise MissingValidationError, "No validations defined for #{name}" if media_type_validations.nil?
165
+
166
+ media_type_validations
167
+ end
125
168
  end
126
169
  end
127
170
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ module Dsl
5
+ class UninitializedConstructable < RuntimeError
6
+ def message
7
+ 'Unable to generate constructable without a name, make sure to have called `use_name(name)` before.'
8
+ end
9
+ end
10
+
11
+ # Raised when an error occurs during setting expected key type
12
+ class KeyTypeExpectationError < StandardError; end
13
+
14
+ class MissingValidationError < StandardError; end
15
+
16
+ class OrganisationNotSetError < StandardError; end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ module Errors
5
+ # Raised when trying to set a module key expectation twice
6
+ class KeyExpectationSetError < StandardError
7
+ def initialize(mod:)
8
+ super(format('%<mod>s already has a key expectation set', mod: mod.name))
9
+ end
10
+ end
11
+
12
+ # Raised when trying to set a module key expectation while default expectation already used
13
+ class KeyExpectationUsedError < StandardError
14
+ def initialize(mod:)
15
+ super(format('Unable to change key type expectation for %<mod>s since its current expectation is already used', mod: mod.name))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  require 'media_types/scheme/validation_options'
4
6
  require 'media_types/scheme/enumeration_context'
5
7
  require 'media_types/scheme/errors'
@@ -17,10 +19,47 @@ require 'media_types/scheme/output_empty_guard'
17
19
  require 'media_types/scheme/output_type_guard'
18
20
  require 'media_types/scheme/rules_exhausted_guard'
19
21
 
20
- require 'json'
21
-
22
22
  module MediaTypes
23
23
  class AssertionError < StandardError
24
+ def initialize(errors)
25
+ @fixture_errors = errors
26
+ end
27
+
28
+ def message
29
+ fixture_errors.map(&:message).join(', ')
30
+ end
31
+
32
+ attr_reader :fixture_errors
33
+ end
34
+
35
+ class UnexpectedValidationResultError < StandardError
36
+ def initialize(fixture_caller, error)
37
+ self.fixture_caller = fixture_caller
38
+ self.error = error
39
+ end
40
+
41
+ def message
42
+ format(
43
+ '%<caller_path>s:%<caller_line>s -> %<error>s',
44
+ caller_path: fixture_caller.path,
45
+ caller_line: fixture_caller.lineno,
46
+ error: error.is_a?(MediaTypes::Scheme::ValidationError) ? "#{error.class}:#{error.message}" : error
47
+ )
48
+ end
49
+
50
+ attr_accessor :fixture_caller, :error
51
+ end
52
+
53
+ class FixtureData
54
+ def initialize(caller:, fixture:, expect_to_pass:)
55
+ self.caller = caller
56
+ self.fixture = fixture
57
+ self.expect_to_pass = expect_to_pass
58
+ end
59
+
60
+ attr_accessor :caller, :fixture, :expect_to_pass
61
+
62
+ alias expect_to_pass? expect_to_pass
24
63
  end
25
64
 
26
65
  ##
@@ -57,11 +96,16 @@ module MediaTypes
57
96
  def initialize(allow_empty: false, expected_type: ::Object, &block)
58
97
  self.rules = Rules.new(allow_empty: allow_empty, expected_type: expected_type)
59
98
  self.type_attributes = {}
99
+ self.fixtures = []
100
+ self.asserted_sane = false
60
101
 
61
102
  instance_exec(&block) if block_given?
62
103
  end
63
104
 
64
- attr_accessor :type_attributes
105
+ attr_accessor :type_attributes, :fixtures
106
+ attr_reader :rules, :asserted_sane
107
+
108
+ alias asserted_sane? asserted_sane
65
109
 
66
110
  ##
67
111
  # Checks if the +output+ is valid
@@ -158,11 +202,10 @@ module MediaTypes
158
202
  # MyMedia.valid?({ foo: { bar: 'my-string' }})
159
203
  # # => true
160
204
  #
161
- def attribute(key, type = ::Object, optional: false, **opts, &block)
162
- raise KeyTypeError, "Unexpected key type #{key.class.name}, please use either a symbol or string." unless key.is_a?(String) || key.is_a?(Symbol)
163
- raise DuplicateKeyError, "An attribute with key #{key.to_s} has already been defined. Please remove one of the two." if rules.has_key?(key)
164
- raise DuplicateKeyError, "A string attribute with the same string representation as the symbol :#{key.to_s} already exists. Please remove one of the two." if key.is_a?(Symbol)&& rules.has_key?(key.to_s)
165
- raise DuplicateKeyError, "A symbol attribute with the same string representation as the string '#{key}' already exists. Please remove one of the two." if key.is_a?(String) && rules.has_key?(key.to_sym)
205
+ def attribute(key, type = nil, optional: false, **opts, &block)
206
+ raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed attribute, either remove the type or the block' if type != ::Hash && block_given? && !type.nil?
207
+
208
+ type ||= ::Object
166
209
 
167
210
  if block_given?
168
211
  return collection(key, expected_type: ::Hash, optional: optional, **opts, &block)
@@ -216,6 +259,8 @@ module MediaTypes
216
259
  # # => true
217
260
  #
218
261
  def any(scheme = nil, expected_type: ::Hash, allow_empty: false, &block)
262
+ raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed property, either remove the type or the block' if scheme != ::Hash && block_given? && !scheme.nil?
263
+
219
264
  unless block_given?
220
265
  if scheme.is_a?(Scheme)
221
266
  return rules.default = scheme
@@ -303,6 +348,8 @@ module MediaTypes
303
348
  # # => true
304
349
  #
305
350
  def collection(key, scheme = nil, allow_empty: false, expected_type: ::Array, optional: false, &block)
351
+ raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed collection, either remove the type or the block' if scheme != ::Hash && block_given? && !scheme.nil?
352
+
306
353
  unless block_given?
307
354
  return rules.add(
308
355
  key,
@@ -385,24 +432,82 @@ module MediaTypes
385
432
  end
386
433
 
387
434
  def assert_pass(fixture)
388
- json = JSON.parse(fixture, { symbolize_names: true })
389
-
390
- validate(json)
435
+ reduced_stack = remove_current_dir_from_stack(caller_locations)
436
+ @fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: true)
391
437
  end
392
-
438
+
393
439
  def assert_fail(fixture)
394
- json = JSON.parse(fixture, { symbolize_names: true })
440
+ reduced_stack = remove_current_dir_from_stack(caller_locations)
441
+ @fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: false)
442
+ end
443
+
444
+ # Removes all calls originating in current dir from given stack
445
+ # We need this so that we find out the caller of an assert_pass/fail in the caller_locations
446
+ # Which gets polluted by Scheme consecutively executing blocks within the validation blocks
447
+ def remove_current_dir_from_stack(stack)
448
+ stack.reject { |location| location.path.include?(__dir__) }
449
+ end
450
+
451
+ def validate_scheme_fixtures(expect_symbol_keys, backtrace)
452
+ @fixtures.map do |fixture_data|
453
+ begin
454
+ validate_fixture(fixture_data, expect_symbol_keys, backtrace)
455
+ nil
456
+ rescue UnexpectedValidationResultError => e
457
+ e
458
+ end
459
+ end.compact
460
+ end
461
+
462
+ def validate_nested_scheme_fixtures(expect_symbol_keys, backtrace)
463
+ @rules.flat_map do |key, rule|
464
+ next unless rule.is_a?(Scheme) || rule.is_a?(Links)
465
+
466
+ begin
467
+ rule.run_fixture_validations(expect_symbol_keys, backtrace.dup.append(key))
468
+ nil
469
+ rescue AssertionError => e
470
+ e.fixture_errors
471
+ end
472
+ end.compact
473
+ end
474
+
475
+ def validate_default_scheme_fixtures(expect_symbol_keys, backtrace)
476
+ return [] unless @rules.default.is_a?(Scheme)
477
+
478
+ @rules.default.run_fixture_validations(expect_symbol_keys, backtrace.dup.append('*'))
479
+ []
480
+ rescue AssertionError => e
481
+ e.fixture_errors
482
+ end
483
+
484
+ def run_fixture_validations(expect_symbol_keys, backtrace = [])
485
+ fixture_errors = validate_scheme_fixtures(expect_symbol_keys, backtrace)
486
+ fixture_errors += validate_nested_scheme_fixtures(expect_symbol_keys, backtrace)
487
+ fixture_errors += validate_default_scheme_fixtures(expect_symbol_keys, backtrace)
488
+
489
+ raise AssertionError, fixture_errors unless fixture_errors.empty?
490
+
491
+ self.asserted_sane = true
492
+ end
493
+
494
+ def validate_fixture(fixture_data, expect_symbol_keys, backtrace = [])
495
+ json = JSON.parse(fixture_data.fixture, { symbolize_names: expect_symbol_keys })
496
+ expected_key_type = expect_symbol_keys ? Symbol : String
395
497
 
396
498
  begin
397
- validate(json)
398
- rescue MediaTypes::Scheme::ValidationError
399
- return
499
+ validate(json, expected_key_type: expected_key_type, backtrace: backtrace)
500
+ unless fixture_data.expect_to_pass?
501
+ raise UnexpectedValidationResultError.new(fixture_data.caller, 'No error encounterd whilst expecting to')
502
+ end
503
+ rescue MediaTypes::Scheme::ValidationError => e
504
+ raise UnexpectedValidationResultError.new(fixture_data.caller, e) if fixture_data.expect_to_pass?
400
505
  end
401
- raise AssertionError
402
506
  end
403
507
 
404
508
  private
405
509
 
406
- attr_accessor :rules
510
+ attr_writer :rules, :asserted_sane
511
+
407
512
  end
408
513
  end