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
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module MediaTypes
6
+ module Serialization
7
+ # Provides the serialization convenience methods
8
+ class SerializationDSL < SimpleDelegator
9
+ def initialize(serializer, links = [], vary = ['Accept'], value = {}, context: nil)
10
+ self.serialization_dsl_result = value
11
+ @serialization_links = links
12
+ @serialization_context = context
13
+ @serialization_vary = vary
14
+ super(serializer)
15
+ end
16
+
17
+ attr_accessor :serialization_dsl_result
18
+
19
+ def attribute(key, value = {}, &block)
20
+ unless block.nil?
21
+ subcontext = SerializationDSL.new(__getobj__, @serialization_links, @serialization_vary, value, context: @serialization_context)
22
+ value = subcontext.instance_exec(&block)
23
+ end
24
+
25
+ serialization_dsl_result[key] = value
26
+
27
+ serialization_dsl_result
28
+ end
29
+
30
+ def link(rel, href:, emit_header: true, **attributes)
31
+ serialization_dsl_result[:_links] = {} unless serialization_dsl_result.has_key? :_links
32
+
33
+ link = {
34
+ href: href,
35
+ rel: rel,
36
+ }
37
+ link = link.merge(attributes)
38
+
39
+ json = {
40
+ href: href,
41
+ }
42
+ json = json.merge(attributes)
43
+
44
+ @serialization_links.append(link) if emit_header
45
+ serialization_dsl_result[:_links][rel] = json
46
+
47
+ serialization_dsl_result
48
+ end
49
+
50
+ def index(array, serializer = __getobj__, version:, view: nil)
51
+ raise CollectionTypeError, array.class.name unless array.is_a? Array
52
+
53
+ links = []
54
+ identifier = serializer.serializer_validator.view(view).version(version).identifier
55
+
56
+ array.each do |e|
57
+ child_links = []
58
+ context = SerializationDSL.new(__getobj__, child_links, context: @serialization_context)
59
+ serializer.serialize(e, identifier, @serialization_context, dsl: context)
60
+
61
+ self_links = child_links.select { |l| l[:rel] == :self }
62
+ raise NoSelfLinkProvidedError, identifier unless self_links.any?
63
+ raise MultipleSelfLinksProvidedError, identifier if self_links.length > 1
64
+
65
+ links.append(self_links.first.reject { |k, _| k == :rel } )
66
+ end
67
+
68
+ serialization_dsl_result[:_index] = links
69
+
70
+ serialization_dsl_result
71
+ end
72
+
73
+ def collection(array, serializer = __getobj__, version:, view: nil, &block)
74
+ raise CollectionTypeError, array.class.name unless array.is_a? Array
75
+
76
+ identifier = serializer.serializer_validator.view(view).version(version).identifier
77
+
78
+ rendered = []
79
+
80
+ array.each do |e|
81
+ context = SerializationDSL.new(__getobj__, [], @serialization_vary, context: @serialization_context)
82
+ result = serializer.serialize(e, identifier, @serialization_context, dsl: context, raw: true)
83
+
84
+ result = block.call(result) unless block.nil?
85
+
86
+ rendered.append(result)
87
+ end
88
+
89
+ serialization_dsl_result[:_embedded] = rendered
90
+
91
+ serialization_dsl_result
92
+ end
93
+
94
+ def hidden(&block)
95
+ context = SerializationDSL.new(__getobj__, @serialization_links, context: @serialization_context)
96
+ context.instance_exec(&block)
97
+
98
+ serialization_dsl_result
99
+ end
100
+
101
+ def render_view(name, context:, **args)
102
+ context.render_to_string(name, **args)
103
+ end
104
+
105
+ def emit
106
+ serialization_dsl_result
107
+ end
108
+
109
+ def object(&block)
110
+ context = SerializationDSL.new(__getobj__, @serialization_links, @serialization_vary, context: @serialization_context)
111
+ context.instance_exec(&block)
112
+
113
+ context.serialization_dsl_result
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/serialization/error'
4
+ require 'media_types'
5
+
6
+ module MediaTypes
7
+ module Serialization
8
+ # A collection that manages media type identifier registrations
9
+ class SerializationRegistration
10
+ def initialize(direction)
11
+ self.registrations = {}
12
+ self.inout = direction
13
+ end
14
+
15
+ attr_accessor :registrations, :inout
16
+
17
+ def has?(identifier)
18
+ registrations.key? identifier
19
+ end
20
+
21
+ def register_block(serializer, validator, version, block, raw, wildcards: true)
22
+ identifier = validator.identifier
23
+
24
+ raise DuplicateDefinitionError.new(identifier, inout) if registrations.key? identifier
25
+
26
+ raise ValidatorNotDefinedError.new(identifier, inout) unless raw || validator.validatable?
27
+
28
+ registration = SerializationBlockRegistration.new serializer, inout, validator, identifier, version, block, raw
29
+ registrations[identifier] = registration
30
+
31
+ register_wildcards(identifier, registration) if wildcards && inout == :output
32
+ end
33
+
34
+ def register_alias(serializer, alias_identifier, target_identifier, optional, hide_variant, wildcards: true)
35
+ raise DuplicateDefinitionError.new(alias_identifier, inout) if registrations.key? alias_identifier
36
+
37
+ unless registrations.key? target_identifier
38
+ potential_match = registrations.keys.find do |k|
39
+ k.starts_with? target_identifier
40
+ end
41
+ raise VersionedAliasDefinitionError.new(target_identifier, inout, potential_match) unless potential_match.nil?
42
+ raise UnbackedAliasDefinitionError.new(target_identifier, inout)
43
+ end
44
+
45
+ target = registrations[target_identifier]
46
+
47
+ result_content_type = alias_identifier
48
+ result_content_type += "; variant=#{target_identifier}" unless hide_variant
49
+
50
+ registration = SerializationAliasRegistration.new serializer, inout, target.validator, result_content_type, target, optional, hide_variant
51
+ registrations[alias_identifier] = registration
52
+
53
+ register_wildcards(alias_identifier, registration) if wildcards && inout == :output
54
+ end
55
+
56
+ def merge(other)
57
+ raise Error, 'Trying to merge two SerializationRegistration objects with a different direction.' unless inout == other.inout
58
+
59
+ result = SerializationRegistration.new(inout)
60
+
61
+ prev_keys = Set.new(registrations.keys)
62
+ new_keys = Set.new(other.registrations.keys)
63
+ overlap = prev_keys & new_keys
64
+
65
+ result.registrations = registrations.merge(other.registrations)
66
+ overlap.each do |identifier|
67
+ prev_item = registrations[identifier]
68
+ new_item = other.registrations[identifier]
69
+ merge_result = prev_item.merge(new_item)
70
+
71
+ raise DuplicateUsageError.new(identifier, inout, prev_item.serializer, new_item.serializer) if merge_result.nil?
72
+
73
+ result.registrations[identifier] = merge_result
74
+ end
75
+
76
+ result
77
+ end
78
+
79
+ def decode(victim, media_type, context)
80
+ registration = registrations[media_type]
81
+ raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
82
+
83
+ registration.decode(victim, context)
84
+ end
85
+
86
+ def call(victim, media_type, context, dsl: nil, raw: nil)
87
+ registration = registrations[media_type]
88
+ raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
89
+
90
+ registration.call(victim, context, dsl: dsl, raw: raw)
91
+ end
92
+
93
+ def identifier_for(input_identifier)
94
+ registration = registrations[input_identifier]
95
+ raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
96
+
97
+ registration.display_identifier
98
+ end
99
+
100
+ def filter(views:)
101
+ result = SerializationRegistration.new inout
102
+
103
+ registrations.each do |identifier, registration|
104
+ if views.include? registration.validator.view
105
+ result.registrations[identifier] = registration
106
+ end
107
+ end
108
+
109
+ result
110
+ end
111
+
112
+ private
113
+
114
+ def register_wildcards(identifier, registration)
115
+ new_alias = SerializationAliasRegistration.new registration.serializer, registration.inout, registration.validator, identifier, registration, true, true
116
+
117
+ registrations['*/*'] = new_alias unless has? '*/*'
118
+
119
+ partial = "#{identifier.split('/')[0]}/*"
120
+ registrations[partial] = new_alias unless has? partial
121
+ end
122
+ end
123
+
124
+ # A registration in a SerializationRegistration collection
125
+ class SerializationBaseRegistration
126
+ def initialize(serializer, inout, validator, display_identifier)
127
+ self.serializer = serializer
128
+ self.inout = inout
129
+ self.validator = validator
130
+ self.display_identifier = display_identifier
131
+ end
132
+
133
+ def merge(_other)
134
+ nil
135
+ end
136
+
137
+ def decode(_victim, _context)
138
+ raise 'Assertion failed, decode function called on base registration.'
139
+ end
140
+ def call(_victim, _context, dsl: nil, raw: nil)
141
+ raise 'Assertion failed, call function called on base registration.'
142
+ end
143
+
144
+ attr_accessor :serializer, :inout, :validator, :display_identifier
145
+ end
146
+
147
+ # A registration with a block to be executed when called.
148
+ class SerializationBlockRegistration < SerializationBaseRegistration
149
+ def initialize(serializer, inout, validator, display_identifier, version, block, raw)
150
+ self.version = version
151
+ self.block = block
152
+ self.raw = raw
153
+ super(serializer, inout, validator, display_identifier)
154
+ end
155
+
156
+ def merge(other)
157
+ return nil unless other.is_a?(SerializationAliasRegistration)
158
+
159
+ return self if other.optional
160
+
161
+ nil
162
+ end
163
+
164
+ def decode(victim, _context)
165
+ raise CannotDecodeOutputError if inout != :input
166
+
167
+ unless raw
168
+ if defined? Oj::ParseError
169
+ begin
170
+ victim = MediaTypes::Serialization.json_decoder.call(victim)
171
+ validator.validate!(victim)
172
+ rescue MediaTypes::Scheme::ValidationError, Oj::ParseError, JSON::ParserError, EncodingError => inner
173
+ raise InputValidationFailedError, inner
174
+ end
175
+ else
176
+ begin
177
+ victim = MediaTypes::Serialization.json_decoder.call(victim)
178
+ validator.validate!(victim)
179
+ rescue MediaTypes::Scheme::ValidationError, JSON::ParserError, EncodingError => inner
180
+ raise InputValidationFailedError, inner
181
+ end
182
+ end
183
+ end
184
+
185
+ victim
186
+ end
187
+
188
+ def call(victim, context, dsl: nil, raw: nil)
189
+ raw = self.raw if raw.nil?
190
+
191
+ result = nil
192
+ if dsl.nil?
193
+ result = victim
194
+ result = block.call(victim, version, context) if block
195
+ else
196
+ result = dsl.instance_exec victim, version, context, &block
197
+ end
198
+
199
+ if !raw && inout == :output
200
+ begin
201
+ validator.validate!(result)
202
+ rescue MediaTypes::Scheme::ValidationError => inner
203
+ raise OutputValidationFailedError, inner
204
+ end
205
+ result = MediaTypes::Serialization.json_encoder.call(result)
206
+ end
207
+
208
+ result
209
+ end
210
+
211
+ attr_accessor :version, :block, :raw
212
+ end
213
+
214
+ # A registration that calls another registration when called.
215
+ class SerializationAliasRegistration < SerializationBaseRegistration
216
+ def initialize(serializer, inout, validator, display_identifier, target, optional, hide_variant)
217
+ self.target = target
218
+ self.optional = optional
219
+ self.hide_variant = hide_variant
220
+ super(serializer, inout, validator, display_identifier)
221
+ end
222
+
223
+ def merge(other)
224
+ if optional
225
+ return other unless other.is_a?(SerializationAliasRegistration)
226
+ else
227
+ return nil if other.is_a?(SerializationAliasRegistration) && !other.optional # two non-optional can't merge
228
+ return self
229
+ end
230
+
231
+ other # if both optional, or other is !optional, newer one wins.
232
+ end
233
+
234
+ def decode(victim, context)
235
+ target.decode(victim, context)
236
+ end
237
+
238
+ def call(victim, context, dsl: nil, raw: nil)
239
+ target.call(victim, context, dsl: dsl, raw: raw)
240
+ end
241
+
242
+ attr_accessor :target, :optional, :hide_variant
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/serialization/base'
4
+ require 'erb'
5
+ require 'cgi'
6
+
7
+ module MediaTypes
8
+ module Serialization
9
+ module Serializers
10
+ class ApiViewer < MediaTypes::Serialization::Base
11
+ unvalidated 'text/html'
12
+
13
+ def self.viewerify(uri, current_host, type: 'last')
14
+ viewer = URI.parse(uri)
15
+
16
+ return uri unless viewer.host == current_host
17
+
18
+ query_parts = viewer.query&.split('&') || []
19
+ query_parts = query_parts.select { |p| !p.starts_with? 'api_viewer=' }
20
+ query_parts.append("api_viewer=#{type}")
21
+ viewer.query = query_parts.join('&')
22
+ viewer.to_s
23
+ end
24
+
25
+ output_raw do |obj, version, context|
26
+ original_identifier = obj[:identifier]
27
+ registrations = obj[:registrations]
28
+ original_output = obj[:output]
29
+ original_links = obj[:links]
30
+
31
+ api_fied_links = original_links.map do |l|
32
+ new = l.dup
33
+ new[:invalid] = false
34
+ begin
35
+ uri = viewerify(new[:href], context.request.host)
36
+ new[:href] = uri.to_s
37
+ rescue URI::InvalidURIError
38
+ new[:invalid] = true
39
+ end
40
+
41
+ new
42
+ end
43
+
44
+ media_types = registrations.registrations.keys.map do |identifier|
45
+ result = {
46
+ identifier: identifier,
47
+ href: viewerify(context.request.original_url, context.request.host, type: identifier),
48
+ selected: identifier == original_identifier,
49
+ }
50
+ result[:href] = '#output' if identifier == original_identifier
51
+
52
+ result
53
+ end
54
+
55
+
56
+ escaped_output = original_output&.split("\n").
57
+ map { |l| CGI::escapeHTML(l).gsub(/ (?= )/, '&nbsp;') }.
58
+ map { |l| (l.gsub(/\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;{}]*[-A-Z0-9+@#\/%=}~_|](?![a-z]*;)/i) do |m|
59
+ converted = m
60
+ invalid = false
61
+ begin
62
+ converted = viewerify(m, context.request.host)
63
+ rescue URI::InvalidURIError
64
+ invalid = true
65
+ end
66
+ style = ''
67
+ style = ' style="color: red"' if invalid
68
+ "<a#{style} href=\"#{converted}\">#{m}</a>"
69
+ end) }.
70
+ join("<br>\n")
71
+
72
+
73
+ input = OpenStruct.new(
74
+ original_identifier: original_identifier,
75
+ escaped_output: escaped_output,
76
+ api_fied_links: api_fied_links,
77
+ media_types: media_types,
78
+ css: CommonCSS.css,
79
+ )
80
+
81
+ template = ERB.new <<-TEMPLATE
82
+ <html lang="en">
83
+ <head>
84
+ <title>API Viewer [<%= CGI::escapeHTML(original_identifier) %>]</title>
85
+ <style>
86
+ <%= css.split("\n").join("\n ") %>
87
+ </style>
88
+ </head>
89
+ <body>
90
+ <header>
91
+ <div id="logo"></div>
92
+ <h1>Api Viewer - <%= CGI::escapeHTML(original_identifier) %></h1>
93
+ </header>
94
+ <section id="content">
95
+ <nav>
96
+ <section id="representations">
97
+ <h2>Representations:</h2>
98
+ <ul>
99
+ <% media_types.each do |m| %>
100
+ <li>
101
+ <a href="<%= m[:href] %>" <%= m[:selected] ? 'class="active" ' : '' %>>
102
+ <%= CGI::escapeHTML(m[:identifier]) %>
103
+ </a>
104
+ </li>
105
+ <% end %>
106
+ </ul>
107
+ <hr>
108
+ </section>
109
+ <section id="links">
110
+ <span class="label">Links:&nbsp</span>
111
+ <ul>
112
+ <% api_fied_links.each do |l| %>
113
+ <li><a <% if l[:invalid] %> style="color: red" <% end %>href="<%= l[:href] %>"><%= CGI::escapeHTML(l[:rel].to_s) %></a></li>
114
+ <% end %>
115
+ </ul>
116
+ </section>
117
+ </nav>
118
+ <main>
119
+ <code id="output">
120
+ <%= escaped_output %>
121
+ </code>
122
+ </main>
123
+ </section>
124
+ <!-- API viewer made with ❤ by: https://delftsolutions.com -->
125
+ </body>
126
+ </html>
127
+ TEMPLATE
128
+ template.result(input.instance_eval { binding })
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end