praxis 0.14.0 → 0.15.0

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