media_types-serialization 0.1.0

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