media_types-serialization 0.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.idea/.rakeTasks +7 -0
  4. data/.idea/dictionaries/Derk_Jan.xml +7 -0
  5. data/.idea/inspectionProfiles/Project_Default.xml +6 -0
  6. data/.idea/media_types-serialization.iml +55 -0
  7. data/.idea/misc.xml +7 -0
  8. data/.idea/modules.xml +8 -0
  9. data/.idea/runConfigurations/test.xml +20 -0
  10. data/.idea/vcs.xml +6 -0
  11. data/.travis.yml +17 -0
  12. data/CHANGELOG.md +5 -0
  13. data/CODE_OF_CONDUCT.md +74 -0
  14. data/Gemfile +4 -0
  15. data/Gemfile.lock +76 -0
  16. data/LICENSE.txt +21 -0
  17. data/README.md +209 -0
  18. data/Rakefile +10 -0
  19. data/bin/console +14 -0
  20. data/bin/setup +8 -0
  21. data/lib/media_types/serialization.rb +194 -0
  22. data/lib/media_types/serialization/base.rb +138 -0
  23. data/lib/media_types/serialization/error.rb +7 -0
  24. data/lib/media_types/serialization/migrations_command.rb +38 -0
  25. data/lib/media_types/serialization/migrations_support.rb +41 -0
  26. data/lib/media_types/serialization/mime_type_support.rb +55 -0
  27. data/lib/media_types/serialization/no_content_type_given.rb +11 -0
  28. data/lib/media_types/serialization/no_media_type_serializers.rb +11 -0
  29. data/lib/media_types/serialization/no_serializer_for_content_type.rb +15 -0
  30. data/lib/media_types/serialization/renderer.rb +31 -0
  31. data/lib/media_types/serialization/renderer/register.rb +4 -0
  32. data/lib/media_types/serialization/version.rb +5 -0
  33. data/lib/media_types/serialization/wrapper.rb +15 -0
  34. data/lib/media_types/serialization/wrapper/html_wrapper.rb +42 -0
  35. data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +48 -0
  36. data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +48 -0
  37. data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +45 -0
  38. data/lib/media_types/serialization/wrapper/media_wrapper.rb +32 -0
  39. data/lib/media_types/serialization/wrapper/root_key.rb +20 -0
  40. data/media_types-serialization.gemspec +48 -0
  41. metadata +212 -0
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "media_types/serialization"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,194 @@
1
+ require 'media_types/serialization/version'
2
+
3
+ require 'abstract_controller'
4
+ require 'action_controller/metal/mime_responds'
5
+ require 'action_dispatch/http/mime_type'
6
+ require 'active_support/concern'
7
+ require 'active_support/core_ext/module/attribute_accessors'
8
+
9
+ require 'http_headers/accept'
10
+
11
+ require 'media_types/serialization/no_media_type_serializers'
12
+ require 'media_types/serialization/no_serializer_for_content_type'
13
+ require 'media_types/serialization/wrapper/html_wrapper'
14
+ require 'media_types/serialization/wrapper/media_wrapper'
15
+
16
+ module MediaTypes
17
+ module Serialization
18
+
19
+ mattr_accessor :common_suffix
20
+
21
+ extend ActiveSupport::Concern
22
+
23
+ HEADER_ACCEPT = 'HTTP_ACCEPT'
24
+ MEDIA_TYPE_HTML = 'text/html'
25
+
26
+ # rubocop:disable Metrics/BlockLength
27
+ class_methods do
28
+ # @see #freeze_accepted_media!
29
+ #
30
+ def accept_serialization(serializer, view: [nil], accept_html: true, **filter_opts)
31
+ before_action(**filter_opts) do
32
+ self.serializers = resolved_media_types(serializer, view: view) do |media_type, media_view, res|
33
+ opts = { media_type: media_type, media_view: media_view }
34
+
35
+ res[MEDIA_TYPE_HTML] = wrap_html(serializer, **opts) if accept_html && !res[MEDIA_TYPE_HTML]
36
+ res[String(media_type)] = wrap_media(serializer, **opts) if media_type != MEDIA_TYPE_HTML
37
+ end
38
+ end
39
+ end
40
+
41
+ def accept_html(serializer, **filter_opts)
42
+ before_action(**filter_opts) do
43
+ self.serializers = resolved_media_types(serializer, view: nil) do |_, media_view, res|
44
+ res[MEDIA_TYPE_HTML] = wrap_html(serializer, media_view: media_view, media_type: MEDIA_TYPE_HTML)
45
+ break
46
+ end
47
+ end
48
+ end
49
+
50
+ ##
51
+ # Register a mime type, but explicitly notify that it can't be serialized.
52
+ # This is done for file serving and redirects.
53
+ #
54
+ # @param [Symbol] mimes takes a list of symbols that should resolve through Mime::Type
55
+ #
56
+ # @see #freeze_accepted_media!
57
+ #
58
+ # @example fingerpint binary format
59
+ #
60
+ # no_serializer_for :fingerprint_bin, :fingerprint_deprecated_bin
61
+ #
62
+ def accept_without_serialization(*mimes, **filter_opts)
63
+ before_action(**filter_opts) do
64
+ self.serializers = Array(mimes).each_with_object(Hash(serializers)) do |mime, res|
65
+ res[(Mime::Type.lookup_by_extension(mime) || mime).to_s] = nil
66
+ end
67
+ end
68
+ end
69
+
70
+ ##
71
+ # Freezes additions to the serializes and notifies the controller what it will be able to respond to.
72
+ #
73
+ def freeze_accepted_media!
74
+ before_action do
75
+ # If the responders gem is available, this freezes what a controller can respond to
76
+ if self.class.respond_to?(:respond_to)
77
+ self.class.respond_to(*Hash(serializers).keys.map { |type| Mime::Type.lookup(type) })
78
+ end
79
+ serializers.freeze
80
+ end
81
+ end
82
+ end
83
+ # rubocop:enable Metrics/BlockLength
84
+
85
+ included do
86
+ protected
87
+
88
+ attr_accessor :serializers
89
+ end
90
+
91
+ protected
92
+
93
+ def media_type_serializer
94
+ @media_type_serializer ||= resolve_media_type_serializer
95
+ end
96
+
97
+ def serialize_media(media, serializer: media_type_serializer)
98
+ @last_serialize_media = media
99
+ @last_media_serializer = serializer.call(media, context: self)
100
+ end
101
+
102
+ def media_type_json_root
103
+ String(request.format.symbol).sub(/_json$/, '')
104
+ end
105
+
106
+ def respond_to_matching(matcher, &block)
107
+ respond_to do |format|
108
+ serializers.each_key do |mime|
109
+ next unless matcher.call(mime: mime, format: format)
110
+ format.custom(mime, &block)
111
+ end
112
+ end
113
+ end
114
+
115
+ def respond_to_accept(&block)
116
+ respond_to do |format|
117
+ serializers.each_key do |mime|
118
+ format.custom(mime, &block)
119
+ end
120
+
121
+ format.any { raise_no_accept_serializer }
122
+ end
123
+ end
124
+
125
+ def request_accept
126
+ @request_accept ||= HttpHeaders::Accept.new(request.get_header(HEADER_ACCEPT) || '')
127
+ end
128
+
129
+ def raise_no_accept_serializer
130
+ raise NoSerializerForContentType.new(request_accept, serializers.keys)
131
+ end
132
+
133
+ private
134
+
135
+ def extract_synonym_version(synonym)
136
+ synonym.rpartition('.').last[1..-1]
137
+ end
138
+
139
+ def resolve_media_type_serializer
140
+ raise NoMediaTypeSerializers unless serializers
141
+
142
+ # Rails negotiation
143
+ if serializers[request.format.to_s]
144
+ return serializers[request.format.to_s]
145
+ end
146
+
147
+ # Ruby negotiation
148
+ request.accepts.each do |mime_type|
149
+ next unless serializers.key?(mime_type.to_s)
150
+ # Override Rails selected format
151
+ request.set_header("action_dispatch.request.formats", [mime_type])
152
+ return serializers[mime_type.to_s]
153
+ end
154
+
155
+ raise_no_accept_serializer
156
+ end
157
+
158
+ def resolved_media_types(serializer, view:)
159
+ Array(view).each_with_object(Hash(serializers)) do |media_view, res|
160
+ media_view = String(media_view)
161
+ Array(serializer.media_type(view: media_view)).each do |media_type|
162
+ yield media_type, media_view, res
163
+ end
164
+ end
165
+ end
166
+
167
+ def wrap_media(serializer, media_view:, media_type:)
168
+ lambda do |*args, **opts|
169
+ Wrapper::MediaWrapper.new(
170
+ serializer.new(*args, media_type: media_type, view: media_view, **opts),
171
+ view: media_view
172
+ )
173
+ end
174
+ end
175
+
176
+ def wrap_html(serializer, media_view:, media_type:)
177
+ lambda do |*args, **opts|
178
+ media_serializer = wrap_media(
179
+ serializer,
180
+ media_view: media_view,
181
+ media_type: media_type
182
+ ).call(*args, **opts)
183
+
184
+ Wrapper::HtmlWrapper.new(
185
+ media_serializer,
186
+ view: media_view,
187
+ mime_type: media_type.to_s,
188
+ representations: serializers.keys,
189
+ url_context: request.original_fullpath.chomp(".#{request.format.symbol}")
190
+ )
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ require 'media_types/serialization/mime_type_support'
6
+ require 'media_types/serialization/migrations_support'
7
+
8
+ module MediaTypes
9
+ module Serialization
10
+ class Base
11
+ include MimeTypeSupport
12
+ include MigrationsSupport
13
+
14
+ attr_accessor :serializable
15
+
16
+ def initialize(serializable, media_type:, view: nil, context:)
17
+ self.context = context
18
+ self.current_media_type = media_type
19
+ self.current_view = view
20
+
21
+ set(serializable)
22
+ end
23
+
24
+ def to_link_header
25
+ {}
26
+ end
27
+
28
+ def to_html
29
+ raise NotImplementedError, format(
30
+ 'In %<class>s, to_html is not implemented.',
31
+ class: self.class.name
32
+ )
33
+ end
34
+
35
+ def to_xml
36
+ raise NotImplementedError, format(
37
+ 'In %<class>s, to_xml is not implemented.',
38
+ class: self.class.name
39
+ )
40
+ end
41
+
42
+ def to_hash
43
+ raise NotImplementedError, format(
44
+ 'In %<class>s, to_hash is not implemented.',
45
+ class: self.class.name
46
+ )
47
+ end
48
+
49
+ def to_text
50
+ raise NotImplementedError, format(
51
+ 'In %<class>s, to_text is not implemented/',
52
+ class: self.class.name
53
+ )
54
+ end
55
+
56
+ def to_json
57
+ raise NotImplementedError, format(
58
+ 'In %<class>s, to_json is not implemented/',
59
+ class: self.class.name
60
+ )
61
+ end
62
+
63
+ def to_h
64
+ raise NotImplementedError, format(
65
+ 'In %<class>s, to_h is not implemented. Missing alias to_h to_hash.',
66
+ class: self.class.name
67
+ )
68
+ end
69
+
70
+ def to_body
71
+ raise NotImplementedError, format(
72
+ 'In %<class>s, to_body is not implemented. This is a general purpose catch all renderer',
73
+ class: self.class.name
74
+ )
75
+ end
76
+
77
+ def respond_to?(sym, include_all = false)
78
+ return false if [:to_h, :to_hash, :to_json, :to_text, :to_xml, :to_html, :to_body, :extract_self].include?(sym)
79
+ return true if sym == :to_link_header
80
+
81
+ super
82
+ end
83
+
84
+ protected
85
+
86
+ attr_accessor :context, :current_media_type, :current_view
87
+
88
+ def extract_self
89
+ raise NotImplementedError, format(
90
+ 'In %<class>s, extract_self is not implemented, thus a self link for %<model>s can not be generated. ' \
91
+ 'Implement extract_self on %<class>s or deny the MediaType[s] %<media_types>s for this request.',
92
+ class: self.class.name,
93
+ model: serializable.class.name,
94
+ media_types: self.class.media_types(view: '[view]').to_s
95
+ )
96
+ end
97
+
98
+ def extract_links
99
+ {}
100
+ end
101
+
102
+ def header_links
103
+ extract_links
104
+ end
105
+
106
+ def set(serializable)
107
+ self.serializable = serializable
108
+ self
109
+ end
110
+
111
+ def extract(extractable, *keys)
112
+ return {} unless keys.present?
113
+ extractable.slice(*keys)
114
+ rescue TypeError => err
115
+ raise TypeError, format(
116
+ '[serializer] failed to slice keys to extract. Given keys: %<keys>s. Extractable: %<extractable>s' \
117
+ 'Error: %<error>s',
118
+ keys: keys,
119
+ extractable: extractable,
120
+ error: err
121
+ )
122
+ end
123
+
124
+ def resolve_file_url(url)
125
+ return url if !url || URI(url).absolute?
126
+
127
+ format(
128
+ 'https://%<host>s:%<port>s%<path>s',
129
+ host: context.default_url_options[:host],
130
+ port: context.default_url_options[:port],
131
+ path: url
132
+ )
133
+ rescue URI::InvalidURIError
134
+ url
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,7 @@
1
+
2
+ module MediaTypes
3
+ module Serialization
4
+ class Error < StandardError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ module MediaTypes
4
+ module Serialization
5
+ class MigrationsCommand
6
+ def initialize(serializer)
7
+ self.serializer = serializer
8
+ self.migrations = {}
9
+ end
10
+
11
+ def call(result, mime_type, view)
12
+ return result if matches_current_mime_type?(view: view, mime_type: mime_type)
13
+
14
+ migrations.reduce(result) do |migrated, (version, migration)|
15
+ migrated = migration.call(migrated)
16
+ next migrated unless matches_mime_type?(mime_type.version(version), mime_type)
17
+ break migrated
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_accessor :serializer, :migrations
24
+
25
+ def version(version, &block)
26
+ migrations[version] = ->(result) { serializer.instance_exec(result, &block) }
27
+ end
28
+
29
+ def matches_current_mime_type?(view:, mime_type:)
30
+ serializer.class.current_mime_type(view: view) == mime_type.to_s
31
+ end
32
+
33
+ def matches_mime_type?(left, right)
34
+ left.to_s == right.to_s
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'media_types/serialization/migrations_command'
5
+
6
+ module MediaTypes
7
+ module Serialization
8
+ module MigrationsSupport
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ mattr_accessor :migrations
13
+ end
14
+
15
+ class_methods do
16
+ def migrator(serializer)
17
+ return nil unless migrations
18
+ migrations.call(serializer)
19
+ end
20
+
21
+ protected
22
+
23
+ def backward_migrations(&block)
24
+ self.migrations = lambda do |serializer|
25
+ MigrationsCommand.new(serializer).tap do |callable|
26
+ callable.instance_exec(&block)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def migrate(result = nil, media_type = current_media_type, view = current_view)
33
+ result ||= yield
34
+
35
+ migrator = self.class.migrator(self)
36
+ return result unless migrator
37
+ migrator.call(result, media_type, view)
38
+ end
39
+ end
40
+ end
41
+ end