roadforest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/examples/file-management.rb +98 -0
- data/lib/roadforest/application/dispatcher.rb +54 -0
- data/lib/roadforest/application/parameters.rb +39 -0
- data/lib/roadforest/application/path-provider.rb +18 -0
- data/lib/roadforest/application/route-adapter.rb +24 -0
- data/lib/roadforest/application/services-host.rb +10 -0
- data/lib/roadforest/application.rb +42 -0
- data/lib/roadforest/blob-model.rb +56 -0
- data/lib/roadforest/content-handling/engine.rb +113 -0
- data/lib/roadforest/content-handling/media-type.rb +222 -0
- data/lib/roadforest/content-handling/type-handlers/jsonld.rb +172 -0
- data/lib/roadforest/http/adapters/excon.rb +47 -0
- data/lib/roadforest/http/graph-response.rb +20 -0
- data/lib/roadforest/http/graph-transfer.rb +112 -0
- data/lib/roadforest/http/message.rb +91 -0
- data/lib/roadforest/model.rb +151 -0
- data/lib/roadforest/models.rb +2 -0
- data/lib/roadforest/rdf/context-fascade.rb +25 -0
- data/lib/roadforest/rdf/document.rb +23 -0
- data/lib/roadforest/rdf/focus-list.rb +19 -0
- data/lib/roadforest/rdf/focus-wrapping.rb +30 -0
- data/lib/roadforest/rdf/graph-copier.rb +16 -0
- data/lib/roadforest/rdf/graph-focus.rb +95 -0
- data/lib/roadforest/rdf/graph-reading.rb +145 -0
- data/lib/roadforest/rdf/graph-store.rb +217 -0
- data/lib/roadforest/rdf/investigation.rb +90 -0
- data/lib/roadforest/rdf/normalization.rb +150 -0
- data/lib/roadforest/rdf/parcel.rb +47 -0
- data/lib/roadforest/rdf/post-focus.rb +35 -0
- data/lib/roadforest/rdf/resource-pattern.rb +60 -0
- data/lib/roadforest/rdf/resource-query.rb +58 -0
- data/lib/roadforest/rdf/source-rigor/credence/any.rb +9 -0
- data/lib/roadforest/rdf/source-rigor/credence/none-if-role-absent.rb +19 -0
- data/lib/roadforest/rdf/source-rigor/credence/role-if-available.rb +19 -0
- data/lib/roadforest/rdf/source-rigor/credence-annealer.rb +22 -0
- data/lib/roadforest/rdf/source-rigor/credence.rb +29 -0
- data/lib/roadforest/rdf/source-rigor/http-investigator.rb +20 -0
- data/lib/roadforest/rdf/source-rigor/investigator.rb +17 -0
- data/lib/roadforest/rdf/source-rigor/null-investigator.rb +10 -0
- data/lib/roadforest/rdf/source-rigor.rb +44 -0
- data/lib/roadforest/rdf/update-focus.rb +73 -0
- data/lib/roadforest/rdf/vocabulary.rb +11 -0
- data/lib/roadforest/rdf.rb +6 -0
- data/lib/roadforest/remote-host.rb +96 -0
- data/lib/roadforest/resource/handlers.rb +43 -0
- data/lib/roadforest/resource/http/form-parsing.rb +81 -0
- data/lib/roadforest/resource/rdf/leaf-item.rb +21 -0
- data/lib/roadforest/resource/rdf/list.rb +19 -0
- data/lib/roadforest/resource/rdf/parent-item.rb +26 -0
- data/lib/roadforest/resource/rdf/read-only.rb +100 -0
- data/lib/roadforest/resource/rdf.rb +4 -0
- data/lib/roadforest/resource/role/has-children.rb +22 -0
- data/lib/roadforest/resource/role/writable.rb +43 -0
- data/lib/roadforest/server.rb +3 -0
- data/lib/roadforest/test-support/dispatcher-facade.rb +77 -0
- data/lib/roadforest/test-support/http-client.rb +151 -0
- data/lib/roadforest/test-support/matchers.rb +67 -0
- data/lib/roadforest/test-support/remote-host.rb +23 -0
- data/lib/roadforest/test-support/trace-formatter.rb +140 -0
- data/lib/roadforest/test-support.rb +2 -0
- data/lib/roadforest/utility/class-registry.rb +49 -0
- data/lib/roadforest.rb +2 -0
- data/spec/client.rb +152 -0
- data/spec/credence-annealer.rb +44 -0
- data/spec/graph-copier.rb +87 -0
- data/spec/graph-store.rb +142 -0
- data/spec/media-types.rb +14 -0
- data/spec/rdf-parcel.rb +158 -0
- data/spec/update-focus.rb +117 -0
- data/spec_support/gem_test_suite.rb +0 -0
- metadata +241 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rdf/vocab/skos'
|
2
|
+
|
3
|
+
module FileManagementExample
|
4
|
+
module Vocabulary
|
5
|
+
class LC < ::RDF::Vocabulary("http://lrdesign.com/vocabularies/logical-construct#"); end
|
6
|
+
end
|
7
|
+
|
8
|
+
class ServicesHost < ::RoadForest::Application::ServicesHost
|
9
|
+
attr_accessor :file_records, :destination_dir
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@file_records = []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
FileRecord = Struct.new(:name, :resolved)
|
17
|
+
|
18
|
+
class Application < RoadForest::Application
|
19
|
+
def setup
|
20
|
+
router.add :root, [], :read_only, Models::Navigation
|
21
|
+
router.add :unresolved_needs, ["unresolved_needs"], :parent, Models::UnresolvedNeedsList
|
22
|
+
router.add_traced :need, ["needs",'*'], :leaf, Models::Need
|
23
|
+
router.add :file_content, ["files","*"], :leaf, Models::NeedContent
|
24
|
+
end
|
25
|
+
|
26
|
+
module Models
|
27
|
+
class Navigation < RoadForest::RDFModel
|
28
|
+
def exists?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def update(graph)
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
|
36
|
+
def nav_entry(graph, name, path)
|
37
|
+
graph.add_node([:skos, :hasTopConcept], "#" + name) do |entry|
|
38
|
+
entry[:rdf, :type] = [:skos, "Concept"]
|
39
|
+
entry[:skos, :label] = name
|
40
|
+
entry[:foaf, "page"] = path
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def fill_graph(graph)
|
45
|
+
graph[:rdf, "type"] = [:skos, "ConceptScheme"]
|
46
|
+
nav_entry(graph, "Unresolved", path_for(:unresolved_needs))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class UnresolvedNeedsList < RoadForest::RDFModel
|
51
|
+
def exists?
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def update(graph)
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_child(graph)
|
59
|
+
new_file = FileRecord.new(graph.first(:lc, "name"), false)
|
60
|
+
services.file_records << new_file
|
61
|
+
end
|
62
|
+
|
63
|
+
def fill_graph(graph)
|
64
|
+
graph.add_list(:lc, "needs") do |list|
|
65
|
+
services.file_records.each do |record|
|
66
|
+
if !record.resolved
|
67
|
+
list << path_for(:need, '*' => record.name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class NeedContent < RoadForest::BlobModel
|
75
|
+
add_type "text/plain", TypeHandlers::Handler.new
|
76
|
+
end
|
77
|
+
|
78
|
+
class Need < RoadForest::RDFModel
|
79
|
+
def data
|
80
|
+
@data = services.file_records.find do |record|
|
81
|
+
record.name == params.remainder
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def graph_update(graph)
|
86
|
+
data.resolved = graph[[:lc, "resolved"]]
|
87
|
+
new_graph
|
88
|
+
end
|
89
|
+
|
90
|
+
def fill_graph(graph)
|
91
|
+
graph[[:lc, "resolved"]] = data.resolved
|
92
|
+
graph[[:lc, "name"]] = data.name
|
93
|
+
graph[[:lc, "contents"]] = path_for(:file_content)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'webmachine'
|
2
|
+
|
3
|
+
module RoadForest
|
4
|
+
class Dispatcher < Webmachine::Dispatcher
|
5
|
+
include Resource::Handlers
|
6
|
+
def initialize(services)
|
7
|
+
super(method(:create_resource))
|
8
|
+
@services = services
|
9
|
+
@route_names = {}
|
10
|
+
end
|
11
|
+
attr_accessor :services
|
12
|
+
|
13
|
+
def resource_route(resource, name, path_spec, bindings)
|
14
|
+
route = Route.new(path_spec, resource, bindings || {})
|
15
|
+
yield route if block_given?
|
16
|
+
@route_names[name] = route
|
17
|
+
@routes << route
|
18
|
+
route
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_route(name, path_spec, resource_type, model_class, bindings = nil, &block)
|
22
|
+
resource = bundle_typed_resource(resource_type, model_class, name)
|
23
|
+
resource_route(resource, name, path_spec, bindings, &block)
|
24
|
+
end
|
25
|
+
alias add add_route
|
26
|
+
|
27
|
+
def add_traced_route(name, path_spec, resource_type, model_class, bindings = nil, &block)
|
28
|
+
resource = bundle_traced_resource(resource_type, model_class, name)
|
29
|
+
resource_route(resource, name, path_spec, bindings, &block)
|
30
|
+
end
|
31
|
+
alias add_traced add_traced_route
|
32
|
+
|
33
|
+
def route_for_name(name)
|
34
|
+
@route_names.fetch(name)
|
35
|
+
end
|
36
|
+
|
37
|
+
class Route < Webmachine::Dispatcher::Route
|
38
|
+
# Create a complete URL for this route, doing any necessary variable
|
39
|
+
# substitution.
|
40
|
+
# @param [Hash] vars values for the path variables
|
41
|
+
# @return [String] the valid URL for the route
|
42
|
+
def build_path(vars = {})
|
43
|
+
"/" + path_spec.map do |segment|
|
44
|
+
case segment
|
45
|
+
when '*',Symbol
|
46
|
+
vars.fetch(segment)
|
47
|
+
when String
|
48
|
+
segment
|
49
|
+
end
|
50
|
+
end.join("/")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module RoadForest
|
2
|
+
class Application
|
3
|
+
#Parameters extracted from a URL, which a model object can use to identify
|
4
|
+
#the resource being discussed
|
5
|
+
class Parameters
|
6
|
+
def initialize
|
7
|
+
@path_info = {}
|
8
|
+
@query_params = {}
|
9
|
+
@path_tokens = []
|
10
|
+
yield self if block_given?
|
11
|
+
end
|
12
|
+
attr_accessor :path_info, :query_params, :path_tokens
|
13
|
+
|
14
|
+
def [](field_name)
|
15
|
+
return path_tokens if field_Name == '*'
|
16
|
+
@path_info[field_name] || @query_params[field_name]
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch(field_name)
|
20
|
+
return path_tokens if field_Name == '*'
|
21
|
+
@path_info[field_name] || @query_params.fetch(field_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def slice(*fields)
|
25
|
+
fields.each_with_object({}) do |name, hash|
|
26
|
+
hash[name] = self[name]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def remainder
|
31
|
+
@remainder = @path_tokens.join("/")
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_hash
|
35
|
+
(query_params||{}).merge(path_info||{}).merge('*' => path_tokens)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RoadForest
|
2
|
+
class PathProvider
|
3
|
+
def initialize(dispatcher)
|
4
|
+
@dispatcher = dispatcher
|
5
|
+
end
|
6
|
+
|
7
|
+
# Get the URL to the given resource, with optional variables to be used
|
8
|
+
# for bindings in the path spec.
|
9
|
+
# @param [Webmachine::Resource] resource the resource to link to
|
10
|
+
# @param [Hash] vars the values for the required path variables
|
11
|
+
# @raise [RuntimeError] Raised if the resource is not routable.
|
12
|
+
# @return [String] the URL
|
13
|
+
def path_for(name, vars = {})
|
14
|
+
route = @dispatcher.route_for_name(name)
|
15
|
+
::RDF::URI.parse(route.build_path(vars))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RoadForest
|
2
|
+
class Application
|
3
|
+
class RouteAdapter
|
4
|
+
def initialize(resource_class, &setup_block)
|
5
|
+
@resource_class = resource_class
|
6
|
+
@setup_block = setup_block
|
7
|
+
end
|
8
|
+
|
9
|
+
def new(request, response)
|
10
|
+
resource = @resource_class.new(request, response)
|
11
|
+
@setup_block[resource, request, response]
|
12
|
+
resource
|
13
|
+
end
|
14
|
+
|
15
|
+
def <(klass)
|
16
|
+
if klass <= Webmachine::Resource
|
17
|
+
return true
|
18
|
+
else
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class RoadForest::Application
|
2
|
+
#XXX Worth doing some meta to get sanity checking of configs here? Better
|
3
|
+
#fail early if there's no DB configured, right?
|
4
|
+
class ServicesHost
|
5
|
+
def initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_accessor :router, :canonical_host, :type_handling
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'webmachine/application'
|
2
|
+
module RoadForest
|
3
|
+
class Application < Webmachine::Application; end
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'roadforest/resource/handlers'
|
7
|
+
require 'roadforest/application/dispatcher'
|
8
|
+
require 'roadforest/application/path-provider'
|
9
|
+
require 'roadforest/application/services-host'
|
10
|
+
require 'roadforest/resource/rdf'
|
11
|
+
require 'roadforest/content-handling/engine'
|
12
|
+
|
13
|
+
module RoadForest
|
14
|
+
class Application
|
15
|
+
include Resource::Handlers
|
16
|
+
|
17
|
+
def initialize(canonical_host, services, configuration = nil, dispatcher = nil)
|
18
|
+
@canonical_host = ::RDF::URI.parse(canonical_host)
|
19
|
+
configuration ||= Webmachine::Configuration.default
|
20
|
+
dispatcher ||= Dispatcher.new(services)
|
21
|
+
super(configuration, dispatcher)
|
22
|
+
self.services = services
|
23
|
+
|
24
|
+
setup
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :services, :canonical_host
|
31
|
+
|
32
|
+
alias router dispatcher
|
33
|
+
|
34
|
+
def services=(service_host)
|
35
|
+
router.services = service_host
|
36
|
+
@services = service_host
|
37
|
+
@services.canonical_host = @canonical_host
|
38
|
+
@services.router = PathProvider.new(@dispatcher)
|
39
|
+
@services.type_handling ||= ContentHandling::Engine.default
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'roadforest/model'
|
2
|
+
require 'roadforest/content-handling/type-handlers/jsonld'
|
3
|
+
|
4
|
+
module RoadForest
|
5
|
+
class BlobModel < Model
|
6
|
+
TypeHandlers = RoadForest::MediaType::Handlers
|
7
|
+
class << self
|
8
|
+
def type_handling
|
9
|
+
@engine ||= ContentHandling::Engine.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_type(type, handler)
|
13
|
+
add_parser(type, handler)
|
14
|
+
add_renderer(type, handler)
|
15
|
+
end
|
16
|
+
alias add add_type
|
17
|
+
|
18
|
+
def add_parser(type, handler)
|
19
|
+
type_handling.add_parser(type, handler)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_renderer(type, handler)
|
23
|
+
type_handling.add_renderer(type, handler)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def type_handling
|
28
|
+
self.class.type_handling
|
29
|
+
end
|
30
|
+
|
31
|
+
def destination_dir
|
32
|
+
Pathname.new(services.destination_dir)
|
33
|
+
end
|
34
|
+
|
35
|
+
def sub_path
|
36
|
+
params.remainder
|
37
|
+
end
|
38
|
+
|
39
|
+
def path
|
40
|
+
destination_dir.join(sub_path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def retrieve
|
44
|
+
File::open(path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def update(incoming)
|
48
|
+
File::open(path, "w") do |file|
|
49
|
+
incoming.each do |chunk|
|
50
|
+
file.write(chunk)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
return nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'roadforest/content-handling/media-type'
|
2
|
+
|
3
|
+
module RoadForest
|
4
|
+
module ContentHandling
|
5
|
+
class Engine
|
6
|
+
class TypeHandlerList
|
7
|
+
def initialize(prefix)
|
8
|
+
@prefix = prefix
|
9
|
+
@types = MediaTypeList.new
|
10
|
+
@handlers = {}
|
11
|
+
@type_map = []
|
12
|
+
@symbol_lookup = {}
|
13
|
+
end
|
14
|
+
attr_reader :handlers, :types, :type_map
|
15
|
+
|
16
|
+
def add(handler)
|
17
|
+
type = handler.type
|
18
|
+
@types.add(type)
|
19
|
+
@handlers[type] = handler
|
20
|
+
symbol = handler_symbol(type)
|
21
|
+
raise "Type collision: #{type} already in #{self.inspect}" if @symbol_lookup.has_key?(symbol)
|
22
|
+
@type_map << [type.content_type_header, symbol]
|
23
|
+
@symbol_lookup[symbol] = handler
|
24
|
+
end
|
25
|
+
|
26
|
+
def handler_symbol(type)
|
27
|
+
"#{@prefix}_#{type.accept_header.gsub(/\W/, "_")}".to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch(symbol, &block)
|
31
|
+
@symbol_lookup.fetch(symbol, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def reset
|
35
|
+
@handlers.clear
|
36
|
+
end
|
37
|
+
|
38
|
+
def handler_for(type)
|
39
|
+
type = MediaType.parse(type)
|
40
|
+
@handlers.fetch(type)
|
41
|
+
rescue KeyError
|
42
|
+
raise "No Content-Type handler for #{content_type}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.default
|
47
|
+
require 'roadforest/content-handling/type-handlers/jsonld'
|
48
|
+
self.new.tap do |engine|
|
49
|
+
engine.add "application/ld+json", RoadForest::MediaType::Handlers::JSONLD.new
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize
|
54
|
+
@renderers = TypeHandlerList.new("provide")
|
55
|
+
@parsers = TypeHandlerList.new("accept")
|
56
|
+
@type_mapping = {}
|
57
|
+
end
|
58
|
+
attr_reader :renderers, :parsers
|
59
|
+
|
60
|
+
def add_type(type, handler)
|
61
|
+
type = MediaType.parse(type)
|
62
|
+
add_parser(type, handler)
|
63
|
+
add_renderer(type, handler)
|
64
|
+
end
|
65
|
+
alias add add_type
|
66
|
+
|
67
|
+
def add_parser(type, object)
|
68
|
+
type = MediaType.parse(type)
|
69
|
+
wrapper = RoadForest::MediaType::Handlers::Wrap::Parse.new(type, object)
|
70
|
+
parsers.add(wrapper)
|
71
|
+
end
|
72
|
+
alias accept add_parser
|
73
|
+
|
74
|
+
def add_renderer(type, object)
|
75
|
+
type = MediaType.parse(type)
|
76
|
+
wrapper = RoadForest::MediaType::Handlers::Wrap::Render.new(type, object)
|
77
|
+
renderers.add(wrapper)
|
78
|
+
end
|
79
|
+
alias provide add_renderer
|
80
|
+
|
81
|
+
def fetch(symbol)
|
82
|
+
@renderers.fetch(symbol){ @parsers.fetch(symbol) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def choose_renderer(header)
|
86
|
+
content_type = choose_media_type(renderers.types, header)
|
87
|
+
return renderers.handler_for(content_type)
|
88
|
+
end
|
89
|
+
|
90
|
+
def each_renderer(&block)
|
91
|
+
renderers.handlers.enum_for(:each_pair) unless block_given?
|
92
|
+
renderers.handlers.each_pair(&block)
|
93
|
+
end
|
94
|
+
|
95
|
+
def choose_parser(header)
|
96
|
+
content_type = choose_media_type(parsers.types, header)
|
97
|
+
return parsers.handler_for(content_type)
|
98
|
+
end
|
99
|
+
|
100
|
+
def each_parser(&block)
|
101
|
+
parsers.handlers.enum_for(:each_pair) unless block_given?
|
102
|
+
parsers.handlers.each_pair(&block)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Given the 'Accept' header and provided types, chooses an
|
106
|
+
# appropriate media type.
|
107
|
+
def choose_media_type(provided, header)
|
108
|
+
requested = MediaTypeList.build(header.split(/\s*,\s*/))
|
109
|
+
requested.best_match_from(provided)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
module RoadForest
|
2
|
+
module ContentHandling
|
3
|
+
#@credit goes to Sean Cribbs & Ruby Webmachine for the basis of this code
|
4
|
+
#
|
5
|
+
# Encapsulates a MIME media type, with logic for matching types.
|
6
|
+
class MediaType
|
7
|
+
# Matches valid media types
|
8
|
+
MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\s*\S+\s*)*)\s*$/
|
9
|
+
|
10
|
+
# Matches sub-type parameters
|
11
|
+
PARAMS_REGEX = /;\s*([^=]+)=([^;=\s]+)/
|
12
|
+
|
13
|
+
# Creates a new MediaType by parsing an alternate representation.
|
14
|
+
# @param [MediaType, String, Array<String,Hash>] obj the raw type
|
15
|
+
# to be parsed
|
16
|
+
# @return [MediaType] the parsed media type
|
17
|
+
# @raise [ArgumentError] when the type could not be parsed
|
18
|
+
def self.parse(obj)
|
19
|
+
case obj
|
20
|
+
when MediaType
|
21
|
+
obj
|
22
|
+
when MEDIA_TYPE_REGEX
|
23
|
+
type, raw_params = $1, $2
|
24
|
+
params = Hash[raw_params.scan(PARAMS_REGEX)]
|
25
|
+
new(type, params)
|
26
|
+
else
|
27
|
+
unless Array === obj && String === obj[0] && Hash === obj[1]
|
28
|
+
raise ArgumentError, "Invalid media type #{obj.inspect}"
|
29
|
+
end
|
30
|
+
type = parse(obj[0])
|
31
|
+
type.params.merge!(obj[1])
|
32
|
+
type
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String] the MIME media type
|
37
|
+
attr_accessor :type
|
38
|
+
|
39
|
+
# @return [Hash] any type parameters, e.g. charset
|
40
|
+
attr_accessor :params
|
41
|
+
|
42
|
+
# @param [String] type the main media type, e.g. application/json
|
43
|
+
# @param [Hash] params the media type parameters
|
44
|
+
def initialize(type, params={})
|
45
|
+
@type, @params = type, params
|
46
|
+
@quality = (@params.delete('q') || "1.0").to_f
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :quality
|
50
|
+
|
51
|
+
# Detects whether the {MediaType} represents an open wildcard
|
52
|
+
# type, that is, "*/*" without any {#params}.
|
53
|
+
def matches_all?
|
54
|
+
@type == "*/*" && @params.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [true,false] Are these two types strictly equal?
|
58
|
+
# @param other the other media type.
|
59
|
+
# @see MediaType.parse
|
60
|
+
def ==(other)
|
61
|
+
other = self.class.parse(other)
|
62
|
+
other.type == type && other.params == params
|
63
|
+
end
|
64
|
+
|
65
|
+
# Detects whether this {MediaType} matches the other {MediaType},
|
66
|
+
# taking into account wildcards. Sub-type parameters are treated
|
67
|
+
# strictly.
|
68
|
+
# @param [MediaType, String, Array<String,Hash>] other the other type
|
69
|
+
# @return [true,false] whether it is an acceptable match
|
70
|
+
def exact_match?(other)
|
71
|
+
other = self.class.parse(other)
|
72
|
+
type_matches?(other) && other.params == params
|
73
|
+
end
|
74
|
+
|
75
|
+
# Detects whether the {MediaType} is an acceptable match for the
|
76
|
+
# other {MediaType}, taking into account wildcards and satisfying
|
77
|
+
# all requested parameters, but allowing this type to have extra
|
78
|
+
# specificity.
|
79
|
+
# @param [MediaType, String, Array<String,Hash>] other the other type
|
80
|
+
# @return [true,false] whether it is an acceptable match
|
81
|
+
def match?(other)
|
82
|
+
other = self.class.parse(other)
|
83
|
+
type_matches?(other) && params_match?(other.params)
|
84
|
+
end
|
85
|
+
alias =~ match?
|
86
|
+
|
87
|
+
# Detects whether the passed sub-type parameters are all satisfied
|
88
|
+
# by this {MediaType}. The receiver is allowed to have other
|
89
|
+
# params than the ones specified, but all specified must be equal.
|
90
|
+
# @param [Hash] params the requested params
|
91
|
+
# @return [true,false] whether it is an acceptable match
|
92
|
+
def params_match?(other)
|
93
|
+
other.all? do |k,v|
|
94
|
+
params[k] == v
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def accept_header
|
99
|
+
[type, "q=#{quality}", *params.map {|k,v| "#{k}=#{v}" }].join(";")
|
100
|
+
end
|
101
|
+
|
102
|
+
def content_type_header
|
103
|
+
[type, *params.map {|k,v| "#{k}=#{v}" }].join(";")
|
104
|
+
end
|
105
|
+
alias to_s content_type_header
|
106
|
+
|
107
|
+
# @return [String] The major type, e.g. "application", "text", "image"
|
108
|
+
def major
|
109
|
+
@major ||= type.split("/").first
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [String] the minor or sub-type, e.g. "json", "html", "jpeg"
|
113
|
+
def minor
|
114
|
+
@minor ||= type.split("/").last
|
115
|
+
end
|
116
|
+
|
117
|
+
def precedence_index
|
118
|
+
[
|
119
|
+
@major == "*" ? 0 : 1,
|
120
|
+
@minor == "*" ? 0 : 1,
|
121
|
+
(@params.keys - %w{q}).length
|
122
|
+
]
|
123
|
+
end
|
124
|
+
|
125
|
+
# @param [MediaType] other the other type
|
126
|
+
# @return [true,false] whether the main media type is acceptable,
|
127
|
+
# ignoring params and taking into account wildcards
|
128
|
+
def type_matches?(other)
|
129
|
+
other = self.class.parse(other)
|
130
|
+
if ["*", "*/*", type].include?(other.type)
|
131
|
+
true
|
132
|
+
else
|
133
|
+
other.major == major && other.minor == "*"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class MediaTypeList
|
139
|
+
# Given an acceptance list, create a PriorityList from them.
|
140
|
+
def self.build(list)
|
141
|
+
return list if self === list
|
142
|
+
|
143
|
+
case list
|
144
|
+
when Array
|
145
|
+
when String
|
146
|
+
list = list.split(/\s*,\s*/)
|
147
|
+
else
|
148
|
+
raise "Cannot build a MediaTypeList from #{list.inspect}"
|
149
|
+
end
|
150
|
+
|
151
|
+
new.tap do |plist|
|
152
|
+
list.each {|item| plist.add_header_val(item) }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
include Enumerable
|
157
|
+
|
158
|
+
# Creates a {PriorityList}.
|
159
|
+
# @see PriorityList::build
|
160
|
+
def initialize
|
161
|
+
@list = []
|
162
|
+
end
|
163
|
+
|
164
|
+
def accept_header
|
165
|
+
@list.map(&:accept_header).join(", ")
|
166
|
+
end
|
167
|
+
alias to_s accept_header
|
168
|
+
|
169
|
+
#Given another MediaTypeList, find the media type that is the best match
|
170
|
+
#between them - generally, the idea is to match an Accept header with a
|
171
|
+
#local list of provided types
|
172
|
+
def best_match_from(other)
|
173
|
+
other.max_by do |their_type|
|
174
|
+
best_type = self.by_precedence.find do |our_type|
|
175
|
+
their_type =~ our_type
|
176
|
+
end
|
177
|
+
if best_type.nil?
|
178
|
+
0
|
179
|
+
else
|
180
|
+
best_type.quality * their_type.quality
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def by_precedence
|
186
|
+
self.sort do |left, right|
|
187
|
+
right.precedence_index <=> left.precedence_index
|
188
|
+
end.enum_for(:each)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Adds an acceptable item with the given priority to the list.
|
192
|
+
# @param [Float] q the priority
|
193
|
+
# @param [String] choice the acceptable item
|
194
|
+
def add(type)
|
195
|
+
@list << type
|
196
|
+
end
|
197
|
+
|
198
|
+
# Given a raw acceptable value from an acceptance header,
|
199
|
+
# parse and add it to the list.
|
200
|
+
# @param [String] c the raw acceptable item
|
201
|
+
# @see #add
|
202
|
+
def add_header_val(type_string)
|
203
|
+
add(MediaType.parse(type_string))
|
204
|
+
rescue ArgumentError
|
205
|
+
raise "Invalid media type"
|
206
|
+
end
|
207
|
+
|
208
|
+
# Iterates over the list in priority order, that is, taking
|
209
|
+
# into account the order in which items were added as well as
|
210
|
+
# their priorities.
|
211
|
+
# @yield [q,v]
|
212
|
+
# @yieldparam [Float] q the acceptable item's priority
|
213
|
+
# @yieldparam [String] v the acceptable item
|
214
|
+
def each
|
215
|
+
return enum_for(:each) unless block_given?
|
216
|
+
@list.each do |item|
|
217
|
+
yield item
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|