media_types 1.0.0 → 2.1.1

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