praxis 0.14.0 → 0.15.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -1
- data/Gemfile +5 -0
- data/bin/praxis +7 -4
- data/lib/api_browser/package.json +1 -1
- data/lib/praxis/action_definition.rb +10 -5
- data/lib/praxis/application.rb +30 -0
- data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -2
- data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +7 -2
- data/lib/praxis/file_group.rb +1 -1
- data/lib/praxis/handlers/json.rb +32 -0
- data/lib/praxis/handlers/www_form.rb +20 -0
- data/lib/praxis/handlers/xml.rb +78 -0
- data/lib/praxis/links.rb +4 -1
- data/lib/praxis/media_type.rb +55 -0
- data/lib/praxis/media_type_identifier.rb +198 -0
- data/lib/praxis/multipart/part.rb +16 -0
- data/lib/praxis/request.rb +34 -25
- data/lib/praxis/response.rb +22 -3
- data/lib/praxis/response_definition.rb +11 -36
- data/lib/praxis/restful_doc_generator.rb +1 -1
- data/lib/praxis/simple_media_type.rb +6 -1
- data/lib/praxis/types/media_type_common.rb +8 -3
- data/lib/praxis/types/multipart.rb +6 -6
- data/lib/praxis/version.rb +1 -1
- data/lib/praxis.rb +7 -1
- data/praxis.gemspec +1 -1
- data/spec/functional_spec.rb +3 -1
- data/spec/praxis/application_spec.rb +58 -1
- data/spec/praxis/handlers/json_spec.rb +37 -0
- data/spec/praxis/handlers/xml_spec.rb +155 -0
- data/spec/praxis/media_type_identifier_spec.rb +209 -0
- data/spec/praxis/media_type_spec.rb +50 -3
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +2 -2
- data/spec/praxis/request_spec.rb +39 -1
- data/spec/praxis/response_definition_spec.rb +12 -9
- data/spec/praxis/response_spec.rb +37 -6
- data/spec/praxis/types/collection_spec.rb +2 -2
- data/spec/praxis/types/multipart_spec.rb +17 -0
- data/spec/spec_app/app/controllers/instances.rb +1 -1
- data/spec/support/spec_media_types.rb +19 -0
- metadata +11 -6
- data/lib/praxis/content_type_parser.rb +0 -62
- data/spec/praxis/content_type_parser_spec.rb +0 -91
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0dbe3630ec0519b3fe1c0c4581155e1fd9858002
|
4
|
+
data.tar.gz: 1a93b0745b2413203e13a69a1a7c83e41f4c7ab8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f0e8f6ff18f14cbfd2e0d18fbbcb7a05ef32c01914106dd29952c63b8d2dd70fbab8c254961ca48f9ae2597206f0548acd057b820adbc5656d74d98ca7073e7
|
7
|
+
data.tar.gz: 77d8c97b50f6f3853002289d59f8e17585269d5dad0d3b1d8089e00694dabd06fddfa67ed0179c918587ef6b1e624a4736b47a4b2b05478ca63ef6cde2135dda
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,21 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
-
## 0.
|
5
|
+
## 0.15.0
|
6
|
+
|
7
|
+
* Fixed handling of no app or design file groups defined in application layout.
|
8
|
+
* Handled and added warning message for doc generation task when no routing block is defined for an action.
|
9
|
+
* Improved `link` method in `MediaType` attribute definition to support inheriting the type from the `:using` option if if that specifies an attribute. For example: `link :posts, using: :posts_summary` would use the type of the `:posts_summary` attribute.
|
10
|
+
* Fixed generated `Links` accessors to properly load the returned value.
|
11
|
+
* Added `MediaTypeIdentifier` class to parse and manipulate Content-Type headers and Praxis::MediaType identifiers.
|
12
|
+
* Created a registry for media type handlers that parse and generate document bodies with formats other than JSON.
|
13
|
+
* Given a structured-data response, Praxis will convert it to JSON, XML or other formats based on the handler indicated by its Content-Type.
|
14
|
+
* Given a request, Praxis will use the handler indicated by its Content-Type header to parse the body into structured data.
|
15
|
+
* Fixed bug allowing "praxis new" to work when Praxis is installed as a system (non-bundled) gem.
|
16
|
+
* Fixed doc generation code for custom types
|
17
|
+
* Hardened Multipart type loading
|
18
|
+
|
19
|
+
## 0.14.0
|
6
20
|
|
7
21
|
* Adds features for customizing and exporting the Doc browser, namely the following changes:
|
8
22
|
1. All doc browser stuff is now centralised under the `praxis:docs` namespace.
|
data/Gemfile
CHANGED
data/bin/praxis
CHANGED
@@ -2,9 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'bundler'
|
4
4
|
|
5
|
-
|
6
|
-
Bundler.
|
7
|
-
|
5
|
+
begin
|
6
|
+
Bundler.setup :default, :test
|
7
|
+
Bundler.require :default, :test
|
8
|
+
rescue Bundler::GemfileNotFound
|
9
|
+
# no-op: we might be installed as a system gem
|
10
|
+
end
|
8
11
|
|
9
12
|
if ["routes","docs","console"].include? ARGV[0]
|
10
13
|
require 'rake'
|
@@ -83,4 +86,4 @@ class PraxisGenerator < Thor
|
|
83
86
|
|
84
87
|
end
|
85
88
|
|
86
|
-
PraxisGenerator.start(ARGV)
|
89
|
+
PraxisGenerator.start(ARGV)
|
@@ -173,10 +173,15 @@ module Praxis
|
|
173
173
|
end
|
174
174
|
|
175
175
|
def params_description
|
176
|
-
route_params =
|
177
|
-
|
178
|
-
|
179
|
-
|
176
|
+
route_params = []
|
177
|
+
if primary_route.nil?
|
178
|
+
warn "Warning: No routes defined for #{resource_definition.name}##{name}."
|
179
|
+
else
|
180
|
+
route_params = primary_route.path.
|
181
|
+
named_captures.
|
182
|
+
keys.
|
183
|
+
collect(&:to_sym)
|
184
|
+
end
|
180
185
|
|
181
186
|
desc = params.describe
|
182
187
|
desc[:type][:attributes].keys.each do |k|
|
@@ -185,7 +190,7 @@ module Praxis
|
|
185
190
|
else
|
186
191
|
'query'
|
187
192
|
end
|
188
|
-
desc[:type][:attributes][k][:source] = source
|
193
|
+
desc[:type][:attributes][k][:source] = source
|
189
194
|
end
|
190
195
|
desc
|
191
196
|
end
|
data/lib/praxis/application.rb
CHANGED
@@ -17,6 +17,7 @@ module Praxis
|
|
17
17
|
attr_accessor :loaded_files
|
18
18
|
attr_accessor :logger
|
19
19
|
attr_accessor :plugins
|
20
|
+
attr_accessor :handlers
|
20
21
|
attr_accessor :root
|
21
22
|
attr_accessor :error_handler
|
22
23
|
|
@@ -39,6 +40,7 @@ module Praxis
|
|
39
40
|
@bootloader = Bootloader.new(self)
|
40
41
|
@file_layout = nil
|
41
42
|
@plugins = Hash.new
|
43
|
+
@handlers = Hash.new
|
42
44
|
@loaded_files = Set.new
|
43
45
|
@config = Config.new
|
44
46
|
@root = nil
|
@@ -48,6 +50,15 @@ module Praxis
|
|
48
50
|
def setup(root: '.')
|
49
51
|
@root = Pathname.new(root).expand_path
|
50
52
|
|
53
|
+
builtin_handlers = {
|
54
|
+
'json' => Praxis::Handlers::JSON,
|
55
|
+
'x-www-form-urlencoded' => Praxis::Handlers::WWWForm
|
56
|
+
}
|
57
|
+
# Register built-in handlers unless the app already provided its own
|
58
|
+
builtin_handlers.each_pair do |name, handler|
|
59
|
+
self.handler(name, handler) unless handlers.key?(name)
|
60
|
+
end
|
61
|
+
|
51
62
|
@bootloader.setup!
|
52
63
|
|
53
64
|
@builder.run(@router)
|
@@ -68,6 +79,25 @@ module Praxis
|
|
68
79
|
@builder.use(middleware, *args, &block)
|
69
80
|
end
|
70
81
|
|
82
|
+
# Register a media type handler used to transform medias' structured data
|
83
|
+
# to HTTP response entitites with a specific encoding (JSON, XML, etc)
|
84
|
+
# and to parse request bodies into structured data.
|
85
|
+
#
|
86
|
+
# @param [String] name
|
87
|
+
# @param [Class] a class that responds to .new, #parse and #generate
|
88
|
+
def handler(name, handler)
|
89
|
+
# Construct an instance, if the handler is a class and needs to be initialized.
|
90
|
+
handler = handler.new
|
91
|
+
|
92
|
+
# Make sure it quacks like a handler.
|
93
|
+
unless handler.respond_to?(:generate) && handler.respond_to?(:parse)
|
94
|
+
raise ArgumentError, "Media type handlers must respond to #generate and #parse"
|
95
|
+
end
|
96
|
+
|
97
|
+
# Register that thing!
|
98
|
+
@handlers[name.to_s] = handler
|
99
|
+
end
|
100
|
+
|
71
101
|
def call(env)
|
72
102
|
response = []
|
73
103
|
Notifications.instrument 'rack.request.all'.freeze, response: response do
|
@@ -9,11 +9,11 @@ module Praxis
|
|
9
9
|
def initialize(name, application)
|
10
10
|
super
|
11
11
|
# always finalize Taylor after loading app code.
|
12
|
-
|
12
|
+
|
13
13
|
end
|
14
14
|
|
15
15
|
def order
|
16
|
-
@order ||= application.file_layout[name].groups.keys
|
16
|
+
@order ||= application.file_layout[name] == [] ? [] : application.file_layout[name].groups.keys
|
17
17
|
end
|
18
18
|
|
19
19
|
def setup!
|
@@ -16,6 +16,11 @@ module Praxis
|
|
16
16
|
|
17
17
|
def execute
|
18
18
|
return unless self.class.enabled
|
19
|
+
|
20
|
+
if application.file_layout[:app] == []
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
19
24
|
base = application.file_layout[:app].base
|
20
25
|
return unless base.exist?
|
21
26
|
file_enum = base.find.to_a
|
@@ -26,12 +31,12 @@ module Praxis
|
|
26
31
|
|
27
32
|
missing = Set.new(files) - application.loaded_files
|
28
33
|
if missing.any?
|
29
|
-
msg = "The following
|
34
|
+
msg = "The following application files under #{base} were not loaded:\n"
|
30
35
|
missing.each do |file|
|
31
36
|
path = file.relative_path_from(base)
|
32
37
|
msg << " * #{path}\n"
|
33
38
|
end
|
34
|
-
|
39
|
+
warn msg
|
35
40
|
end
|
36
41
|
end
|
37
42
|
|
data/lib/praxis/file_group.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Praxis
|
2
|
+
module Handlers
|
3
|
+
class JSON
|
4
|
+
# Construct a JSON handler and initialize any related libraries.
|
5
|
+
#
|
6
|
+
# @raise [Praxis::Exceptions::InvalidConfiguration] if the handler is unsupported
|
7
|
+
def initialize
|
8
|
+
require 'json'
|
9
|
+
rescue LoadError
|
10
|
+
# Should never happen since JSON is a default gem; might as well be cautious!
|
11
|
+
raise Praxis::Exceptions::InvalidConfiguration,
|
12
|
+
"JSON handler depends on json ~> 1.0; please add it to your Gemfile"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Parse a JSON document into structured data.
|
16
|
+
#
|
17
|
+
# @param [String] document
|
18
|
+
# @return [Hash,Array] the structured-data representation of the document
|
19
|
+
def parse(document)
|
20
|
+
::JSON.parse(document)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generate a pretty-printed JSON document from structured data.
|
24
|
+
#
|
25
|
+
# @param [Hash,Array] structured_data
|
26
|
+
# @return [String]
|
27
|
+
def generate(structured_data)
|
28
|
+
::JSON.pretty_generate(structured_data)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Praxis
|
2
|
+
module Handlers
|
3
|
+
class WWWForm
|
4
|
+
def initialize
|
5
|
+
require 'rack' # superfluous, but might as well be safe
|
6
|
+
end
|
7
|
+
|
8
|
+
# Parse a URL-encoded WWW form into structured data.
|
9
|
+
def parse(entity)
|
10
|
+
::Rack::Utils.parse_nested_query(entity)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Generate a URL-encoded WWW form from structured data. Not implemented since this format
|
14
|
+
# is not very useful for a response body.
|
15
|
+
def generate(structured_data)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Praxis
|
2
|
+
module Handlers
|
3
|
+
class XML
|
4
|
+
# Construct an XML handler and initialize any related libraries.
|
5
|
+
#
|
6
|
+
# @raise [Praxis::Exceptions::InvalidConfiguration] if the handler is unsupported
|
7
|
+
def initialize
|
8
|
+
require 'nokogiri'
|
9
|
+
require 'builder'
|
10
|
+
require 'active_support'
|
11
|
+
ActiveSupport::XmlMini.backend = 'Nokogiri'
|
12
|
+
rescue LoadError
|
13
|
+
raise Praxis::Exceptions::InvalidConfiguration,
|
14
|
+
"XML handler depends on builder ~> 3.2 and nokogiri ~> 1.6; please add them to your Gemfile"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Parse an XML document into structured data.
|
18
|
+
#
|
19
|
+
# @param [String] document
|
20
|
+
# @return [Hash,Array] the structured-data representation of the document
|
21
|
+
def parse(document)
|
22
|
+
p = Nokogiri::XML(document)
|
23
|
+
process(p.root, p.root.attributes['type'])
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generate a pretty-printed XML document from structured data.
|
27
|
+
#
|
28
|
+
# @param [Hash,Array] structured_data
|
29
|
+
# @return [String]
|
30
|
+
def generate(structured_data)
|
31
|
+
# courtesy of active_support + builder
|
32
|
+
structured_data.to_xml
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
# Transform a Nokogiri DOM object into structured data.
|
38
|
+
def process(node, type_attribute)
|
39
|
+
type = type_attribute.value if type_attribute
|
40
|
+
|
41
|
+
case type
|
42
|
+
when nil
|
43
|
+
if node.children.size == 1 && node.child.text?
|
44
|
+
# leaf text node
|
45
|
+
return node.content
|
46
|
+
else
|
47
|
+
# A hash
|
48
|
+
return node.children.each_with_object({}) do |child, hash|
|
49
|
+
next unless child.element? # There might be text fragments like newlines...spaces
|
50
|
+
hash[child.name] = process(child, child.attributes['type'])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
when "array"
|
54
|
+
return node.children.each_with_object([]) do |child, arr|
|
55
|
+
next unless child.element? # There might be text fragments like newlines...spaces
|
56
|
+
arr << process(child, child.attributes['type'])
|
57
|
+
end
|
58
|
+
when "integer"
|
59
|
+
return Integer(node.content)
|
60
|
+
when "symbol"
|
61
|
+
return node.content.to_sym
|
62
|
+
when "decimal"
|
63
|
+
return BigDecimal.new(node.content)
|
64
|
+
when "float"
|
65
|
+
return Float(node.content)
|
66
|
+
when "boolean"
|
67
|
+
return ((node.content == "false") ? false : true)
|
68
|
+
when "date"
|
69
|
+
return Date.parse(node.content)
|
70
|
+
when "dateTime"
|
71
|
+
return DateTime.parse(node.content)
|
72
|
+
else
|
73
|
+
raise ArgumentError, "Unknown attribute type: #{type}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/praxis/links.rb
CHANGED
@@ -13,6 +13,9 @@ module Praxis
|
|
13
13
|
|
14
14
|
def link(name, type=nil, using: name, **opts, &block)
|
15
15
|
links[name] = using
|
16
|
+
if type.nil? && (name != using)
|
17
|
+
type = options[:reference].attributes[using].type
|
18
|
+
end
|
16
19
|
attribute(name, type, **opts, &block)
|
17
20
|
end
|
18
21
|
end
|
@@ -69,7 +72,7 @@ module Praxis
|
|
69
72
|
define_method(name) do
|
70
73
|
value = @object.__send__(using)
|
71
74
|
return value if value.nil? || value.kind_of?(attribute.type)
|
72
|
-
attribute.
|
75
|
+
attribute.load(value)
|
73
76
|
end
|
74
77
|
|
75
78
|
# do whatever crazy aliasing we need to here....
|
data/lib/praxis/media_type.rb
CHANGED
@@ -1,4 +1,59 @@
|
|
1
1
|
module Praxis
|
2
|
+
# An Internet Media Type as defined in RFC 1590, as used in HTTP (see RFC 2616). As used in the
|
3
|
+
# Praxis framework, media types also define the structure and content of entities of that type:
|
4
|
+
# the attributes that exist, their names and types.
|
5
|
+
#
|
6
|
+
# An object with a media type can be represented on the wire using different structured-syntax
|
7
|
+
# encodings; for example, a controller might respond with an actual Widget object, but a
|
8
|
+
# Content-Type header specifying 'application/vnd.acme.widget+json'; Praxis uses the information
|
9
|
+
# contained in the media-type definition of Widget to transform the object into an equivalent
|
10
|
+
# JSON representation. If the content type ends with '+xml' instead, and the XML handler is
|
11
|
+
# registered with the framework, Praxis will respond with an XML representation of the
|
12
|
+
# widget. The use of media types allows your application's models to be decoupled from its
|
13
|
+
# HTTP interface specification.
|
14
|
+
#
|
15
|
+
# A media type definition consists of:
|
16
|
+
# - a MIME type identifier
|
17
|
+
# - attributes, each of which has a name and a data type
|
18
|
+
# - named links to other resources
|
19
|
+
# - named views, which expose interesting subsets of attributes
|
20
|
+
#
|
21
|
+
# @example Declare a widget type that's used by my supply-chain management app
|
22
|
+
# class MyApp::MediaTypes::Widget < Praxis::MediaType
|
23
|
+
# description 'Represents a widget'
|
24
|
+
# identifier 'application/vnd.acme.widget'
|
25
|
+
#
|
26
|
+
# attributes do
|
27
|
+
# attribute :id, Integer
|
28
|
+
# description: 'Database ID'
|
29
|
+
# attribute :href, Attributor::Href,
|
30
|
+
# description: 'Canonical resource refernece'
|
31
|
+
# attribute :color, String,
|
32
|
+
# example: 'red'
|
33
|
+
# attribute :material, String,
|
34
|
+
# description: 'Construction medium of the widget',
|
35
|
+
# values: ['copper', 'steel', 'aluminum']
|
36
|
+
# attribute :factory, MyApp::MediaTypes::Factory,
|
37
|
+
# description: 'The factory in which this widget was produced'
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# links do
|
41
|
+
# link :factory,
|
42
|
+
# description: 'Link to the factory in which this widget was produced'
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# # If widgets can be linked-to by other resources, they should have a link view
|
46
|
+
# view :link do
|
47
|
+
# attribute :href
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# # All resources should have a default view
|
51
|
+
# view :default do
|
52
|
+
# attribute :id
|
53
|
+
# attribute :color
|
54
|
+
# attribute :material
|
55
|
+
# end
|
56
|
+
# end
|
2
57
|
class MediaType < Praxis::Blueprint
|
3
58
|
include Types::MediaTypeCommon
|
4
59
|
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Praxis
|
4
|
+
# Ruby object representation of an Internet Media Type Identifier as defined by
|
5
|
+
# RFC6838; also known as MIME types due to their origin in RFC2046 (the
|
6
|
+
# MIME specification).
|
7
|
+
class MediaTypeIdentifier < Attributor::Model
|
8
|
+
# Postel's principle encourages us to accept anything that MIGHT be an identifier, although
|
9
|
+
# the syntax for type identifiers is substantially narrower than what we accept there.
|
10
|
+
#
|
11
|
+
# Note that this ONLY matches type, subtype and suffix; we handle options differently.
|
12
|
+
VALID_TYPE = /^\s*(?<type>[^\/]+)\/(?<subtype>[^\+]+)(\+(?<suffix>[^; ]+))?\s*$/x.freeze
|
13
|
+
|
14
|
+
# Pattern that separates parameters of a media type from each other, and from the base identifier.
|
15
|
+
PARAMETER_SEPARATOR = /\s*;\s*/x.freeze
|
16
|
+
|
17
|
+
# Pattern used to identify the first "word" when we encounter a malformed type identifier, so
|
18
|
+
# we can apply a heuristic and assume the user meant "application/XYZ".
|
19
|
+
WORD_SEPARATOR = /[^a-z0-9-]/i.freeze
|
20
|
+
|
21
|
+
# Pattern that lets us strip quotes from parameter values.
|
22
|
+
QUOTED_STRING = /^".*"$/.freeze
|
23
|
+
|
24
|
+
# Token that indicates a media-type component that matches anything.
|
25
|
+
WILDCARD = '*'.freeze
|
26
|
+
|
27
|
+
# Inner type representing semicolon-delimited parameters.
|
28
|
+
Parameters = Attributor::Hash.of(key: String)
|
29
|
+
|
30
|
+
attributes do
|
31
|
+
attribute :type, Attributor::String, default: 'application', description: 'RFC2046 media type'
|
32
|
+
attribute :subtype, Attributor::String, default: '*', description: 'RFC2046 media subtype', example: 'vnd.widget'
|
33
|
+
attribute :suffix, Attributor::String, default: '', description: 'RFC6838 structured-syntax suffix', example: 'json'
|
34
|
+
attribute :parameters, Parameters, default: {}, description: "Type-specific parameters", example: "{'type' => 'collection'}"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Parse a media type identifier from a String, or load it from a Hash or Model. Assume malformed
|
38
|
+
# types represent a subtype, e.g. "nachos" => application/nachos"
|
39
|
+
#
|
40
|
+
# @param [String,Hash,Attributor::Model] value
|
41
|
+
# @return [MediaTypeIdentifier]
|
42
|
+
# @see Attributor::Model#load
|
43
|
+
def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
|
44
|
+
if value.is_a?(String)
|
45
|
+
base, *parameters = value.split(PARAMETER_SEPARATOR)
|
46
|
+
match = VALID_TYPE.match(base)
|
47
|
+
|
48
|
+
obj = new
|
49
|
+
if match
|
50
|
+
parameters = parameters.each_with_object({}) do |e, h|
|
51
|
+
k, v = e.split('=', 2)
|
52
|
+
v = v[1...-1] if v =~ QUOTED_STRING
|
53
|
+
h[k] = v
|
54
|
+
end
|
55
|
+
|
56
|
+
obj.type, obj.subtype, obj.suffix, obj.parameters =
|
57
|
+
match[:type], match[:subtype], match[:suffix], parameters
|
58
|
+
else
|
59
|
+
obj.type = 'application'
|
60
|
+
obj.subtype = base.split(WORD_SEPARATOR, 2).first
|
61
|
+
obj.suffix = ''
|
62
|
+
obj.parameters = {}
|
63
|
+
end
|
64
|
+
obj
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Determine whether another identifier is compatible with (i.e. is a subset or specialization of)
|
71
|
+
# this identifier.
|
72
|
+
#
|
73
|
+
# @return [Boolean] true if this type is compatible with other, false otherwise
|
74
|
+
#
|
75
|
+
# @param [MediaTypeIdentifier,String] other
|
76
|
+
#
|
77
|
+
# @example match anything
|
78
|
+
# MediaTypeIdentifier.load('*/*').match('application/icecream+cone; flavor=vanilla') # => true
|
79
|
+
#
|
80
|
+
# @example match a subtype wildcard
|
81
|
+
# MediaTypeIdentifier.load('image/*').match('image/jpeg') # => true
|
82
|
+
#
|
83
|
+
# @example match a specific type irrespective of structured syntax
|
84
|
+
# MediaTypeIdentifier.load('application/vnd.widget').match('application/vnd.widget+json') # => true
|
85
|
+
#
|
86
|
+
# @example match a specific type, respective of important parameters but irrespective of extra parameters or structured syntax
|
87
|
+
# MediaTypeIdentifier.load('application/vnd.widget; type=collection').match('application/vnd.widget+json; material=steel; type=collection') # => true
|
88
|
+
def match(other)
|
89
|
+
other = MediaTypeIdentifier.load(other)
|
90
|
+
|
91
|
+
return false if other.nil?
|
92
|
+
return false unless type == other.type || type == WILDCARD
|
93
|
+
return false unless subtype == other.subtype || subtype == WILDCARD
|
94
|
+
return false unless suffix.empty? || suffix == other.suffix
|
95
|
+
parameters.each_pair do |k, v|
|
96
|
+
return false unless other.parameters[k] == v
|
97
|
+
end
|
98
|
+
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
# Determine whether this type is compatible with (i.e. is a subset or specialization of) another identifier.
|
103
|
+
# This is the same operation as #match, but with the position of the operands switched -- analogous to
|
104
|
+
# "Regexp#match(String)" vs "String =~ Regexp".
|
105
|
+
#
|
106
|
+
# @return [Boolean] true if this type is compatible with other, false otherwise
|
107
|
+
#
|
108
|
+
# @param [MediaTypeIdentifier,String] other
|
109
|
+
#
|
110
|
+
# @see #match
|
111
|
+
def =~(other)
|
112
|
+
other.match(self)
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [String] canonicalized representation of the media type including all suffixes and parameters
|
116
|
+
def to_s
|
117
|
+
# Our handcrafted media types consist of a rich chocolatey center
|
118
|
+
s = "#{type}/#{subtype}"
|
119
|
+
|
120
|
+
# coated in a hard candy shell
|
121
|
+
s << '+' << suffix unless suffix.empty?
|
122
|
+
|
123
|
+
# and encrusted with lexically-ordered sprinkles
|
124
|
+
unless parameters.empty?
|
125
|
+
s << '; '
|
126
|
+
s << parameters.keys.sort.map { |k| "#{k}=#{parameters[k]}" }.join('; ')
|
127
|
+
end
|
128
|
+
|
129
|
+
# May contain peanuts, tree nuts, soy, dairy, sawdust or glue
|
130
|
+
s
|
131
|
+
end
|
132
|
+
|
133
|
+
# If parameters are empty, return self; otherwise return a new object consisting of this type
|
134
|
+
# minus parameters.
|
135
|
+
#
|
136
|
+
# @return [MediaTypeIdentifier]
|
137
|
+
def without_parameters
|
138
|
+
if self.parameters.empty?
|
139
|
+
self
|
140
|
+
else
|
141
|
+
MediaTypeIdentifier.load(type: self.type, subtype: self.subtype, suffix: self.suffix)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Make an educated guess about the structured-syntax encoding implied by this media type,
|
146
|
+
# which in turn governs which handler should be used to parse and generate media of this
|
147
|
+
# type.
|
148
|
+
#
|
149
|
+
# If a suffix e.g. "+json" is present, return it. Otherwise, return the subtype.
|
150
|
+
#
|
151
|
+
# @return [String] a type identifier fragment e.g. "json" or "xml" that MAY indicate encoding
|
152
|
+
#
|
153
|
+
# @see xxx
|
154
|
+
def handler_name
|
155
|
+
suffix.empty? ? subtype : suffix
|
156
|
+
end
|
157
|
+
|
158
|
+
# Extend this type identifier by adding a suffix or parameter(s); return a new type identifier.
|
159
|
+
#
|
160
|
+
# @param [String] extension an optional suffix, followed by an optional semicolon-separated list of name="value" pairs
|
161
|
+
# @return [MediaTypeIdentifier]
|
162
|
+
#
|
163
|
+
# @raise [ArgumentError] when an invalid string is passed (e.g. containing neither parameters nor a suffix)
|
164
|
+
#
|
165
|
+
# @example Indicate JSON structured syntax
|
166
|
+
# MediaTypeIdentifier.new('application/vnd.widget') + 'json' # => 'application/vnd.widget+json'
|
167
|
+
#
|
168
|
+
# @example Indicate UTF8 encoding
|
169
|
+
# MediaTypeIdentifier.new('application/vnd.widget') + 'charset=UTF8' # => 'application/vnd.widget; charset="UTF8"'
|
170
|
+
def +(extension)
|
171
|
+
parameters = extension.split(PARAMETER_SEPARATOR)
|
172
|
+
# remove useless initial '; '
|
173
|
+
parameters.shift if parameters.first && parameters.first.empty?
|
174
|
+
|
175
|
+
raise ArgumentError, "Must pass a type identifier suffix and/or parameters" if parameters.empty?
|
176
|
+
|
177
|
+
suffix = parameters.shift unless parameters.first.include?('=')
|
178
|
+
# remove redundant '+'
|
179
|
+
suffix = suffix[1..-1] if suffix && suffix[0] == '+'
|
180
|
+
|
181
|
+
parameters = parameters.each_with_object({}) do |e, h|
|
182
|
+
k, v = e.split('=', 2)
|
183
|
+
v = v[1...-1] if v =~ /^".*"$/
|
184
|
+
h[k] = v
|
185
|
+
h
|
186
|
+
end
|
187
|
+
parameters = Parameters.load(parameters)
|
188
|
+
|
189
|
+
obj = self.class.new
|
190
|
+
obj.type = self.type
|
191
|
+
obj.subtype = self.subtype
|
192
|
+
obj.suffix = suffix || self.suffix || ''
|
193
|
+
obj.parameters = self.parameters.merge(parameters)
|
194
|
+
|
195
|
+
obj
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -11,6 +11,22 @@ module Praxis
|
|
11
11
|
@filename = filename
|
12
12
|
end
|
13
13
|
|
14
|
+
# Determine the content type of this response.
|
15
|
+
#
|
16
|
+
# @return [MediaTypeIdentifier]
|
17
|
+
def content_type
|
18
|
+
MediaTypeIdentifier.load(headers['Content-Type']).freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set the content type for this response.
|
22
|
+
# @todo DRY this out (also used in Response)
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
# @param [String,MediaTypeIdentifier] identifier
|
26
|
+
def content_type=(identifier)
|
27
|
+
headers['Content-Type'] = MediaTypeIdentifier.load(identifier).to_s
|
28
|
+
end
|
29
|
+
|
14
30
|
def status
|
15
31
|
@headers['Status'].to_i
|
16
32
|
end
|