media_types 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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