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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -1
  3. data/Gemfile +5 -0
  4. data/bin/praxis +7 -4
  5. data/lib/api_browser/package.json +1 -1
  6. data/lib/praxis/action_definition.rb +10 -5
  7. data/lib/praxis/application.rb +30 -0
  8. data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -2
  9. data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +7 -2
  10. data/lib/praxis/file_group.rb +1 -1
  11. data/lib/praxis/handlers/json.rb +32 -0
  12. data/lib/praxis/handlers/www_form.rb +20 -0
  13. data/lib/praxis/handlers/xml.rb +78 -0
  14. data/lib/praxis/links.rb +4 -1
  15. data/lib/praxis/media_type.rb +55 -0
  16. data/lib/praxis/media_type_identifier.rb +198 -0
  17. data/lib/praxis/multipart/part.rb +16 -0
  18. data/lib/praxis/request.rb +34 -25
  19. data/lib/praxis/response.rb +22 -3
  20. data/lib/praxis/response_definition.rb +11 -36
  21. data/lib/praxis/restful_doc_generator.rb +1 -1
  22. data/lib/praxis/simple_media_type.rb +6 -1
  23. data/lib/praxis/types/media_type_common.rb +8 -3
  24. data/lib/praxis/types/multipart.rb +6 -6
  25. data/lib/praxis/version.rb +1 -1
  26. data/lib/praxis.rb +7 -1
  27. data/praxis.gemspec +1 -1
  28. data/spec/functional_spec.rb +3 -1
  29. data/spec/praxis/application_spec.rb +58 -1
  30. data/spec/praxis/handlers/json_spec.rb +37 -0
  31. data/spec/praxis/handlers/xml_spec.rb +155 -0
  32. data/spec/praxis/media_type_identifier_spec.rb +209 -0
  33. data/spec/praxis/media_type_spec.rb +50 -3
  34. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +2 -2
  35. data/spec/praxis/request_spec.rb +39 -1
  36. data/spec/praxis/response_definition_spec.rb +12 -9
  37. data/spec/praxis/response_spec.rb +37 -6
  38. data/spec/praxis/types/collection_spec.rb +2 -2
  39. data/spec/praxis/types/multipart_spec.rb +17 -0
  40. data/spec/spec_app/app/controllers/instances.rb +1 -1
  41. data/spec/support/spec_media_types.rb +19 -0
  42. metadata +11 -6
  43. data/lib/praxis/content_type_parser.rb +0 -62
  44. 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: 969f34805663e8986a5ac60e86216c3139324d98
4
- data.tar.gz: 8fb76988854889627663ff66874ecb1a8f4549b0
3
+ metadata.gz: 0dbe3630ec0519b3fe1c0c4581155e1fd9858002
4
+ data.tar.gz: 1a93b0745b2413203e13a69a1a7c83e41f4c7ab8
5
5
  SHA512:
6
- metadata.gz: 228bc86fd20fef3ad70b282519266378b6f90333e36438923b5a5ed8c416ccacf7d5ac4444bab3d675a2bdd210a3cb89a62090d3ce780d2a5c62cc9c848216d1
7
- data.tar.gz: b355e2bd55fe51389ba695d8348303560208d960694f8832d74ebf59942b3e9088cf757c48ae37109b975d6dac238815f15528457c32655c2baa3c3c9b7cf290
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.14.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
@@ -1,3 +1,8 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ group :test do
6
+ gem 'nokogiri'
7
+ gem 'builder'
8
+ end
data/bin/praxis CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  require 'bundler'
4
4
 
5
- Bundler.setup :default, :test
6
- Bundler.require :default, :test
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)
@@ -23,7 +23,7 @@
23
23
  "grunt-file-blocks": "^0.3.0",
24
24
  "grunt-filerev": "^0.2.1",
25
25
  "grunt-ngmin": "0.0.3",
26
- "grunt-sass": "^0.17.0",
26
+ "grunt-sass": "^0.18.0",
27
27
  "grunt-usemin": "^2.1.1",
28
28
  "grunt-wiredep": "^1.7.0",
29
29
  "load-grunt-tasks": "^0.4.0"
@@ -173,10 +173,15 @@ module Praxis
173
173
  end
174
174
 
175
175
  def params_description
176
- route_params = primary_route.path.
177
- named_captures.
178
- keys.
179
- collect(&:to_sym)
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
@@ -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 files application files under #{base} were not loaded:\n"
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
- puts msg
39
+ warn msg
35
40
  end
36
41
  end
37
42
 
@@ -7,7 +7,7 @@ module Praxis
7
7
  def initialize(base, &block)
8
8
  if base.nil?
9
9
  raise ArgumentError, "base must not be nil." \
10
- "Are you missing a call Praxis::Application.instance.setup?"
10
+ "Are you missing a call Praxis::Application.instance.setup?"
11
11
  end
12
12
 
13
13
 
@@ -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.type.new(value)
75
+ attribute.load(value)
73
76
  end
74
77
 
75
78
  # do whatever crazy aliasing we need to here....
@@ -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