media_types 1.0.0 → 2.1.1

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 +4 -4
  2. data/.github/workflows/debian.yml +43 -0
  3. data/.github/workflows/ruby.yml +3 -0
  4. data/.gitignore +10 -10
  5. data/CHANGELOG.md +80 -54
  6. data/Gemfile +6 -6
  7. data/Gemfile.lock +43 -114
  8. data/LICENSE +21 -0
  9. data/README.md +278 -85
  10. data/Rakefile +12 -12
  11. data/lib/media_types.rb +46 -3
  12. data/lib/media_types/constructable.rb +15 -9
  13. data/lib/media_types/dsl.rb +66 -31
  14. data/lib/media_types/dsl/errors.rb +18 -0
  15. data/lib/media_types/errors.rb +19 -0
  16. data/lib/media_types/scheme.rb +127 -13
  17. data/lib/media_types/scheme/errors.rb +66 -0
  18. data/lib/media_types/scheme/links.rb +15 -0
  19. data/lib/media_types/scheme/missing_validation.rb +12 -4
  20. data/lib/media_types/scheme/output_empty_guard.rb +5 -4
  21. data/lib/media_types/scheme/output_iterator_with_predicate.rb +13 -2
  22. data/lib/media_types/scheme/output_type_guard.rb +1 -1
  23. data/lib/media_types/scheme/rules.rb +53 -1
  24. data/lib/media_types/scheme/rules_exhausted_guard.rb +15 -4
  25. data/lib/media_types/scheme/validation_options.rb +17 -5
  26. data/lib/media_types/testing/assertions.rb +20 -0
  27. data/lib/media_types/validations.rb +29 -7
  28. data/lib/media_types/version.rb +1 -1
  29. data/media_types.gemspec +4 -7
  30. metadata +19 -63
  31. data/.travis.yml +0 -19
  32. data/lib/media_types/.dsl.rb.swp +0 -0
  33. data/lib/media_types/defaults.rb +0 -31
  34. data/lib/media_types/integrations.rb +0 -32
  35. data/lib/media_types/integrations/actionpack.rb +0 -21
  36. data/lib/media_types/integrations/http.rb +0 -47
  37. data/lib/media_types/minitest/assert_media_type_format.rb +0 -10
  38. data/lib/media_types/minitest/assert_media_types_registered.rb +0 -166
  39. data/lib/media_types/registrar.rb +0 -148
data/Rakefile CHANGED
@@ -1,12 +1,12 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rake/testtask'
5
-
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << 'test'
8
- t.libs << 'lib'
9
- t.test_files = FileList['test/**/*_test.rb']
10
- end
11
-
12
- task default: :test
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ task default: :test
data/lib/media_types.rb CHANGED
@@ -7,9 +7,9 @@ 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
- require 'media_types/integrations'
13
13
 
14
14
  module MediaTypes
15
15
  def self.set_organisation(mod, organisation)
@@ -17,14 +17,57 @@ module MediaTypes
17
17
  @organisation_prefixes[mod.name] = organisation
18
18
  end
19
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
+
20
64
  def self.get_organisation(mod)
21
65
  name = mod.name
22
66
  prefixes = @organisation_prefixes.keys.select { |p| name.start_with? p }
23
67
  return nil unless prefixes.any?
68
+
24
69
  best = prefixes.max_by { |p| p.length }
25
70
 
26
71
  @organisation_prefixes[best]
27
72
  end
28
73
  end
29
-
30
-
@@ -28,11 +28,6 @@ module MediaTypes
28
28
  with(view: view)
29
29
  end
30
30
 
31
- def suffix(suffix = NO_ARG)
32
- return opts[:suffix] if suffix == NO_ARG
33
- with(suffix: suffix)
34
- end
35
-
36
31
  def collection
37
32
  view(COLLECTION_VIEW)
38
33
  end
@@ -74,13 +69,24 @@ module MediaTypes
74
69
  end
75
70
 
76
71
  def as_key
77
- [type, view, version, suffix]
72
+ [type, view&.to_s, version]
78
73
  end
79
74
 
80
75
  def hash
81
76
  as_key.hash
82
77
  end
83
78
 
79
+ def override_suffix(suffix)
80
+ with(suffix: suffix)
81
+ end
82
+
83
+ def suffix
84
+ return opts[:suffix] if opts.key?(:suffix)
85
+
86
+ schema = schema_for(self)
87
+ schema.type_attributes.fetch(:suffix, 'json')
88
+ end
89
+
84
90
  def to_str(qualifier = nil)
85
91
  qualified(
86
92
  qualifier,
@@ -88,11 +94,11 @@ module MediaTypes
88
94
  type: opts[:type],
89
95
  view: opts[:view],
90
96
  version: opts[:version],
91
- suffix: opts[:suffix],
97
+ suffix: suffix
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
 
@@ -1,55 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  require 'media_types/constructable'
4
- require 'media_types/defaults'
5
- require 'media_types/registrar'
6
6
  require 'media_types/validations'
7
7
 
8
+ require 'media_types/dsl/errors'
9
+
8
10
  module MediaTypes
9
11
  module Dsl
10
-
11
12
  def self.included(base)
12
13
  base.extend ClassMethods
13
14
  base.class_eval do
14
15
  class << self
15
- attr_accessor :media_type_name_for, :media_type_combinations
16
+ attr_accessor :media_type_name_for, :media_type_combinations, :media_type_validations, :symbol_keys
16
17
 
17
18
  private
18
19
 
19
- attr_accessor :media_type_constructable, :symbol_base, :media_type_registrar, :media_type_validations
20
+ attr_accessor :media_type_constructable, :symbol_base, :media_type_registrar
20
21
  end
21
22
  base.media_type_combinations = Set.new
22
23
  end
23
24
  end
24
25
 
25
26
  module ClassMethods
26
-
27
27
  def to_constructable
28
+ raise UninitializedConstructable if media_type_constructable.nil?
29
+
28
30
  media_type_constructable.dup.tap do |constructable|
29
31
  constructable.__setobj__(self)
30
32
  end
31
33
  end
32
34
 
35
+ def symbol_keys?
36
+ if symbol_keys.nil?
37
+ MediaTypes.get_key_expectation(self)
38
+ else
39
+ symbol_keys
40
+ end
41
+ end
42
+
43
+ def string_keys?
44
+ !symbol_keys?
45
+ end
46
+
33
47
  def valid?(output, **opts)
34
48
  to_constructable.valid?(output, **opts)
35
49
  end
36
50
 
37
51
  def valid_unsafe?(output, media_type = to_constructable, **opts)
52
+ opts[:expected_key_type] = string_keys? ? String : Symbol
38
53
  validations.find(media_type).valid?(output, backtrace: ['.'], **opts)
39
54
  end
40
-
55
+
41
56
  def validate!(output, **opts)
57
+ assert_sane!
42
58
  to_constructable.validate!(output, **opts)
43
59
  end
44
60
 
45
61
  def validate_unsafe!(output, media_type = to_constructable, **opts)
62
+ opts[:expected_key_type] = string_keys? ? String : Symbol
46
63
  validations.find(media_type).validate(output, backtrace: ['.'], **opts)
47
64
  end
48
65
 
49
66
  def validatable?(media_type = to_constructable)
50
67
  return false unless validations
51
68
 
52
- validations.find(media_type, -> { nil })
69
+ resolved = validations.find(media_type, -> { nil })
70
+
71
+ !resolved.nil?
53
72
  end
54
73
 
55
74
  def register
@@ -58,19 +77,17 @@ module MediaTypes
58
77
  registerable
59
78
  end
60
79
  end
61
-
80
+
62
81
  def view(v)
63
82
  to_constructable.view(v)
64
83
  end
84
+
65
85
  def version(v)
66
86
  to_constructable.version(v)
67
87
  end
68
- def suffix(s)
69
- to_constructable.suffix(s)
70
- end
71
88
 
72
89
  def identifier_format
73
- self.media_type_name_for = Proc.new do |type:, view:, version:, suffix:|
90
+ self.media_type_name_for = proc do |type:, view:, version:, suffix:|
74
91
  yield(type: type, view: view, version: version, suffix: suffix)
75
92
  end
76
93
  end
@@ -80,24 +97,37 @@ module MediaTypes
80
97
  end
81
98
 
82
99
  def available_validations
83
- self.media_type_combinations.map do |a|
84
- _, view, version, suffix = a
85
- view(view).version(version).suffix(suffix)
100
+ media_type_combinations.map do |a|
101
+ _, view, version = a
102
+ view(view).version(version)
86
103
  end
87
104
  end
88
105
 
106
+ def schema_for(constructable)
107
+ validations.find(constructable)
108
+ end
109
+
110
+ def assert_sane!
111
+ return if media_type_validations.scheme.asserted_sane?
112
+
113
+ media_type_validations.run_fixture_validations(symbol_keys?)
114
+ end
115
+
89
116
  private
90
117
 
91
- def use_name(name, defaults: {})
92
- if self.media_type_name_for.nil?
93
- self.media_type_name_for = Proc.new do |type:, view:, version:, suffix:|
118
+ def use_name(name)
119
+ if media_type_name_for.nil?
120
+ self.media_type_name_for = proc do |type:, view:, version:, suffix:|
94
121
  resolved_org = nil
95
122
  if defined?(organisation)
96
123
  resolved_org = organisation
97
124
  else
98
- resolved_org = MediaTypes::get_organisation(self)
125
+ resolved_org = MediaTypes.get_organisation(self)
99
126
 
100
- 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?
127
+ if resolved_org.nil?
128
+ raise OrganisationNotSetError,
129
+ format('Implement the class method "organisation" in %<klass>s or specify a global organisation using MediaTypes::set_organisation', klass: self)
130
+ end
101
131
  end
102
132
  raise ArgumentError, 'Unable to create a name for a schema with a nil name.' if type.nil?
103
133
  raise ArgumentError, 'Unable to create a name for a schema with a nil organisation.' if resolved_org.nil?
@@ -109,29 +139,34 @@ module MediaTypes
109
139
  result
110
140
  end
111
141
  end
112
- self.media_type_constructable = Constructable.new(self, type: name).suffix(defaults.fetch(:suffix) { nil })
142
+ self.media_type_constructable = Constructable.new(self, type: name)
113
143
  end
114
144
 
115
- def defaults(&block)
116
- return media_type_constructable unless block_given?
117
- self.media_type_constructable = Defaults.new(to_constructable, &block).to_constructable
145
+ def expect_string_keys
146
+ raise KeyTypeExpectationError, 'Key expectation already set' unless symbol_keys.nil?
118
147
 
119
- self
148
+ self.symbol_keys = false
120
149
  end
121
150
 
122
- def registrations(symbol = nil, &block)
123
- return media_type_registrar unless block_given?
124
- self.media_type_registrar = Registrar.new(self, symbol: symbol, &block)
151
+ def expect_symbol_keys
152
+ raise KeyTypeExpectationError, 'Key expectation already set' unless symbol_keys.nil?
125
153
 
126
- self
154
+ self.symbol_keys = true
127
155
  end
128
156
 
129
157
  def validations(&block)
130
- return media_type_validations unless block_given?
158
+ return lookup_validations unless block_given?
159
+
131
160
  self.media_type_validations = Validations.new(to_constructable, &block)
132
161
 
133
162
  self
134
163
  end
164
+
165
+ def lookup_validations
166
+ raise MissingValidationError, "No validations defined for #{name}" if media_type_validations.nil?
167
+
168
+ media_type_validations
169
+ end
135
170
  end
136
171
  end
137
172
  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
  ##
@@ -56,10 +95,18 @@ module MediaTypes
56
95
  #
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)
98
+ self.type_attributes = {}
99
+ self.fixtures = []
100
+ self.asserted_sane = false
59
101
 
60
102
  instance_exec(&block) if block_given?
61
103
  end
62
104
 
105
+ attr_accessor :type_attributes, :fixtures
106
+ attr_reader :rules, :asserted_sane
107
+
108
+ alias asserted_sane? asserted_sane
109
+
63
110
  ##
64
111
  # Checks if the +output+ is valid
65
112
  #
@@ -98,6 +145,7 @@ module MediaTypes
98
145
  #
99
146
  def validate(output, options = nil, **opts)
100
147
  options ||= ValidationOptions.new(**opts)
148
+ options.context = output
101
149
 
102
150
  catch(:end) do
103
151
  validate!(output, options, context: nil)
@@ -154,7 +202,11 @@ module MediaTypes
154
202
  # MyMedia.valid?({ foo: { bar: 'my-string' }})
155
203
  # # => true
156
204
  #
157
- def attribute(key, type = ::Object, optional: false, **opts, &block)
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
209
+
158
210
  if block_given?
159
211
  return collection(key, expected_type: ::Hash, optional: optional, **opts, &block)
160
212
  end
@@ -207,6 +259,8 @@ module MediaTypes
207
259
  # # => true
208
260
  #
209
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
+
210
264
  unless block_given?
211
265
  if scheme.is_a?(Scheme)
212
266
  return rules.default = scheme
@@ -294,6 +348,8 @@ module MediaTypes
294
348
  # # => true
295
349
  #
296
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
+
297
353
  unless block_given?
298
354
  return rules.add(
299
355
  key,
@@ -376,24 +432,82 @@ module MediaTypes
376
432
  end
377
433
 
378
434
  def assert_pass(fixture)
379
- json = JSON.parse(fixture)
380
-
381
- 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)
382
437
  end
383
-
438
+
384
439
  def assert_fail(fixture)
385
- json = JSON.parse(fixture)
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
386
497
 
387
498
  begin
388
- validate(json)
389
- rescue MediaTypes::Scheme::ValidationError
390
- 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?
391
505
  end
392
- raise AssertionError
393
506
  end
394
507
 
395
508
  private
396
509
 
397
- attr_accessor :rules
510
+ attr_writer :rules, :asserted_sane
511
+
398
512
  end
399
513
  end