media_types-serialization 0.8.0 → 1.1.0

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