media_types-serialization 0.8.0 → 1.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -3
  3. data/.prettierrc +1 -0
  4. data/CHANGELOG.md +42 -0
  5. data/CODE_OF_CONDUCT.md +74 -74
  6. data/Gemfile.lock +74 -83
  7. data/README.md +691 -179
  8. data/lib/media_types/problem.rb +64 -0
  9. data/lib/media_types/serialization.rb +497 -173
  10. data/lib/media_types/serialization/base.rb +115 -91
  11. data/lib/media_types/serialization/error.rb +186 -0
  12. data/lib/media_types/serialization/fake_validator.rb +52 -0
  13. data/lib/media_types/serialization/serialization_dsl.rb +117 -0
  14. data/lib/media_types/serialization/serialization_registration.rb +245 -0
  15. data/lib/media_types/serialization/serializers/api_viewer.rb +133 -0
  16. data/lib/media_types/serialization/serializers/common_css.rb +168 -0
  17. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -0
  18. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -0
  19. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -0
  20. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +89 -0
  21. data/lib/media_types/serialization/serializers/problem_serializer.rb +100 -0
  22. data/lib/media_types/serialization/utils/accept_header.rb +77 -0
  23. data/lib/media_types/serialization/utils/accept_language_header.rb +82 -0
  24. data/lib/media_types/serialization/utils/header_list.rb +89 -0
  25. data/lib/media_types/serialization/version.rb +1 -1
  26. data/media_types-serialization.gemspec +48 -50
  27. metadata +48 -79
  28. data/.travis.yml +0 -17
  29. data/lib/generators/media_types/serialization/api_viewer/api_viewer_generator.rb +0 -25
  30. data/lib/generators/media_types/serialization/api_viewer/templates/api_viewer.html.erb +0 -98
  31. data/lib/generators/media_types/serialization/api_viewer/templates/initializer.rb +0 -33
  32. data/lib/generators/media_types/serialization/api_viewer/templates/template_controller.rb +0 -23
  33. data/lib/media_types/serialization/media_type/register.rb +0 -4
  34. data/lib/media_types/serialization/migrations_command.rb +0 -38
  35. data/lib/media_types/serialization/migrations_support.rb +0 -50
  36. data/lib/media_types/serialization/mime_type_support.rb +0 -64
  37. data/lib/media_types/serialization/no_content_type_given.rb +0 -11
  38. data/lib/media_types/serialization/no_media_type_serializers.rb +0 -11
  39. data/lib/media_types/serialization/no_serializer_for_content_type.rb +0 -15
  40. data/lib/media_types/serialization/renderer.rb +0 -41
  41. data/lib/media_types/serialization/renderer/register.rb +0 -4
  42. data/lib/media_types/serialization/wrapper.rb +0 -13
  43. data/lib/media_types/serialization/wrapper/html_wrapper.rb +0 -45
  44. data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +0 -59
  45. data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +0 -59
  46. data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +0 -55
  47. data/lib/media_types/serialization/wrapper_support.rb +0 -38
@@ -1,126 +1,150 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'uri'
4
-
5
- require 'http_headers/link'
6
- require 'http_headers/utils/list'
7
-
8
- require 'media_types/serialization/mime_type_support'
9
- require 'media_types/serialization/migrations_support'
10
- require 'media_types/serialization/wrapper_support'
3
+ require 'media_types/serialization/error'
4
+ require 'media_types/serialization/fake_validator'
5
+ require 'media_types/serialization/serialization_registration'
6
+ require 'media_types/serialization/serialization_dsl'
11
7
 
12
8
  module MediaTypes
13
9
  module Serialization
14
10
  class Base
15
- include MimeTypeSupport
16
- include MigrationsSupport
17
- include WrapperSupport
11
+ module ClassMethods
12
+ def unvalidated(prefix)
13
+ self.serializer_validated = false
14
+ self.serializer_validator = FakeValidator.new(prefix)
15
+ self.serializer_input_registration = SerializationRegistration.new(:input)
16
+ self.serializer_output_registration = SerializationRegistration.new(:output)
17
+ end
18
18
 
19
- attr_reader :current_media_type, :current_view, :serializable
19
+ def validator(validator = nil)
20
+ raise NoValidatorSetError if !defined? serializer_validator && validator.nil?
21
+ return serializer_validator if validator.nil?
20
22
 
21
- def initialize(serializable, media_type:, view: nil, context:)
22
- self.context = context
23
- self.current_media_type = media_type
24
- self.current_view = view
23
+ self.serializer_validated = true
24
+ self.serializer_validator = validator
25
+ self.serializer_input_registration = SerializationRegistration.new(:input)
26
+ self.serializer_output_registration = SerializationRegistration.new(:output)
27
+ end
25
28
 
26
- set(serializable)
27
- end
29
+ def disable_wildcards
30
+ self.serializer_disable_wildcards = true
31
+ end
28
32
 
29
- def to_link_header
30
- entries = header_links(view: current_view).each_with_index.map do |(rel, links), index|
31
- links = [links] unless links.is_a?(Array)
33
+ def enable_wildcards
34
+ self.serializer_disable_wildcards = false
35
+ end
36
+
37
+ def output(view: nil, version: nil, versions: nil, &block)
38
+ versions = [version] if versions.nil?
39
+ raise VersionsNotAnArrayError unless versions.is_a? Array
40
+
41
+ raise ValidatorNotSpecifiedError, :output if serializer_validator.nil?
32
42
 
33
- links.map do |opts|
34
- next unless opts.is_a?(String) || opts.try(:[], :href)
35
- href = opts.is_a?(String) ? opts : opts.delete(:href)
36
- parameters = { rel: rel }.merge(opts.is_a?(String) ? {} : opts)
43
+ versions.each do |v|
44
+ validator = serializer_validator.view(view).version(v)
45
+ validator.override_suffix(:json) unless serializer_validated
37
46
 
38
- HttpHeaders::Link::Entry.new("<#{href}>", index: index, parameters: parameters)
47
+ serializer_output_registration.register_block(self, validator, v, block, false, wildcards: !self.serializer_disable_wildcards)
39
48
  end
40
- end.flatten.compact
49
+ end
41
50
 
42
- return nil unless entries.present?
43
- HttpHeaders::Utils::List.to_header(entries)
44
- end
51
+ def output_raw(view: nil, version: nil, versions: nil, &block)
52
+ versions = [version] if versions.nil?
53
+ raise VersionsNotAnArrayError unless versions.is_a? Array
45
54
 
46
- COMMON_DERIVED_CALLERS = [:to_h, :to_hash, :to_json, :to_text, :to_xml, :to_html, :to_body, :extract_self].freeze
55
+ raise ValidatorNotSpecifiedError, :output if serializer_validator.nil?
47
56
 
48
- def method_missing(symbol, *args, &block)
49
- if COMMON_DERIVED_CALLERS.include?(symbol)
50
- raise NotImplementedError, format(
51
- 'In %<class>s, %<symbol>s is not implemented. ' \
52
- 'Implement it or deny the MediaType[s] %<media_types>s for %<model>s',
53
- symbol: symbol,
54
- class: self.class.name,
55
- model: serializable.class.name,
56
- media_types: self.class.media_types(view: '[view]').to_s
57
- )
57
+ versions.each do |v|
58
+ validator = serializer_validator.view(view).version(v)
59
+
60
+ serializer_output_registration.register_block(self, validator, v, block, true, wildcards: !self.serializer_disable_wildcards)
61
+ end
58
62
  end
59
63
 
60
- super
61
- end
64
+ def output_alias(media_type_identifier, view: nil, hide_variant: false)
65
+ validator = serializer_validator.view(view)
66
+ victim_identifier = validator.identifier
62
67
 
63
- def respond_to_missing?(method_name, include_private = false)
64
- if COMMON_DERIVED_CALLERS.include?(method_name)
65
- return false
68
+ serializer_output_registration.register_alias(self, media_type_identifier, victim_identifier, false, hide_variant, wildcards: !self.serializer_disable_wildcards)
66
69
  end
67
70
 
68
- super
69
- end
71
+ def output_alias_optional(media_type_identifier, view: nil, hide_variant: false)
72
+ validator = serializer_validator.view(view)
73
+ victim_identifier = validator.identifier
70
74
 
71
- def header_links(view: current_view)
72
- extract_view_links(view: view)
73
- end
75
+ serializer_output_registration.register_alias(self, media_type_identifier, victim_identifier, true, hide_variant, wildcards: !self.serializer_disable_wildcards)
76
+ end
74
77
 
75
- def set(serializable)
76
- self.serializable = serializable
77
- self
78
- end
78
+ def input(view: nil, version: nil, versions: nil, &block)
79
+ versions = [version] if versions.nil?
80
+ raise VersionsNotAnArrayError unless versions.is_a? Array
79
81
 
80
- protected
82
+ raise ValidatorNotSpecifiedError, :input if serializer_validator.nil?
81
83
 
82
- attr_accessor :context
83
- attr_writer :current_media_type, :current_view, :serializable
84
+ versions.each do |v|
85
+ validator = serializer_validator.view(view).version(v)
86
+ validator.override_suffix(:json) unless serializer_validated
84
87
 
85
- def extract_links
86
- {}
87
- end
88
+ serializer_input_registration.register_block(self, validator, v, block, false)
89
+ end
90
+ end
88
91
 
89
- def extract_set_links(view: current_view)
90
- {}
91
- end
92
+ def input_raw(view: nil, version: nil, versions: nil, &block)
93
+ versions = [version] if versions.nil?
94
+ raise VersionsNotAnArrayError unless versions.is_a? Array
92
95
 
93
- def extract_view_links(view: current_view)
94
- return extract_set_links(view: view) if view == ::MediaTypes::INDEX_VIEW
95
- return extract_set_links(view: view) if view == ::MediaTypes::COLLECTION_VIEW
96
+ raise ValidatorNotSpecifiedError, :input if serializer_validator.nil?
96
97
 
97
- extract_links
98
- end
98
+ versions.each do |v|
99
+ validator = serializer_validator.view(view).version(v)
100
+
101
+ serializer_input_registration.register_block(self, validator, v, block, true)
102
+ end
103
+ end
104
+
105
+ def input_alias(media_type_identifier, view: nil)
106
+ validator = serializer_validator.view(view)
107
+ victim_identifier = validator.identifier
108
+
109
+ serializer_input_registration.register_alias(self, media_type_identifier, victim_identifier, false)
110
+ end
111
+
112
+ def input_alias_optional(media_type_identifier, view: nil)
113
+ validator = serializer_validator.view(view)
114
+ victim_identifier = validator.identifier
115
+
116
+ serializer_input_registration.register_alias(self, media_type_identifier, victim_identifier, true)
117
+ end
118
+
119
+ def serialize(victim, media_type_identifier, context, dsl: nil, raw: nil)
120
+ dsl ||= SerializationDSL.new(self, context: context)
121
+ serializer_output_registration.call(victim, media_type_identifier.to_s, context, dsl: dsl, raw: raw)
122
+ end
123
+
124
+ def deserialize(victim, media_type_identifier, context)
125
+ serializer_input_registration.call(victim, media_type_identifier, context)
126
+ end
99
127
 
100
- def extract(extractable, *keys)
101
- return {} unless keys.present?
102
- extractable.slice(*keys)
103
- rescue TypeError => err
104
- raise TypeError, format(
105
- '[serializer] failed to slice keys to extract. Given keys: %<keys>s. Extractable: %<extractable>s' \
106
- 'Error: %<error>s',
107
- keys: keys,
108
- extractable: extractable,
109
- error: err
110
- )
128
+ def outputs_for(views:)
129
+ serializer_output_registration.filter(views: views)
130
+ end
131
+
132
+ def inputs_for(views:)
133
+ serializer_input_registration.filter(views: views)
134
+ end
111
135
  end
112
136
 
113
- def resolve_file_url(url)
114
- return url if !url || URI(url).absolute?
115
-
116
- format(
117
- 'https://%<host>s:%<port>s%<path>s',
118
- host: context.default_url_options[:host],
119
- port: context.default_url_options[:port],
120
- path: url
121
- )
122
- rescue URI::InvalidURIError
123
- url
137
+ def self.inherited(subclass)
138
+ subclass.extend(ClassMethods)
139
+ subclass.instance_eval do
140
+ class << self
141
+ attr_accessor :serializer_validated
142
+ attr_accessor :serializer_validator
143
+ attr_accessor :serializer_input_registration
144
+ attr_accessor :serializer_output_registration
145
+ attr_accessor :serializer_disable_wildcards
146
+ end
147
+ end
124
148
  end
125
149
  end
126
150
  end
@@ -3,5 +3,191 @@ module MediaTypes
3
3
  module Serialization
4
4
  class Error < StandardError
5
5
  end
6
+
7
+ class ControlFlowError < Error
8
+ end
9
+
10
+ class InputNotAcceptableError < ControlFlowError
11
+ def initialize
12
+ super('Content-Type provided in the request is not acceptable.')
13
+ end
14
+ end
15
+
16
+ class RuntimeError < Error
17
+ end
18
+
19
+ class NoInputReceivedError < RuntimeError
20
+ def initialize
21
+ super('No Content-Type specified in request.')
22
+ end
23
+ end
24
+
25
+ class InputValidationFailedError < RuntimeError
26
+ def initialize(inner)
27
+ @inner = inner
28
+ super(inner.message)
29
+ end
30
+ end
31
+
32
+ class OutputValidationFailedError < RuntimeError
33
+ def initialize(inner)
34
+ @inner = inner
35
+ super(inner.message)
36
+ end
37
+ end
38
+
39
+ class CollectionTypeError < RuntimeError
40
+ def initialize(type)
41
+ super("Unable to serialize the collection. Input was of type #{type} but I expected an Array.")
42
+ end
43
+ end
44
+
45
+ class UnsupportedMediaTypeError < RuntimeError
46
+ def initialize(available)
47
+ super("The controller was unable to process your Content-Type. Please use one of: [#{available.join(', ')}]")
48
+ end
49
+ end
50
+
51
+ class ConfigurationError < Error
52
+ end
53
+
54
+ class CannotDecodeOutputError < ConfigurationError
55
+ def initialize
56
+ super('Unable to call decode on an output registration.')
57
+ end
58
+ end
59
+
60
+ class ValidatorNotSpecifiedError < ConfigurationError
61
+ def initialize(inout)
62
+ super("Serializer tried to define an #{inout} without first specifying a validator using either the validator function or unvalidated function. Please call one of those before defining #{inout}s.")
63
+ end
64
+ end
65
+
66
+ class ValidatorNotDefinedError < ConfigurationError
67
+ def initialize(identifier, inout)
68
+ super("Serializer tried to define an #{inout} using the media type identifier #{identifier}, but no validation has been set up for that identifier. Please add it to the validator.")
69
+ end
70
+ end
71
+
72
+ class UnbackedAliasDefinitionError < ConfigurationError
73
+ def initialize(identifier, inout)
74
+ super(
75
+ "Serializer tried to define an #{inout}_alias that points to the media type identifier #{identifier} but no such #{inout} has been defined yet. Please move the #{inout} definition above the alias.\n\n" \
76
+ "Move the #{inout} definition above the alias:\n" \
77
+ "\n" \
78
+ "class MySerializer < MediaTypes::Serialization::Base\n" \
79
+ "#...\n" \
80
+ "#{inout} do\n" \
81
+ " # ...\n" \
82
+ "end\n" \
83
+ "\n" \
84
+ "#{inout}_alias 'text/html'\n" \
85
+ "# ^----- move here\n" \
86
+ 'end'
87
+ )
88
+ end
89
+ end
90
+
91
+ class VersionedAliasDefinitionError < ConfigurationError
92
+ def initialize(identifier, inout, prefix_match)
93
+ super(
94
+ "Serializer tried to define an #{inout}_alias that points to the media type identifier #{identifier} but no such #{inout} has been defined yet. An #{inout} named #{prefix_match} was found. Often this can be fixed by providing an #{inout} with a nil version."
95
+ )
96
+ end
97
+ end
98
+
99
+ class DuplicateDefinitionError < ConfigurationError
100
+ def initialize(identifier, inout)
101
+ super("Serializer tried to define an #{inout} using the media type identifier #{identifier}, but another #{inout} was already defined with that identifier. Please remove one of the two.")
102
+ end
103
+ end
104
+
105
+ class DuplicateUsageError < ConfigurationError
106
+ def initialize(identifier, inout, serializer1, serializer2)
107
+ super("Controller tried to use two #{inout} serializers (#{serializer1}, #{serializer2}) that both have a non-optional #{inout} defined for the media type identifier #{identifier}. Please remove one of the two or filter them more specifically.")
108
+ end
109
+ end
110
+
111
+ class UnregisteredMediaTypeUsageError < ConfigurationError
112
+ def initialize(identifier, available)
113
+ super("A serialization or deserialization method was called using a media type identifier '#{identifier}' but no such identifier has been registered yet. Available media types: [#{available.join ', '}]")
114
+ end
115
+ end
116
+
117
+ class UnmatchedSerializerError < ConfigurationError
118
+ def initialize(serializer)
119
+ super("Called render_media with a resolved serializer that was not specified in the do block. Please add a 'serializer #{serializer.name}, <value>' entry.")
120
+ end
121
+ end
122
+
123
+ class VersionsNotAnArrayError < ConfigurationError
124
+ def initialize
125
+ super('Tried to create an input or output with a versions: parameter that is set to something that is not an array. Please use the version: parameter or conver the current value to an array.')
126
+ end
127
+ end
128
+ class ViewsNotAnArrayError < ConfigurationError
129
+ def initialize
130
+ super('Tried to create an input or output with a views: parameter that is set to something that is not an array. Please use the view: parameter or conver the current value to an array.')
131
+ end
132
+ end
133
+
134
+ class NoValidatorSetError < ConfigurationError
135
+ def initialize
136
+ super("Unable to return validator as no validator has been set. Either someone tried to fetch the currently defined validator or someone tried to set the validator to 'nil'.")
137
+ end
138
+ end
139
+
140
+ class NoSelfLinkProvidedError < ConfigurationError
141
+ def initialize(media_type_identifier)
142
+ super("Tried to render an index of '#{media_type_identifier}' elements but the serializer did not return a :self link for me to use. Please call 'link rel: :self, href: 'https://...' in the #{media_type_identifier} serializer.")
143
+ end
144
+ end
145
+
146
+ class MultipleSelfLinksProvidedError < ConfigurationError
147
+ def initialize(media_type_identifier)
148
+ super("Tried to render an index of '#{media_type_identifier}' elements but the serializer returned more than one :self link. Please make sure to only call 'link rel: :self, ...' once in the #{media_type_identifier} serializer.")
149
+ end
150
+ end
151
+
152
+ class ArrayInViewParameterError < ConfigurationError
153
+ def initialize(function)
154
+ super("Tried to call #{function} with an array in the view: parameter. Please use the views: parameter instead.")
155
+ end
156
+ end
157
+
158
+ class SerializersNotFrozenError < ConfigurationError
159
+ def initialize
160
+ super("Unable to serialize or deserialize objects with unfrozen serializers. Please add 'freeze_io!' to the controller definition.")
161
+ end
162
+ end
163
+
164
+ class SerializersAlreadyFrozenError < ConfigurationError
165
+ def initialize
166
+ super("Unable to add a serializer when they are already frozen. Please make sure to call 'freeze_io!' last.")
167
+ end
168
+ end
169
+
170
+ class UnableToRefreezeError < ConfigurationError
171
+ def initialize
172
+ super("Freeze was called while the serializers are already frozen. Please make sure to only call 'freeze_io!' once.")
173
+ end
174
+ end
175
+
176
+ class NoOutputSerializersDefinedError < ConfigurationError
177
+ def intialize
178
+ super("Called freeze_io! without any allowed output serializers. Users won't be able to make any requests. Please make sure to add at least one allow_output_serializer call to your controller.")
179
+ end
180
+ end
181
+
182
+ class AddedEmptyOutputSerializer < ConfigurationError
183
+ def initialize(name)
184
+ super("A serializer with name '#{name}' was just added to the controller but it contained no output definitions. Usually this is due to using the wrong view parameter when adding it.")
185
+ end
186
+ end
187
+ class AddedEmptyInputSerializer < ConfigurationError
188
+ def initialize(name)
189
+ super("A serializer with name '#{name}' was just added to the controller but it contained no input definitions. Usually this is due to using the wrong view parameter when adding it.")
190
+ end
191
+ end
6
192
  end
7
193
  end
@@ -0,0 +1,52 @@
1
+
2
+ # Validator that accepts all input
3
+ class FakeValidator
4
+ def initialize(prefix, view = nil, version = nil, suffixes = {})
5
+ self.prefix = prefix
6
+ self.suffixes = suffixes
7
+ self.internal_view = view
8
+ self.internal_version = version
9
+ end
10
+
11
+ UNSET = Object.new
12
+
13
+ def view(view = UNSET)
14
+ return self.internal_view if view == UNSET
15
+ FakeValidator.new(prefix, view, internal_version, suffixes)
16
+ end
17
+
18
+ def version(version = UNSET)
19
+ return self.internal_version if version == UNSET
20
+ FakeValidator.new(prefix, internal_view, version, suffixes)
21
+ end
22
+
23
+ def override_suffix(suffix)
24
+ suffixes[[internal_view, internal_version]] = suffix
25
+ end
26
+
27
+ def identifier
28
+ suffix = suffixes[[internal_view, internal_version]] || ''
29
+ result = prefix
30
+ result += '.v' + internal_version.to_s unless internal_version.nil?
31
+ result += '.' + internal_view.to_s unless internal_view.nil?
32
+ result += '+' + suffix.to_s unless suffix.empty?
33
+
34
+ result
35
+ end
36
+
37
+ def validatable?
38
+ true
39
+ end
40
+
41
+ def validate!(_)
42
+ true
43
+ end
44
+
45
+ attr_accessor :prefix
46
+ attr_accessor :suffixes
47
+
48
+ protected
49
+
50
+ attr_accessor :internal_view
51
+ attr_accessor :internal_version
52
+ end