roadforest 0.5 → 0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/examples/file-management.rb +70 -58
  3. data/lib/roadforest/application.rb +9 -17
  4. data/lib/roadforest/application/dispatcher.rb +76 -9
  5. data/lib/roadforest/application/parameters.rb +9 -1
  6. data/lib/roadforest/application/path-provider.rb +30 -3
  7. data/lib/roadforest/application/route-adapter.rb +96 -14
  8. data/lib/roadforest/application/services-host.rb +21 -3
  9. data/lib/roadforest/augment/affordance.rb +82 -11
  10. data/lib/roadforest/augment/augmentation.rb +24 -6
  11. data/lib/roadforest/augment/augmenter.rb +12 -3
  12. data/lib/roadforest/authorization.rb +7 -229
  13. data/lib/roadforest/authorization/auth-entity.rb +26 -0
  14. data/lib/roadforest/authorization/authentication-chain.rb +79 -0
  15. data/lib/roadforest/authorization/default-authentication-store.rb +33 -0
  16. data/lib/roadforest/authorization/grant-builder.rb +23 -0
  17. data/lib/roadforest/authorization/grants-holder.rb +58 -0
  18. data/lib/roadforest/authorization/manager.rb +85 -0
  19. data/lib/roadforest/authorization/policy.rb +19 -0
  20. data/lib/roadforest/graph/access-manager.rb +25 -2
  21. data/lib/roadforest/graph/focus-list.rb +4 -0
  22. data/lib/roadforest/graph/graph-focus.rb +30 -13
  23. data/lib/roadforest/graph/nav-affordance-builder.rb +62 -0
  24. data/lib/roadforest/graph/normalization.rb +3 -3
  25. data/lib/roadforest/graph/path-vocabulary.rb +64 -0
  26. data/lib/roadforest/graph/post-focus.rb +5 -0
  27. data/lib/roadforest/graph/vocabulary.rb +4 -1
  28. data/lib/roadforest/http/adapters/excon.rb +4 -0
  29. data/lib/roadforest/http/graph-transfer.rb +17 -1
  30. data/lib/roadforest/http/keychain.rb +121 -33
  31. data/lib/roadforest/http/user-agent.rb +5 -3
  32. data/lib/roadforest/interface/application.rb +25 -8
  33. data/lib/roadforest/interface/rdf.rb +114 -15
  34. data/lib/roadforest/interface/utility.rb +3 -0
  35. data/lib/roadforest/interface/utility/backfill.rb +63 -0
  36. data/lib/roadforest/interface/utility/grant-list.rb +45 -0
  37. data/lib/roadforest/interface/utility/grant.rb +22 -0
  38. data/lib/roadforest/interfaces.rb +1 -0
  39. data/lib/roadforest/path-matcher.rb +471 -0
  40. data/lib/roadforest/remote-host.rb +159 -35
  41. data/lib/roadforest/resource/read-only.rb +23 -4
  42. data/lib/roadforest/server.rb +32 -3
  43. data/lib/roadforest/source-rigor/graph-store.rb +0 -2
  44. data/lib/roadforest/source-rigor/rigorous-access.rb +138 -21
  45. data/lib/roadforest/templates/affordance-property-values.haml +3 -0
  46. data/lib/roadforest/templates/rdfpost-curie.haml +1 -1
  47. data/lib/roadforest/test-support/matchers.rb +41 -12
  48. data/lib/roadforest/test-support/remote-host.rb +3 -3
  49. data/lib/roadforest/type-handlers/rdfa-writer/environment-decorator.rb +1 -1
  50. data/lib/roadforest/type-handlers/rdfa-writer/render-engine.rb +40 -27
  51. data/lib/roadforest/type-handlers/rdfa.rb +10 -3
  52. data/lib/roadforest/utility/class-registry.rb +44 -4
  53. data/spec/affordance-augmenter.rb +46 -19
  54. data/spec/affordances-flow.rb +46 -30
  55. data/spec/authorization.rb +16 -4
  56. data/spec/client.rb +22 -4
  57. data/spec/focus-list.rb +24 -0
  58. data/spec/full-integration.rb +8 -3
  59. data/spec/graph-store.rb +8 -0
  60. data/spec/keychain.rb +18 -14
  61. data/spec/rdf-normalization.rb +32 -6
  62. data/spec/update-focus.rb +36 -39
  63. metadata +19 -5
@@ -0,0 +1,62 @@
1
+ require 'roadforest/graph'
2
+
3
+ module RoadForest
4
+ module Graph
5
+ class NavAffordanceBuilder
6
+ def initialize(focus, path_provider)
7
+ @focus, @path_provider = focus, path_provider
8
+ end
9
+ attr_reader :path_provider, :focus
10
+
11
+ def to(name, params=nil)
12
+ route = path_provider.route_for_name(name)
13
+
14
+ pattern = iri_template(route, params)
15
+
16
+ node = ::RDF::Node.new
17
+ tmpl = ::RDF::Node.new
18
+ focus << [ node, ::RDF.type, Af.Navigate ]
19
+ focus << [ node, Af.target, tmpl ]
20
+ focus << [ tmpl, Af.pattern, pattern ]
21
+ end
22
+
23
+ def iri_template(route, params)
24
+ klass = route.interface_class
25
+ return if klass.nil?
26
+
27
+ variables = klass.path_params
28
+
29
+ params ||= {}
30
+ params = params.dup
31
+
32
+ variables -= params.keys
33
+
34
+ path_spec = route.resolve_path_spec(params)
35
+
36
+ path = path_provider.services.canonical_host.to_s.sub(%r{/$}, '') +
37
+ path_spec.map do |segment|
38
+ case segment
39
+ when Symbol
40
+ variables.delete(segment)
41
+ "{/#{segment}}"
42
+ when '*'
43
+ "{/extra*}"
44
+ else
45
+ "/" + segment.to_s
46
+ end
47
+ end.join("")
48
+
49
+ unless params.empty?
50
+ path += "?" + params.map do |key, value|
51
+ [key, value].join("=")
52
+ end.join("&")
53
+ end
54
+
55
+ unless variables.empty?
56
+ path += "{?#{variables.join(",")}}"
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ end
@@ -6,7 +6,7 @@ module RoadForest::Graph
6
6
  Vocabs = {}
7
7
  Vocabs["rdf"] = RDF
8
8
 
9
- def normalize_statement(subject, predicate, object, context)
9
+ def normalize_statement(subject, predicate, object, context=nil)
10
10
  subject = normalize_resource(subject) || RDF::Node.new
11
11
  predicate = normalize_uri(predicate)
12
12
  object = normalize_term(object) || RDF::Node.new
@@ -43,7 +43,7 @@ module RoadForest::Graph
43
43
  def normalize_context(from)
44
44
  case from
45
45
  when Array
46
- from = expand_curie(from)
46
+ from = uri(expand_curie(from))
47
47
  when RDF::URI, Addressable::URI, String
48
48
  from = uri(from)
49
49
  else
@@ -107,7 +107,7 @@ module RoadForest::Graph
107
107
 
108
108
  def expand_curie_pair(prefix, property)
109
109
  vocab = Vocabs.fetch(prefix) do
110
- vocab = RDF::Vocabulary.find do |vocab|
110
+ vocab = RDF::Vocabulary.detect do |vocab|
111
111
  unless vocab.__prefix__.is_a? RDF::URI
112
112
  Vocabs[vocab.__prefix__.to_s] = vocab
113
113
  end
@@ -0,0 +1,64 @@
1
+ # This file generated automatically using vocab-fetch from path.ttl
2
+ require 'rdf'
3
+ module RDF
4
+ class RoadForest::Graph::Path < StrictVocabulary("http://judsonlester.info/rdf-vocab/path#")
5
+
6
+ # Class definitions
7
+ property :Literal
8
+ property :MultipleStep, :comment =>
9
+ %(A step that may appear more than once on a subject)
10
+ property :RepeatingStep, :comment =>
11
+ %(A step that repeats - the subject of a matching step become
12
+ the subject of a new match)
13
+ property :Root, :comment =>
14
+ %(The starting point of a path pattern)
15
+ property :Step, :comment =>
16
+ %(Base class for path steps)
17
+ property :Target, :comment =>
18
+ %(A target for the pattern)
19
+
20
+ # Property definitions
21
+ property :after, :comment =>
22
+ %(Constrains the matching of this literal to values that come
23
+ after the subject value in some order.)
24
+ property :before, :comment =>
25
+ %(Constrains the matching of this literal to values that come
26
+ before the subject value in some order.)
27
+ property :constraint, :comment =>
28
+ %(Description of constraints on values that can match this
29
+ Literal.)
30
+ property :defaultValue
31
+ property :forward, :comment =>
32
+ %(Indicates that the subject of the matched statement matches
33
+ the subject, and the predicate of the matched statement
34
+ matches the predicate.)
35
+ property :is, :comment =>
36
+ %(The target of this step has exactly this value.)
37
+ property :label
38
+ property :maxMulti, :comment =>
39
+ %(Limits the number of times a repeating step may repeat -
40
+ otherwise a repeating step is assumed to have no limit to
41
+ repetitions.)
42
+ property :maxRepeat, :comment =>
43
+ %(Limits the number of times a repeating step may repeat -
44
+ otherwise a repeating step is assumed to have no limit to
45
+ repetitions.)
46
+ property :minMulti, :comment =>
47
+ %(Limits the number of times a repeating step may repeat -
48
+ otherwise a repeating step is assumed allow 0 repetions.)
49
+ property :minRepeat, :comment =>
50
+ %(Limits the number of times a repeating step may repeat -
51
+ otherwise a repeating step is assumed allow 0 repetions.)
52
+ property :name
53
+ property :order, :comment =>
54
+ %(The subject resource describes the order in which to consider
55
+ values of this Literal.)
56
+ property :predicate, :comment =>
57
+ %(The property of a statement that matches this step)
58
+ property :reverse, :comment =>
59
+ %(Indicates that the subject of the matched statement matches
60
+ the predicate, and the predicate of the matched statement
61
+ matches the subject.)
62
+ property :type
63
+ end
64
+ end
@@ -5,6 +5,11 @@ module RoadForest::Graph
5
5
 
6
6
  attr_accessor :graphs
7
7
 
8
+ def initialize(access_manager, subject = nil)
9
+ super
10
+ @graphs = {}
11
+ end
12
+
8
13
  def dup
9
14
  other = super
10
15
  other.graphs = graphs
@@ -1,6 +1,8 @@
1
1
  require 'roadforest/graph'
2
2
  require 'rdf'
3
3
 
4
+ require 'roadforest/graph/path-vocabulary'
5
+
4
6
  module RoadForest::Graph
5
7
  module Vocabulary
6
8
  class RF < ::RDF::Vocabulary("http://lrdesign.com/graph/roadforest#")
@@ -10,7 +12,7 @@ module RoadForest::Graph
10
12
  end
11
13
  end
12
14
 
13
- class Af < ::RDF::StrictVocabulary("http://judsonlester.info/affordance#")
15
+ class Af < ::RDF::StrictVocabulary("http://judsonlester.info/rdf-vocab/affordance#")
14
16
 
15
17
  # Class definitions
16
18
  property :Affordance, :comment =>
@@ -57,6 +59,7 @@ module RoadForest::Graph
57
59
  the tokens be defreferenceable, and that they be accessible
58
60
  iff the user is authorized to activate the affordance in
59
61
  question.)
62
+ property :grants
60
63
  property :controlName, :comment =>
61
64
  %(Valid values are limited per application. Examples include
62
65
  'Media-Type', 'Encoding' or 'EntityTag')
@@ -15,6 +15,10 @@ module RoadForest
15
15
  Excon.new(site.to_s, @connection_defaults)
16
16
  end
17
17
 
18
+ def reset_connections
19
+ @connections.clear
20
+ end
21
+
18
22
  def site_connection(uri)
19
23
  uri = Addressable::URI.parse(uri)
20
24
  @connections[uri.normalized_site]
@@ -5,9 +5,10 @@ module RoadForest
5
5
  module HTTP
6
6
  class GraphTransfer
7
7
  attr_writer :type_handling
8
- attr_accessor :user_agent
8
+ attr_accessor :user_agent, :trace
9
9
 
10
10
  def initialize(user_agent)
11
+ @trace = false
11
12
  @user_agent = user_agent
12
13
  @type_preferences = Hash.new{|h,k| k.nil? ? "*/*" : h[nil]}
13
14
  end
@@ -28,6 +29,8 @@ module RoadForest
28
29
  headers = {"Accept" => type_handling.parsers.types.accept_header}
29
30
  body = nil
30
31
 
32
+ trace_graph("OUT", graph)
33
+
31
34
  if(%w{POST PUT PATCH}.include? method.upcase)
32
35
  content_type = best_type_for(url)
33
36
  renderer = type_handling.choose_renderer(content_type)
@@ -49,6 +52,17 @@ module RoadForest
49
52
  retry
50
53
  end
51
54
 
55
+ def trace_graph(tag, graph)
56
+ return unless @trace
57
+ require 'rdf/turtle'
58
+ @trace = $stderr unless @trace.respond_to?(:puts)
59
+ @trace.puts "<#{tag}"
60
+ if graph.respond_to?(:dump)
61
+ @trace.puts graph.dump(:ntriples, :standard_prefixes => true, :prefixes => { "af" => "http://judsonlester.info/affordance#"})
62
+ end
63
+ @trace.puts "#{tag}>"
64
+ end
65
+
52
66
  def type_handling
53
67
  @type_handling || ContentHandling.rdf_engine
54
68
  end
@@ -67,6 +81,8 @@ module RoadForest
67
81
  parser = type_handling.choose_parser(response.headers["Content-Type"])
68
82
  graph = parser.to_graph(url, response.body_string)
69
83
 
84
+ trace_graph("IN", graph)
85
+
70
86
  response = GraphResponse.new(url, response)
71
87
  response.graph = graph
72
88
  return response
@@ -3,76 +3,164 @@ require 'addressable/uri'
3
3
 
4
4
  module RoadForest
5
5
  module HTTP
6
- #Manages user credentials for HTTP Basic auth
7
- class Keychain
8
- class Credentials < Struct.new(:user, :secret)
9
- def header_value
10
- "Basic #{Base64.strict_encode64("#{user}:#{secret}")}"
11
- end
6
+ class BasicCredentials < Struct.new(:user, :secret)
7
+ def header_value
8
+ "Basic #{Base64.strict_encode64("#{user}:#{secret}")}"
9
+ end
10
+ end
11
+ YAML.add_tag("!basic", BasicCredentials)
12
+
13
+ class CredentialSource
14
+ def canonical_root(url)
15
+ url = Addressable::URI.parse(url)
16
+ url.path = "/"
17
+ url.to_s
18
+ end
19
+
20
+ # @returns {BasicCredentials}
21
+ # @returns nil to indicate no further credentials available
22
+ def respond_to_challenge(url, realm, attempt)
23
+ nil
24
+ end
25
+ end
26
+
27
+ class PreparedCredentialSource < CredentialSource
28
+ def initialize
29
+ @for_url = Hash.new{|h,k| h[k] = []}
30
+ end
31
+
32
+ def add(url, user, secret)
33
+ creds = BasicCredentials.new(user, secret)
34
+ add_credentials(url, creds)
12
35
  end
13
36
 
37
+ def add_credentials(url, creds)
38
+ @for_url[canonical_root(url)] << creds
39
+ end
40
+
41
+ def respond_to_challenge(url, realm, attempt)
42
+ @for_url[canonical_root(url)].fetch(attempt)
43
+ rescue IndexError
44
+ nil
45
+ end
46
+ end
47
+
48
+ #Manages user credentials for HTTP Basic auth
49
+ class Keychain
50
+ ATTEMPT_LIMIT = 5
14
51
  def initialize
15
52
  @realm_for_url = {}
16
53
  @with_realm = {}
54
+ @sources = []
55
+ @source_enums = Hash.new{|h,k| @sources.each}
56
+ @attempt_enums = Hash.new{|h,k| (0...ATTEMPT_LIMIT).each}
17
57
  end
18
58
 
19
- def add(url, user, secret, realm=nil)
20
- creds = Credentials.new(user, secret)
21
- add_credentials(url, creds, realm || :default)
59
+ def add_source(source)
60
+ @sources << source
61
+ @source_enums.clear
62
+ @attempt_enums.clear
22
63
  end
23
64
 
24
- def add_credentials(url, creds, realm)
25
- if url.to_s[-1] != "/"
26
- url << "/"
27
- end
28
- @realm_for_url[url.to_s] = realm
29
-
65
+ def canonical_root(url)
30
66
  url = Addressable::URI.parse(url)
31
67
  url.path = "/"
32
- @with_realm[[url.to_s,realm]] = creds
68
+ url.fragment = nil
69
+ url.query = nil
70
+ url.to_s
71
+ end
72
+
73
+ def stripped_url(url)
74
+ url = Addressable::URI.parse(url)
75
+ url.fragment = nil
76
+ url.query = nil
77
+ url
33
78
  end
34
79
 
35
80
  BASIC_SCHEME = /basic\s+realm=(?<q>['"])(?<realm>(?:(?!['"]).)*)\k<q>/i
36
81
 
37
82
  def challenge_response(url, challenge)
83
+ url = stripped_url(url).to_s
84
+ #Future note: the RFC means that the creds selection mechanics are
85
+ #valid for all HTTP WWW-Authenticate reponses
38
86
  if (match = BASIC_SCHEME.match(challenge)).nil?
39
87
  return nil
40
88
  end
41
89
  realm = match[:realm]
90
+ @realm_for_url[url] = realm
42
91
 
43
- response(url, realm)
92
+ cached_response(canonical_root(url), realm) || missing_credentials(url, realm)
44
93
  end
45
94
 
46
- def response(url, realm)
47
- lookup_url = Addressable::URI.parse(url)
48
- lookup_url.path = "/"
49
- creds = @with_realm[[lookup_url.to_s,realm]]
50
- if creds.nil? and not realm.nil?
51
- creds = missing_credentials(url, realm)
52
- unless creds.nil?
53
- add_credentials(url, creds, realm)
54
- end
55
- end
95
+ def preemptive_response(url)
96
+ realm = realm_for_url(url)
97
+ url = canonical_root(url)
98
+ return cached_response(url, realm)
99
+ end
56
100
 
101
+ def cached_response(url, realm)
102
+ creds = credentials(url, realm)
57
103
  return nil if creds.nil?
58
-
59
104
  return creds.header_value
60
105
  end
61
106
 
107
+ def credentials_for(url)
108
+ realm = realm_for_url(url)
109
+ url = canonical_root(url)
110
+ credentials(url, realm)
111
+ end
112
+
113
+ def credentials(url, realm = nil)
114
+ @with_realm[[url, realm]]
115
+ end
116
+
62
117
  def missing_credentials(url, realm)
118
+ loop do
119
+ attempt = next_attempt(url, realm)
120
+ creds = current_source(url, realm).respond_to_challenge(url, realm, attempt)
121
+ if creds.nil?
122
+ next_source(url, realm)
123
+ else
124
+ @with_realm[[canonical_root(url), realm]] = creds
125
+ return creds.header_value
126
+ end
127
+ end
128
+ return nil
129
+ rescue StopIteration
63
130
  nil
64
131
  end
65
132
 
66
- def preemptive_response(url)
67
- url = Addressable::URI.parse(url)
133
+ def forget(url, realm)
134
+ @with_realm.delete([url, realm])
135
+ end
136
+
137
+ def current_source(url, realm)
138
+ @source_enums[[url, realm]].peek
139
+ end
140
+
141
+ def next_source(url, realm)
142
+ @attempt_enums.delete([url, realm])
143
+ @source_enums[[url, realm]].next
144
+ rescue StopIteration
145
+ @source_enums.delete([url, realm])
146
+ raise
147
+ end
68
148
 
149
+ def next_attempt(url, realm)
150
+ @attempt_enums[[url, realm]].next
151
+ rescue StopIteration
152
+ next_source(url, realm)
153
+ retry
154
+ end
155
+
156
+ def realm_for_url(url)
157
+ url = stripped_url(url)
69
158
  while (realm = @realm_for_url[url.to_s]).nil?
70
159
  new_url = url.join("..")
71
- break if new_url == url
160
+ return realm if new_url == url
72
161
  url = new_url
73
162
  end
74
-
75
- return response(url, realm)
163
+ return realm || :default
76
164
  end
77
165
  end
78
166
  end