roadforest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/examples/file-management.rb +98 -0
  2. data/lib/roadforest/application/dispatcher.rb +54 -0
  3. data/lib/roadforest/application/parameters.rb +39 -0
  4. data/lib/roadforest/application/path-provider.rb +18 -0
  5. data/lib/roadforest/application/route-adapter.rb +24 -0
  6. data/lib/roadforest/application/services-host.rb +10 -0
  7. data/lib/roadforest/application.rb +42 -0
  8. data/lib/roadforest/blob-model.rb +56 -0
  9. data/lib/roadforest/content-handling/engine.rb +113 -0
  10. data/lib/roadforest/content-handling/media-type.rb +222 -0
  11. data/lib/roadforest/content-handling/type-handlers/jsonld.rb +172 -0
  12. data/lib/roadforest/http/adapters/excon.rb +47 -0
  13. data/lib/roadforest/http/graph-response.rb +20 -0
  14. data/lib/roadforest/http/graph-transfer.rb +112 -0
  15. data/lib/roadforest/http/message.rb +91 -0
  16. data/lib/roadforest/model.rb +151 -0
  17. data/lib/roadforest/models.rb +2 -0
  18. data/lib/roadforest/rdf/context-fascade.rb +25 -0
  19. data/lib/roadforest/rdf/document.rb +23 -0
  20. data/lib/roadforest/rdf/focus-list.rb +19 -0
  21. data/lib/roadforest/rdf/focus-wrapping.rb +30 -0
  22. data/lib/roadforest/rdf/graph-copier.rb +16 -0
  23. data/lib/roadforest/rdf/graph-focus.rb +95 -0
  24. data/lib/roadforest/rdf/graph-reading.rb +145 -0
  25. data/lib/roadforest/rdf/graph-store.rb +217 -0
  26. data/lib/roadforest/rdf/investigation.rb +90 -0
  27. data/lib/roadforest/rdf/normalization.rb +150 -0
  28. data/lib/roadforest/rdf/parcel.rb +47 -0
  29. data/lib/roadforest/rdf/post-focus.rb +35 -0
  30. data/lib/roadforest/rdf/resource-pattern.rb +60 -0
  31. data/lib/roadforest/rdf/resource-query.rb +58 -0
  32. data/lib/roadforest/rdf/source-rigor/credence/any.rb +9 -0
  33. data/lib/roadforest/rdf/source-rigor/credence/none-if-role-absent.rb +19 -0
  34. data/lib/roadforest/rdf/source-rigor/credence/role-if-available.rb +19 -0
  35. data/lib/roadforest/rdf/source-rigor/credence-annealer.rb +22 -0
  36. data/lib/roadforest/rdf/source-rigor/credence.rb +29 -0
  37. data/lib/roadforest/rdf/source-rigor/http-investigator.rb +20 -0
  38. data/lib/roadforest/rdf/source-rigor/investigator.rb +17 -0
  39. data/lib/roadforest/rdf/source-rigor/null-investigator.rb +10 -0
  40. data/lib/roadforest/rdf/source-rigor.rb +44 -0
  41. data/lib/roadforest/rdf/update-focus.rb +73 -0
  42. data/lib/roadforest/rdf/vocabulary.rb +11 -0
  43. data/lib/roadforest/rdf.rb +6 -0
  44. data/lib/roadforest/remote-host.rb +96 -0
  45. data/lib/roadforest/resource/handlers.rb +43 -0
  46. data/lib/roadforest/resource/http/form-parsing.rb +81 -0
  47. data/lib/roadforest/resource/rdf/leaf-item.rb +21 -0
  48. data/lib/roadforest/resource/rdf/list.rb +19 -0
  49. data/lib/roadforest/resource/rdf/parent-item.rb +26 -0
  50. data/lib/roadforest/resource/rdf/read-only.rb +100 -0
  51. data/lib/roadforest/resource/rdf.rb +4 -0
  52. data/lib/roadforest/resource/role/has-children.rb +22 -0
  53. data/lib/roadforest/resource/role/writable.rb +43 -0
  54. data/lib/roadforest/server.rb +3 -0
  55. data/lib/roadforest/test-support/dispatcher-facade.rb +77 -0
  56. data/lib/roadforest/test-support/http-client.rb +151 -0
  57. data/lib/roadforest/test-support/matchers.rb +67 -0
  58. data/lib/roadforest/test-support/remote-host.rb +23 -0
  59. data/lib/roadforest/test-support/trace-formatter.rb +140 -0
  60. data/lib/roadforest/test-support.rb +2 -0
  61. data/lib/roadforest/utility/class-registry.rb +49 -0
  62. data/lib/roadforest.rb +2 -0
  63. data/spec/client.rb +152 -0
  64. data/spec/credence-annealer.rb +44 -0
  65. data/spec/graph-copier.rb +87 -0
  66. data/spec/graph-store.rb +142 -0
  67. data/spec/media-types.rb +14 -0
  68. data/spec/rdf-parcel.rb +158 -0
  69. data/spec/update-focus.rb +117 -0
  70. data/spec_support/gem_test_suite.rb +0 -0
  71. 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