media_types-serialization 0.8.1 → 1.0.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +10 -1
  3. data/.gitignore +12 -12
  4. data/.idea/.rakeTasks +5 -5
  5. data/.idea/inspectionProfiles/Project_Default.xml +5 -5
  6. data/.idea/runConfigurations/test.xml +19 -19
  7. data/CHANGELOG.md +18 -0
  8. data/CODE_OF_CONDUCT.md +74 -74
  9. data/Gemfile +4 -4
  10. data/Gemfile.lock +58 -61
  11. data/LICENSE.txt +21 -21
  12. data/README.md +640 -173
  13. data/Rakefile +10 -10
  14. data/bin/console +14 -14
  15. data/bin/setup +8 -8
  16. data/lib/media_types/problem.rb +64 -0
  17. data/lib/media_types/serialization.rb +431 -172
  18. data/lib/media_types/serialization/base.rb +111 -91
  19. data/lib/media_types/serialization/error.rb +178 -0
  20. data/lib/media_types/serialization/fake_validator.rb +52 -0
  21. data/lib/media_types/serialization/serialization_dsl.rb +117 -0
  22. data/lib/media_types/serialization/serialization_registration.rb +235 -0
  23. data/lib/media_types/serialization/serializers/api_viewer.rb +133 -0
  24. data/lib/media_types/serialization/serializers/common_css.rb +168 -0
  25. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -0
  26. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -0
  27. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -0
  28. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +89 -0
  29. data/lib/media_types/serialization/serializers/problem_serializer.rb +87 -0
  30. data/lib/media_types/serialization/version.rb +1 -1
  31. data/media_types-serialization.gemspec +50 -50
  32. metadata +40 -43
  33. data/.travis.yml +0 -17
  34. data/lib/generators/media_types/serialization/api_viewer/api_viewer_generator.rb +0 -25
  35. data/lib/generators/media_types/serialization/api_viewer/templates/api_viewer.html.erb +0 -98
  36. data/lib/generators/media_types/serialization/api_viewer/templates/initializer.rb +0 -33
  37. data/lib/generators/media_types/serialization/api_viewer/templates/template_controller.rb +0 -23
  38. data/lib/media_types/serialization/media_type/register.rb +0 -4
  39. data/lib/media_types/serialization/migrations_command.rb +0 -38
  40. data/lib/media_types/serialization/migrations_support.rb +0 -50
  41. data/lib/media_types/serialization/mime_type_support.rb +0 -64
  42. data/lib/media_types/serialization/no_content_type_given.rb +0 -11
  43. data/lib/media_types/serialization/no_media_type_serializers.rb +0 -11
  44. data/lib/media_types/serialization/no_serializer_for_content_type.rb +0 -15
  45. data/lib/media_types/serialization/renderer.rb +0 -41
  46. data/lib/media_types/serialization/renderer/register.rb +0 -4
  47. data/lib/media_types/serialization/wrapper.rb +0 -13
  48. data/lib/media_types/serialization/wrapper/html_wrapper.rb +0 -45
  49. data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +0 -61
  50. data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +0 -61
  51. data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +0 -55
  52. data/lib/media_types/serialization/wrapper_support.rb +0 -38
@@ -1,126 +1,146 @@
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
32
+
33
+ def output(view: nil, version: nil, versions: nil, &block)
34
+ versions = [version] if versions.nil?
35
+ raise VersionsNotAnArrayError unless versions.is_a? Array
28
36
 
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)
37
+ raise ValidatorNotSpecifiedError, :output if serializer_validator.nil?
32
38
 
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)
39
+ versions.each do |v|
40
+ validator = serializer_validator.view(view).version(v)
41
+ validator.override_suffix(:json) unless serializer_validated
37
42
 
38
- HttpHeaders::Link::Entry.new("<#{href}>", index: index, parameters: parameters)
43
+ serializer_output_registration.register_block(self, validator, v, block, false, wildcards: !self.serializer_disable_wildcards)
39
44
  end
40
- end.flatten.compact
45
+ end
41
46
 
42
- return nil unless entries.present?
43
- HttpHeaders::Utils::List.to_header(entries)
44
- end
47
+ def output_raw(view: nil, version: nil, versions: nil, &block)
48
+ versions = [version] if versions.nil?
49
+ raise VersionsNotAnArrayError unless versions.is_a? Array
50
+
51
+ raise ValidatorNotSpecifiedError, :output if serializer_validator.nil?
45
52
 
46
- COMMON_DERIVED_CALLERS = [:to_h, :to_hash, :to_json, :to_text, :to_xml, :to_html, :to_body, :extract_self].freeze
53
+ versions.each do |v|
54
+ validator = serializer_validator.view(view).version(v)
47
55
 
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
- )
56
+ serializer_output_registration.register_block(self, validator, v, block, true, wildcards: !self.serializer_disable_wildcards)
57
+ end
58
58
  end
59
59
 
60
- super
61
- end
60
+ def output_alias(media_type_identifier, view: nil)
61
+ validator = serializer_validator.view(view)
62
+ victim_identifier = validator.identifier
62
63
 
63
- def respond_to_missing?(method_name, include_private = false)
64
- if COMMON_DERIVED_CALLERS.include?(method_name)
65
- return false
64
+ serializer_output_registration.register_alias(self, media_type_identifier, victim_identifier, false, wildcards: !self.serializer_disable_wildcards)
66
65
  end
67
66
 
68
- super
69
- end
67
+ def output_alias_optional(media_type_identifier, view: nil)
68
+ validator = serializer_validator.view(view)
69
+ victim_identifier = validator.identifier
70
70
 
71
- def header_links(view: current_view)
72
- extract_view_links(view: view)
73
- end
71
+ serializer_output_registration.register_alias(self, media_type_identifier, victim_identifier, true, wildcards: !self.serializer_disable_wildcards)
72
+ end
74
73
 
75
- def set(serializable)
76
- self.serializable = serializable
77
- self
78
- end
74
+ def input(view: nil, version: nil, versions: nil, &block)
75
+ versions = [version] if versions.nil?
76
+ raise VersionsNotAnArrayError unless versions.is_a? Array
79
77
 
80
- protected
78
+ raise ValidatorNotSpecifiedError, :input if serializer_validator.nil?
81
79
 
82
- attr_accessor :context
83
- attr_writer :current_media_type, :current_view, :serializable
80
+ versions.each do |v|
81
+ validator = serializer_validator.view(view).version(v)
82
+ validator.override_suffix(:json) unless serializer_validated
84
83
 
85
- def extract_links
86
- {}
87
- end
84
+ serializer_input_registration.register_block(self, validator, v, block, false)
85
+ end
86
+ end
88
87
 
89
- def extract_set_links(view: current_view)
90
- {}
91
- end
88
+ def input_raw(view: nil, version: nil, versions: nil, &block)
89
+ versions = [version] if versions.nil?
90
+ raise VersionsNotAnArrayError unless versions.is_a? Array
92
91
 
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
92
+ raise ValidatorNotSpecifiedError, :input if serializer_validator.nil?
96
93
 
97
- extract_links
98
- end
94
+ versions.each do |v|
95
+ validator = serializer_validator.view(view).version(v)
96
+
97
+ serializer_input_registration.register_block(self, validator, v, block, true)
98
+ end
99
+ end
100
+
101
+ def input_alias(media_type_identifier, view: nil)
102
+ validator = serializer_validator.view(view)
103
+ victim_identifier = validator.identifier
104
+
105
+ serializer_input_registration.register_alias(self, media_type_identifier, victim_identifier, false)
106
+ end
107
+
108
+ def input_alias_optional(media_type_identifier, view: nil)
109
+ validator = serializer_validator.view(view)
110
+ victim_identifier = validator.identifier
111
+
112
+ serializer_input_registration.register_alias(self, media_type_identifier, victim_identifier, true)
113
+ end
99
114
 
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
- )
115
+ def serialize(victim, media_type_identifier, context, dsl: nil, raw: nil)
116
+ dsl ||= SerializationDSL.new(self, context: context)
117
+ serializer_output_registration.call(victim, media_type_identifier.to_s, context, dsl: dsl, raw: raw)
118
+ end
119
+
120
+ def deserialize(victim, media_type_identifier, context)
121
+ serializer_input_registration.call(victim, media_type_identifier, context)
122
+ end
123
+
124
+ def outputs_for(views:)
125
+ serializer_output_registration.filter(views: views)
126
+ end
127
+
128
+ def inputs_for(views:)
129
+ serializer_input_registration.filter(views: views)
130
+ end
111
131
  end
112
132
 
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
133
+ def self.inherited(subclass)
134
+ subclass.extend(ClassMethods)
135
+ subclass.instance_eval do
136
+ class << self
137
+ attr_accessor :serializer_validated
138
+ attr_accessor :serializer_validator
139
+ attr_accessor :serializer_input_registration
140
+ attr_accessor :serializer_output_registration
141
+ attr_accessor :serializer_disable_wildcards
142
+ end
143
+ end
124
144
  end
125
145
  end
126
146
  end
@@ -3,5 +3,183 @@ 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 output definition above the alias:\n" \
77
+ "\n" \
78
+ "class MySerializer < MediaTypes::Serialization::Base\n" \
79
+ "#...\n" \
80
+ "output do\n" \
81
+ " # ...\n" \
82
+ "end\n" \
83
+ "\n" \
84
+ "output_alias 'text/html'\n" \
85
+ "# ^----- move here\n" \
86
+ 'end'
87
+ )
88
+ end
89
+ end
90
+
91
+ class DuplicateDefinitionError < ConfigurationError
92
+ def initialize(identifier, inout)
93
+ 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.")
94
+ end
95
+ end
96
+
97
+ class DuplicateUsageError < ConfigurationError
98
+ def initialize(identifier, inout, serializer1, serializer2)
99
+ 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.")
100
+ end
101
+ end
102
+
103
+ class UnregisteredMediaTypeUsageError < ConfigurationError
104
+ def initialize(identifier, available)
105
+ 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 ', '}]")
106
+ end
107
+ end
108
+
109
+ class UnmatchedSerializerError < ConfigurationError
110
+ def initialize(serializer)
111
+ super("Called render_media with a resolved serializer that was not specified in the do block. Please add a 'serializer #{serializer.name}, <value>' entry.")
112
+ end
113
+ end
114
+
115
+ class VersionsNotAnArrayError < ConfigurationError
116
+ def initialize
117
+ 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.')
118
+ end
119
+ end
120
+ class ViewsNotAnArrayError < ConfigurationError
121
+ def initialize
122
+ 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.')
123
+ end
124
+ end
125
+
126
+ class NoValidatorSetError < ConfigurationError
127
+ def initialize
128
+ 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'.")
129
+ end
130
+ end
131
+
132
+ class NoSelfLinkProvidedError < ConfigurationError
133
+ def initialize(media_type_identifier)
134
+ 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.")
135
+ end
136
+ end
137
+
138
+ class MultipleSelfLinksProvidedError < ConfigurationError
139
+ def initialize(media_type_identifier)
140
+ 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.")
141
+ end
142
+ end
143
+
144
+ class ArrayInViewParameterError < ConfigurationError
145
+ def initialize(function)
146
+ super("Tried to call #{function} with an array in the view: parameter. Please use the views: parameter instead.")
147
+ end
148
+ end
149
+
150
+ class SerializersNotFrozenError < ConfigurationError
151
+ def initialize
152
+ super("Unable to serialize or deserialize objects with unfrozen serializers. Please add 'freeze_io!' to the controller definition.")
153
+ end
154
+ end
155
+
156
+ class SerializersAlreadyFrozenError < ConfigurationError
157
+ def initialize
158
+ super("Unable to add a serializer when they are already frozen. Please make sure to call 'freeze_io!' last.")
159
+ end
160
+ end
161
+
162
+ class UnableToRefreezeError < ConfigurationError
163
+ def initialize
164
+ super("Freeze was called while the serializers are already frozen. Please make sure to only call 'freeze_io!' once.")
165
+ end
166
+ end
167
+
168
+ class NoOutputSerializersDefinedError < ConfigurationError
169
+ def intialize
170
+ 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.")
171
+ end
172
+ end
173
+
174
+ class AddedEmptyOutputSerializer < ConfigurationError
175
+ def initialize(name)
176
+ 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.")
177
+ end
178
+ end
179
+ class AddedEmptyInputSerializer < ConfigurationError
180
+ def initialize(name)
181
+ 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.")
182
+ end
183
+ end
6
184
  end
7
185
  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 += '.' + internal_view.to_s unless internal_view.nil?
31
+ result += '.v' + internal_version.to_s unless internal_version.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