michael-ken 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -5,18 +5,15 @@ h2. Introduction
5
5
  Ken is a Data Layer for Knowledge Representation.
6
6
 
7
7
  It's being built to access the Metaweb Services supplied by Freebase.com.
8
- Just born, the project’s goals are the provision of a concise API for querying and writing.
8
+ The project’s goals are the provision of a concise API for querying and writing.
9
9
  Therefore it wraps the Metaweb Architecture to smart Ruby Objects.
10
10
 
11
11
  You can navigate the Freebase Graph using a rubyish syntax.
12
-
13
- If things go right, you should be able to use this library as a Data Layer (instead of or in addition to
14
- ActiveRecord/DataMapper) for your Web Framework of choice (Merb, Rails).
15
-
12
+ Also you can use this library as a Data Layer (instead of or in addition to ActiveRecord/DataMapper) for your Web Framework of choice (Merb, Rails).
16
13
 
17
14
  h2. Installation
18
15
 
19
- Use GitHub RubyGems.
16
+ Use GitHub RubyGems:
20
17
 
21
18
  <pre>
22
19
  <code>
@@ -25,8 +22,15 @@ Use GitHub RubyGems.
25
22
  </code>
26
23
  </pre>
27
24
 
28
- Or even better, stay on The Edge.
29
-
25
+ In your Ruby files add:
26
+ <pre>
27
+ <code>
28
+ require 'rubygems'
29
+ gem 'michael-ken'
30
+ require 'ken'
31
+ </code>
32
+ </pre>
33
+
30
34
 
31
35
  h2. Getting started
32
36
 
@@ -159,7 +163,7 @@ So asking for values in a nested level does not make sense. Use nested statement
159
163
  lowering the top level result.
160
164
 
161
165
  However you can instead navigate the normal way to figure out that values.
162
- _But won't that require another query to triggered? Doubtful._
166
+ _But won't that require another query to triggered? Certainly._
163
167
 
164
168
  Let's look at a nested query:
165
169
 
@@ -182,32 +186,76 @@ Let's look at a nested query:
182
186
  </code>
183
187
  </pre>
184
188
 
185
- h2. Status
189
+ h3. Access properties attributes directly
190
+
191
+ Ken is primarily designed for inspecting resources in a generic way, what's ideal for domain independent browsing applications.
192
+ However, there are legitimate situations where you already know what you want to access.
193
+
194
+ That's why I now added direct Property/Attribute access, but only on a Type/View level:
195
+
196
+ <pre>
197
+ <code>
198
+ resource = Ken.get('/en/new_order')
199
+ type = resource.types[1] # => #<Type id="/music/artist" name="Musical Artist">
200
+ # because we know _/music/artist_ has a _genre_ property we can access that directly
201
+ type.genre # => #<Property id="/music/artist/genre" expected_type="/music/genre" unique="false" object_type="true">
202
+ </code>
203
+ </pre>
204
+ The same works for views:
205
+ <pre>
206
+ <code>
207
+ resource = Ken.get('/en/new_order')
208
+ view = resource.views[1] # => #<View type="/music/artist">
209
+ # because we know _/music/artist_ has a _genre_ property we can access attribute directly as well
210
+ view.genre # => #<Attribute property="/music/artist/genre">
211
+ </code>
212
+ </pre>
213
+
214
+ If you rather want to query based on Types and access Properties/Attributes directly you can consider using
215
+ Chris "Eppsteins Freebase Library":http://github.com/chriseppstein/freebase/tree/master as an alternative.
216
+
186
217
 
187
- Currently, the focus lies on inspecting Freebase Resources in a generic way. That's why
188
- there is no support for accessing Properties or Attributes directly when you know them already.
189
- That keeps me focussed and prevents me from adding too much sugar.
218
+ h3. Low Level API
190
219
 
191
- Therefore I would say that the first application using Ken will definitely be a Browser Application. :)
220
+ Sometimes you may want to do specific queries instead of inspecting Resources as a whole.
221
+ In such a case you would want to use Ken's low level API.
222
+
223
+ _mqlread_ works like the regular _mqlread service_, except that you are able to pass ruby hashes instead of JSON.
224
+ And you don't have to deal with HTTP, parameter encoding and parsing JSON.
225
+
226
+ <pre>
227
+ <code>
228
+ artists = Ken.session.mqlread([{
229
+ :type => "/music/artist",
230
+ :id => nil,
231
+ :"/common/topic/webpage" => [{:uri => nil}],
232
+ :home_page => [{:uri => nil}],
233
+ :limit => 2
234
+ }])
235
+
236
+ # => [
237
+ {"type"=>"/music/artist", "home_page"=>[{"uri"=>"http://www.massiveattack.co.uk/"}], "id"=>"/en/massive_attack", "/common/topic/webpage"=>[{"uri"=>"http://musicmoz.org/Bands_and_Artists/M/Massive_Attack/"}, {"uri"=>"http://www.discogs.com/artist/Massive+Attack"}, {"uri"=>"http://www.massiveattackarea.com/"}, {"uri"=>"http://www.massiveattack.co.uk/"}, {"uri"=>"http://www.massiveattack.com/"}]},
238
+ {"type"=>"/music/artist", "home_page"=>[{"uri"=>"http://www.apartment26.com/"}], "id"=>"/en/apartment_26", "/common/topic/webpage"=>[{"uri"=>"http://www.discogs.com/artist/Apartment+26"}, {"uri"=>"http://musicmoz.org/Bands_and_Artists/A/Apartment_26/"}, {"uri"=>"http://www.apartment26.com/"}]}
239
+ ]
240
+ </code>
241
+ </pre>
192
242
 
193
- If you rather want to query based on Types and access Properties/Attributes directly you should consider using
194
- Chris "Eppsteins Freebase Library":http://github.com/chriseppstein/freebase/tree/master instead.
243
+ h2. Project Status
195
244
 
196
245
  h3. Features
197
246
 
198
247
  * Fetching of single Resources
199
248
  * Fetching of multiple Resources by specifying a query
249
+ * Accessing Properties/Attributes directly (on a type/view level)
200
250
  * Type inspection
201
- * Attributes inspection
251
+ * Attribute inspection
252
+ * Low Level API (mqlread)
253
+ * Rails and Merb support
202
254
  * Views on Resources to group Attributes based on the Resource's types
203
- * Some specs
204
255
 
205
256
  h3. Roadmap
206
257
 
207
- # Much more specs
208
- # Better Type Support
209
- # API for Set Based Browsing (see http://mqlx.com/~david/parallax/)
210
- # Accessing Properties/Attributes directly (e.g. resource.genres )
258
+ # More tests
211
259
  # Write-Support
212
260
 
213
261
  Initial thoughts, obviously not up-to-date and not conforming to the current version, are available at "http://wiki.github.com/michael/ken":http://wiki.github.com/michael/ken.
data/Rakefile CHANGED
@@ -9,6 +9,7 @@ begin
9
9
  gem.email = "ma[at]zive[dot]at"
10
10
  gem.homepage = "http://github.com/michael/ken"
11
11
  gem.authors = ["michael"]
12
+ gem.add_dependency('extlib')
12
13
  # gem.files = FileList["[A-Z]*.*"]
13
14
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
15
  end
@@ -37,7 +38,6 @@ rescue LoadError
37
38
  end
38
39
  end
39
40
 
40
-
41
41
  task :default => :test
42
42
 
43
43
  require 'rake/rdoctask'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.1.0
data/examples/artist.rb CHANGED
@@ -10,10 +10,10 @@ resource = Ken.get('/en/the_police')
10
10
 
11
11
  resource.views.each do |view|
12
12
  puts view
13
- puts "==============================="
13
+ puts "="*20
14
14
  view.attributes.each do |a|
15
15
  puts a.property
16
- puts "----------------"
16
+ puts "-"*20
17
17
  puts a
18
18
  puts # newline
19
19
  end
@@ -0,0 +1,28 @@
1
+ require 'pathname'
2
+ require 'rubygems'
3
+
4
+ # displays all links related to music/artists
5
+ # low level api through Ken.session.mqlread is used here
6
+
7
+ EXAMPLES_ROOT = Pathname(__FILE__).dirname.expand_path
8
+ require EXAMPLES_ROOT.parent + 'lib/ken'
9
+
10
+ Ken::Session.new('http://www.freebase.com', 'ma', '*****')
11
+
12
+ artists = Ken.session.mqlread([{
13
+ :type => "/music/artist",
14
+ :id => nil,
15
+ :"/common/topic/webpage" => [{:uri => nil}],
16
+ :home_page => [{:uri => nil}],
17
+ :limit => 50
18
+ }])
19
+
20
+ artists.each do |artist|
21
+ artist["/common/topic/webpage"].each do |webpage|
22
+ puts webpage["uri"]
23
+ end
24
+
25
+ artist["home_page"].each do |homepage|
26
+ puts homepage["uri"]
27
+ end
28
+ end
data/ken.gemspec ADDED
@@ -0,0 +1,82 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{ken}
5
+ s.version = "0.1.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["michael"]
9
+ s.date = %q{2009-07-08}
10
+ s.email = %q{ma[at]zive[dot]at}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.textile",
14
+ "README.txt"
15
+ ]
16
+ s.files = [
17
+ ".gitignore",
18
+ "History.txt",
19
+ "LICENSE",
20
+ "README.textile",
21
+ "README.txt",
22
+ "Rakefile",
23
+ "TODO",
24
+ "VERSION",
25
+ "examples/artist.rb",
26
+ "examples/artist_links.rb",
27
+ "ken.gemspec",
28
+ "lib/ken.rb",
29
+ "lib/ken/attribute.rb",
30
+ "lib/ken/collection.rb",
31
+ "lib/ken/logger.rb",
32
+ "lib/ken/property.rb",
33
+ "lib/ken/resource.rb",
34
+ "lib/ken/session.rb",
35
+ "lib/ken/type.rb",
36
+ "lib/ken/util.rb",
37
+ "lib/ken/view.rb",
38
+ "rails/init.rb",
39
+ "tasks/ken.rb",
40
+ "tasks/spec.rb",
41
+ "test/fixtures/music_artist.json",
42
+ "test/fixtures/the_police.json",
43
+ "test/integration/ken_test.rb",
44
+ "test/test_helper.rb",
45
+ "test/unit/attribute_test.rb",
46
+ "test/unit/property_test.rb",
47
+ "test/unit/resource_test.rb",
48
+ "test/unit/session_test.rb",
49
+ "test/unit/type_test.rb",
50
+ "test/unit/view_test.rb"
51
+ ]
52
+ s.homepage = %q{http://github.com/michael/ken}
53
+ s.rdoc_options = ["--charset=UTF-8"]
54
+ s.require_paths = ["lib"]
55
+ s.rubygems_version = %q{1.3.4}
56
+ s.summary = %q{Ruby API for Accessing the Freebase}
57
+ s.test_files = [
58
+ "test/integration/ken_test.rb",
59
+ "test/test_helper.rb",
60
+ "test/unit/attribute_test.rb",
61
+ "test/unit/property_test.rb",
62
+ "test/unit/resource_test.rb",
63
+ "test/unit/session_test.rb",
64
+ "test/unit/type_test.rb",
65
+ "test/unit/view_test.rb",
66
+ "examples/artist.rb",
67
+ "examples/artist_links.rb"
68
+ ]
69
+
70
+ if s.respond_to? :specification_version then
71
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
72
+ s.specification_version = 3
73
+
74
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
75
+ s.add_runtime_dependency(%q<extlib>, [">= 0"])
76
+ else
77
+ s.add_dependency(%q<extlib>, [">= 0"])
78
+ end
79
+ else
80
+ s.add_dependency(%q<extlib>, [">= 0"])
81
+ end
82
+ end
data/lib/ken/attribute.rb CHANGED
@@ -2,7 +2,6 @@ module Ken
2
2
  class Attribute
3
3
 
4
4
  include Extlib::Assertions
5
-
6
5
  attr_reader :property
7
6
 
8
7
  # initializes a resource by json result
@@ -12,6 +11,8 @@ module Ken
12
11
  @data, @property = data, property
13
12
  end
14
13
 
14
+ # factory method for creating an attribute instance
15
+ # @api semipublic
15
16
  def self.create(data, property)
16
17
  Ken::Attribute.new(data, property)
17
18
  end
@@ -26,26 +27,34 @@ module Ken
26
27
  result = "#<Attribute property=\"#{property.id || "nil"}\">"
27
28
  end
28
29
 
29
- # returns just the subject
30
+ # returns a collection of values
31
+ # in case of a unique property the array holds just one value
32
+ # @api public
30
33
  def values
31
34
  subject
32
35
  end
33
36
 
37
+ # unique properties can have at least one value
38
+ # @api public
34
39
  def unique?
35
40
  @property.unique?
36
41
  end
37
42
 
43
+ # object type properties always link to resources
44
+ # @api public
38
45
  def object_type?
39
46
  @property.object_type?
40
47
  end
41
48
 
42
- # is this a good idea? using the values method seems more natural to me
43
- # def method_missing(name, *args, &block)
44
- # subject.send(name, *args, &block)
45
- # end
49
+ # returns true if the property is a value type
50
+ # value type properties refer to simple values like /type/text
51
+ # @api public
52
+ def value_type?
53
+ @property.value_type?
54
+ end
46
55
 
47
- # initializes the subject if used the first time
48
56
  private
57
+ # initializes the subject if used for the first time
49
58
  def subject
50
59
  @subject ||= Ken::Collection.new(@data.map { |r| object_type? ? Ken::Resource.new(r) : r["value"] })
51
60
  end
@@ -1,13 +1,5 @@
1
1
  module Ken
2
2
  class Collection < Array
3
- # def initialize(data = [])
4
- # @data = data
5
- # end
6
-
7
- # def method_missing(name, *args, &blk)
8
- # @data.send name, *args, &blk
9
- # end
10
-
11
3
  # add a linebreak after each entry
12
4
  def to_s
13
5
  self.inject("") { |m,i| "#{m}#{i.to_s}\n"}
data/lib/ken/property.rb CHANGED
@@ -22,11 +22,13 @@ module Ken
22
22
  @data, @type = data, type
23
23
  end
24
24
 
25
+ # property id
25
26
  # @api public
26
27
  def id
27
28
  @data["id"]
28
29
  end
29
30
 
31
+ # property name
30
32
  # @api public
31
33
  def name
32
34
  @data["name"]
@@ -50,11 +52,13 @@ module Ken
50
52
  @type
51
53
  end
52
54
 
55
+ # reverse property, which represent incoming links
53
56
  # @api public
54
57
  def reverse_property
55
58
  @data["reverse_property"]
56
59
  end
57
60
 
61
+ # master property, which represent an outgoing link (or primitive value)
58
62
  # @api public
59
63
  def master_property
60
64
  @data["master_property"]
@@ -78,6 +82,7 @@ module Ken
78
82
  VALUE_TYPES.include?(expected_type)
79
83
  end
80
84
 
85
+ # type, which attribute values of that property are expected to have
81
86
  # @api public
82
87
  def expected_type
83
88
  @data["expected_type"]
data/lib/ken/resource.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  module Ken
2
2
  class Resource
3
-
4
3
  include Extlib::Assertions
5
4
 
6
5
  FETCH_SCHEMA_QUERY = {
@@ -19,6 +18,7 @@ module Ken
19
18
  }
20
19
 
21
20
  FETCH_ATTRIBUTES_QUERY = {
21
+ # :id => id # needs to be merg!d in instance method
22
22
  :"/type/reflect/any_master" => [
23
23
  {
24
24
  :id => nil,
@@ -37,27 +37,27 @@ module Ken
37
37
  {
38
38
  :link => nil,
39
39
  :value => nil
40
+ # TODO: support multiple language
40
41
  # :lang => "/lang/en",
41
42
  # :type => "/type/text"
42
43
  }
43
- ],
44
-
45
- # :id => id # needs to be merg!d in instance method
44
+ ]
46
45
  }
47
46
 
48
-
49
- # initializes a resource by json result
47
+ # initializes a resource using a json result
50
48
  def initialize(data)
51
49
  assert_kind_of 'data', data, Hash
52
50
  # intialize lazy if there is no type supplied
53
51
  @schema_loaded, @attributes_loaded, @data = false, false, data
54
52
  end
55
53
 
54
+ # resource id
56
55
  # @api public
57
56
  def id
58
57
  @data["id"] || ""
59
58
  end
60
59
 
60
+ # resource name
61
61
  # @api public
62
62
  def name
63
63
  @data["name"] || ""
@@ -80,7 +80,7 @@ module Ken
80
80
  @types
81
81
  end
82
82
 
83
- # returns all available vies based on the assigned types
83
+ # returns all available views based on the assigned types
84
84
  # @api public
85
85
  def views
86
86
  @views ||= Ken::Collection.new(types.map { |type| Ken::View.new(self, type) })
@@ -96,7 +96,7 @@ module Ken
96
96
  @properties
97
97
  end
98
98
 
99
- # returns all attributes for every type the resource is an instance from
99
+ # returns all attributes for every type the resource is an instance of
100
100
  # @api public
101
101
  def attributes
102
102
  load_attributes! unless attributes_loaded?
@@ -116,15 +116,18 @@ module Ken
116
116
  end
117
117
 
118
118
  private
119
+ # executes the fetch attributes query in order to load the full set if attributes
120
+ # more info at http://lists.freebase.com/pipermail/developers/2007-December/001022.html
121
+ # @api private
119
122
  def fetch_attributes
120
- # fetching all objects regardless of the type
121
- # check this http://lists.freebase.com/pipermail/developers/2007-December/001022.html
122
123
  Ken.session.mqlread(FETCH_ATTRIBUTES_QUERY.merge!(:id => id))
123
124
  end
124
125
 
126
+ # loads the full set of attributes using reflection
127
+ # information is extracted from master, value and reverse attributes
128
+ # @api private
125
129
  def load_attributes!
126
130
  data = @data["ken:attribute"] || fetch_attributes
127
-
128
131
  # master & value attributes
129
132
  raw_attributes = Ken::Util.convert_hash(data["/type/reflect/any_master"])
130
133
  raw_attributes.merge!(Ken::Util.convert_hash(data["/type/reflect/any_value"]))
@@ -146,18 +149,18 @@ module Ken
146
149
  @attributes_loaded = true
147
150
  end
148
151
 
152
+ # executes the fetch schema query in order to load all schema information
153
+ # @api private
149
154
  def fetch_schema
150
155
  Ken.session.mqlread(FETCH_SCHEMA_QUERY.merge!(:id => id))["ken:type"]
151
156
  end
152
157
 
153
- # loads the resources metainfo
158
+ # loads the resource's metainfo
154
159
  # @api private
155
160
  def load_schema!
156
161
  @data["ken:type"] ||= fetch_schema
157
162
  @types = Ken::Collection.new(@data["ken:type"].map { |type| Ken::Type.new(type) })
158
163
  @schema_loaded = true
159
164
  end
160
-
161
-
162
165
  end # class Resource
163
166
  end # module Ken
data/lib/ken/session.rb CHANGED
@@ -16,10 +16,13 @@ module Ken
16
16
  end
17
17
  end
18
18
 
19
+ class AttributeNotFound < StandardError; end
20
+ class PropertyNotFound < StandardError; end
21
+ class ResourceNotFound < StandardError; end
22
+
19
23
  # partially taken from chris eppstein's freebase api
20
24
  # http://github.com/chriseppstein/freebase/tree
21
25
  class Session
22
- # include Singleton
23
26
 
24
27
  public
25
28
  # Initialize a new Ken Session
@@ -48,7 +51,6 @@ module Ken
48
51
 
49
52
  # get the service url for the specified service.
50
53
  def service_url(svc)
51
- #"http://#{Configuration.instance[:host]}#{SERVICES[svc]}"
52
54
  "#{@host}#{SERVICES[svc]}"
53
55
  end
54
56
 
@@ -67,30 +69,51 @@ module Ken
67
69
  end
68
70
  end # handle_read_error
69
71
 
72
+
70
73
  # perform a mqlread and return the results
71
74
  # TODO: should support multiple queries
72
75
  # you should be able to pass an array of queries
76
+ # Specify :cursor => true to batch the results of a query, sending multiple requests if necessary.
73
77
  def mqlread(query, options = {})
74
78
  Ken.logger.info ">>> Sending Query: #{query.to_json}"
75
-
79
+ cursor = options[:cursor]
80
+ if cursor
81
+ query_result = []
82
+ while cursor
83
+ response = get_query_response(query, cursor)
84
+ query_result += response['result']
85
+ cursor = response['cursor']
86
+ end
87
+ else
88
+ response = get_query_response(query, cursor)
89
+ cursor = response['cursor']
90
+ query_result = response['result']
91
+ end
92
+ query_result
93
+ end
94
+
95
+ protected
96
+ # returns parsed json response from freebase mqlread service
97
+ def get_query_response(query, cursor=nil)
76
98
  envelope = { :qname => {:query => query }}
99
+ envelope[:qname][:cursor] = cursor if cursor
77
100
 
78
101
  response = http_request mqlread_service_url, :queries => envelope.to_json
102
+
79
103
  result = JSON.parse response
104
+
80
105
  inner = result['qname']
81
106
  handle_read_error(inner)
82
-
83
107
  Ken.logger.info "<<< Received Response: #{inner['result'].inspect}"
84
-
85
- # will always return the converted ruby hash (from json)
86
- inner['result']
87
- end # mqlread
88
-
89
- protected
108
+ inner
109
+ end
110
+
111
+ # encode parameters
90
112
  def params_to_string(parameters)
91
113
  parameters.keys.map {|k| "#{URI.encode(k.to_s)}=#{URI.encode(parameters[k])}" }.join('&')
92
114
  end
93
-
115
+
116
+ # does the dirty work
94
117
  def http_request(url, parameters = {})
95
118
  params = params_to_string(parameters)
96
119
  url << '?'+params unless params !~ /\S/
data/lib/ken/type.rb CHANGED
@@ -3,7 +3,7 @@ module Ken
3
3
 
4
4
  include Extlib::Assertions
5
5
 
6
- # initializes a resource by json result
6
+ # initializes a resource using a json result
7
7
  def initialize(data)
8
8
  assert_kind_of 'data', data, Hash
9
9
  @data = data
@@ -12,14 +12,16 @@ module Ken
12
12
  # access property info
13
13
  # @api public
14
14
  def properties
15
- Ken::Collection.new(@data["properties"].map { |property| Ken::Property.new(property, self) })
15
+ @properties ||= Ken::Collection.new(@data["properties"].map { |property| Ken::Property.new(property, self) })
16
16
  end
17
17
 
18
+ # type id
18
19
  # @api public
19
20
  def id
20
21
  @data["id"]
21
22
  end
22
23
 
24
+ # type name
23
25
  # @api public
24
26
  def name
25
27
  @data["name"]
@@ -34,5 +36,18 @@ module Ken
34
36
  def inspect
35
37
  result = "#<Type id=\"#{id}\" name=\"#{name || "nil"}\">"
36
38
  end
39
+
40
+ # delegate to property_get
41
+ def method_missing sym
42
+ property_get(sym.to_s)
43
+ end
44
+
45
+ private
46
+ # @api private
47
+ # search for a property by name and return it
48
+ def property_get(name)
49
+ properties.each { |p| return p if p.id =~ /\/#{name}$/ }
50
+ raise PropertyNotFound
51
+ end
37
52
  end
38
53
  end
data/lib/ken/view.rb CHANGED
@@ -17,6 +17,7 @@ module Ken
17
17
  @type.to_s
18
18
  end
19
19
 
20
+ # return correspondent type
20
21
  # @api public
21
22
  def type
22
23
  @type
@@ -39,5 +40,17 @@ module Ken
39
40
  @resource.properties.select { |p| p.type == @type}
40
41
  end
41
42
 
43
+ # delegate to attribute_get
44
+ def method_missing sym
45
+ attribute_get(sym.to_s)
46
+ end
47
+
48
+ private
49
+ # search for an attribute by name and return it
50
+ # @api private
51
+ def attribute_get(name)
52
+ attributes.each { |a| return a if a.property.id =~ /\/#{name}$/ }
53
+ raise AttributeNotFound
54
+ end
42
55
  end
43
56
  end