media_types-serialization 0.8.1 → 1.0.1

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