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