roadforest 0.0.1
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.
- 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
|