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
@@ -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,235 @@
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, wildcards: true)
35
+ raise DuplicateDefinitionError.new(identifier, inout) if registrations.key? alias_identifier
36
+
37
+ raise UnbackedAliasDefinitionError.new(target_identifier, inout) unless registrations.key? target_identifier
38
+
39
+ target = registrations[target_identifier]
40
+
41
+ registration = SerializationAliasRegistration.new serializer, inout, target.validator, alias_identifier, target, optional
42
+ registrations[alias_identifier] = registration
43
+
44
+ register_wildcards(alias_identifier, registration) if wildcards && inout == :output
45
+ end
46
+
47
+ def merge(other)
48
+ raise Error, 'Trying to merge two SerializationRegistration objects with a different direction.' unless inout == other.inout
49
+
50
+ result = SerializationRegistration.new(inout)
51
+
52
+ prev_keys = Set.new(registrations.keys)
53
+ new_keys = Set.new(other.registrations.keys)
54
+ overlap = prev_keys & new_keys
55
+
56
+ result.registrations = registrations.merge(other.registrations)
57
+ overlap.each do |identifier|
58
+ prev_item = registrations[identifier]
59
+ new_item = other.registrations[identifier]
60
+ merge_result = prev_item.merge(new_item)
61
+
62
+ raise DuplicateUsageError.new(identifier, inout, prev_item.serializer, new_item.serializer) if merge_result.nil?
63
+
64
+ result.registrations[identifier] = merge_result
65
+ end
66
+
67
+ result
68
+ end
69
+
70
+ def decode(victim, media_type, context)
71
+ registration = registrations[media_type]
72
+ raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
73
+
74
+ registration.decode(victim, context)
75
+ end
76
+
77
+ def call(victim, media_type, context, dsl: nil, raw: nil)
78
+ registration = registrations[media_type]
79
+ raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
80
+
81
+ registration.call(victim, context, dsl: dsl, raw: raw)
82
+ end
83
+
84
+ def identifier_for(input_identifier)
85
+ registration = registrations[input_identifier]
86
+ raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
87
+
88
+ registration.display_identifier
89
+ end
90
+
91
+ def filter(views:)
92
+ result = SerializationRegistration.new inout
93
+
94
+ registrations.each do |identifier, registration|
95
+ if views.include? registration.validator.view
96
+ result.registrations[identifier] = registration
97
+ end
98
+ end
99
+
100
+ result
101
+ end
102
+
103
+ private
104
+
105
+ def register_wildcards(identifier, registration)
106
+ new_alias = SerializationAliasRegistration.new registration.serializer, registration.inout, registration.validator, identifier, registration, true
107
+
108
+ registrations['*/*'] = new_alias unless has? '*/*'
109
+
110
+ partial = "#{identifier.split('/')[0]}/*"
111
+ registrations[partial] = new_alias unless has? partial
112
+ end
113
+ end
114
+
115
+ # A registration in a SerializationRegistration collection
116
+ class SerializationBaseRegistration
117
+ def initialize(serializer, inout, validator, display_identifier)
118
+ self.serializer = serializer
119
+ self.inout = inout
120
+ self.validator = validator
121
+ self.display_identifier = display_identifier
122
+ end
123
+
124
+ def merge(_other)
125
+ nil
126
+ end
127
+
128
+ def decode(_victim, _context)
129
+ raise 'Assertion failed, decode function called on base registration.'
130
+ end
131
+ def call(_victim, _context, dsl: nil, raw: nil)
132
+ raise 'Assertion failed, call function called on base registration.'
133
+ end
134
+
135
+ attr_accessor :serializer, :inout, :validator, :display_identifier
136
+ end
137
+
138
+ # A registration with a block to be executed when called.
139
+ class SerializationBlockRegistration < SerializationBaseRegistration
140
+ def initialize(serializer, inout, validator, display_identifier, version, block, raw)
141
+ self.version = version
142
+ self.block = block
143
+ self.raw = raw
144
+ super(serializer, inout, validator, display_identifier)
145
+ end
146
+
147
+ def merge(other)
148
+ return nil unless other.is_a?(SerializationAliasRegistration)
149
+
150
+ return self if other.optional
151
+
152
+ nil
153
+ end
154
+
155
+ def decode(victim, _context)
156
+ raise CannotDecodeOutputError if inout != :input
157
+
158
+ unless raw
159
+ if defined? Oj::ParseError
160
+ begin
161
+ victim = MediaTypes::Serialization.json_decoder.call(victim)
162
+ validator.validate!(victim)
163
+ rescue MediaTypes::Scheme::ValidationError, Oj::ParseError, JSON::ParserError => inner
164
+ raise InputValidationFailedError, inner
165
+ end
166
+ else
167
+ begin
168
+ victim = MediaTypes::Serialization.json_decoder.call(victim)
169
+ validator.validate!(victim)
170
+ rescue MediaTypes::Scheme::ValidationError, JSON::ParserError => inner
171
+ raise InputValidationFailedError, inner
172
+ end
173
+ end
174
+ end
175
+
176
+ victim
177
+ end
178
+
179
+ def call(victim, context, dsl: nil, raw: nil)
180
+ raw = self.raw if raw.nil?
181
+
182
+ result = nil
183
+ if dsl.nil?
184
+ result = victim
185
+ result = block.call(victim, version, context) if block
186
+ else
187
+ result = dsl.instance_exec victim, version, context, &block
188
+ end
189
+
190
+ if !raw && inout == :output
191
+ begin
192
+ validator.validate!(result)
193
+ rescue MediaTypes::Scheme::ValidationError => inner
194
+ raise OutputValidationFailedError, inner
195
+ end
196
+ result = MediaTypes::Serialization.json_encoder.call(result)
197
+ end
198
+
199
+ result
200
+ end
201
+
202
+ attr_accessor :version, :block, :raw
203
+ end
204
+
205
+ # A registration that calls another registration when called.
206
+ class SerializationAliasRegistration < SerializationBaseRegistration
207
+ def initialize(serializer, inout, validator, display_identifier, target, optional)
208
+ self.target = target
209
+ self.optional = optional
210
+ super(serializer, inout, validator, display_identifier)
211
+ end
212
+
213
+ def merge(other)
214
+ if optional
215
+ return other unless other.is_a?(SerializationAliasRegistration)
216
+ else
217
+ return nil if other.is_a?(SerializationAliasRegistration) && !other.optional # two non-optional can't merge
218
+ return self
219
+ end
220
+
221
+ other # if both optional, or other is !optional, newer one wins.
222
+ end
223
+
224
+ def decode(victim, context)
225
+ target.decode(victim, context)
226
+ end
227
+
228
+ def call(victim, context, dsl: nil, raw: nil)
229
+ target.call(victim, context, dsl: dsl, raw: raw)
230
+ end
231
+
232
+ attr_accessor :target, :optional
233
+ end
234
+ end
235
+ 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