safrano 0.2.0 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6894a03a0224ba8beedc0a05fba7700ad4e06b34d79858478c5e4fe998ed8eef
4
- data.tar.gz: '059f1dc5d7d3c4537f279be2b170b5c67b2c61fdbe27199f511e8f1e894776ad'
3
+ metadata.gz: ffa3b9e1f282380dcdd32162bca490faa4749e05a1db11d65dc2b0c25c5ee76c
4
+ data.tar.gz: eae063a3541b8eec09bea8d98adf04dc4ad6bed16bdfe6dc3c06fd2df7bb0baf
5
5
  SHA512:
6
- metadata.gz: d41f73aa8bdd9084eaa150ee06d6c9b05e91ff18dd24f25daae75f25572cf0500c0c5669ade95275b65891655fae4e0e0c53fbb2712e414fe8daf6df91efb8cb
7
- data.tar.gz: 11e49d68d0fc005be501f069ab8fcbaad40050449435f4369938b10f1cb90c71cc86b361b4bbaa7541ac7e274ec34de1f6925a1a63d481bcd5bb5690bfb84649
6
+ metadata.gz: 0c910a0f9677d820f85ae00926b3e7f4f5b29126aa86f5386868d8681e3b9808d44d3f31a6adddac861d98ed1d6969a955691c75262c7a2f892110a4dc948797
7
+ data.tar.gz: 54b66c00dc7456739457cc58d7a1a68aff8f9a35dccfda3f04b7b83ce12ac7a7e9525d95b7a620cb0e779467e67b26ae3342675477e5c7a49fdb8063bad12183
data/lib/multipart.rb CHANGED
@@ -24,50 +24,64 @@ module MIME
24
24
  @lines << line
25
25
  end
26
26
 
27
- def parse(level: 0)
28
- @level = level
29
- return unless @lines
27
+ def parse_first_part(line)
28
+ if @target.parser.next_part(line)
29
+ @state = :next_part
30
+ # this is for when there is only one part
31
+ # (first part is the last one)
32
+ elsif @target.parser.last_part(line)
33
+ @state = :end
34
+ else
35
+ @target.parser.addline(line)
36
+ end
37
+ end
30
38
 
31
- @lines.each do |line|
32
- case @state
33
- when :h
34
- if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
35
- @target_hd[hmd[1].downcase] = hmd[2].strip
39
+ def parse_next_part(line)
40
+ if @target.parser.next_part(line)
36
41
 
37
- elsif /^#{CRLF}$/ =~ line
38
- @target_ct = @target_hd['content-type'] || 'text/plain'
39
- @state = new_content
42
+ elsif @target.parser.last_part(line)
43
+ @state = :end
44
+ else
45
+ @target.parser.addline(line)
46
+ end
47
+ end
40
48
 
41
- end
49
+ def parse_head(line)
50
+ if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
51
+ @target_hd[hmd[1].downcase] = hmd[2].strip
42
52
 
43
- when :b
44
- @target.parser.addline(line)
53
+ elsif /^#{CRLF}$/ =~ line
54
+ @target_ct = @target_hd['content-type'] || 'text/plain'
55
+ @state = new_content
45
56
 
46
- when :bmp
57
+ end
58
+ end
47
59
 
48
- @state = :first_part if @target.parser.first_part(line)
60
+ def parse_line(line)
61
+ case @state
62
+ when :h
63
+ parse_head(line)
49
64
 
50
- when :first_part
51
- if @target.parser.next_part(line)
52
- @state = :next_part
53
- # this is for when there is only one part
54
- # (first part is the last one)
55
- elsif @target.parser.last_part(line)
56
- @state = :end
57
- else
58
- @target.parser.addline(line)
59
- end
65
+ when :b
66
+ @target.parser.addline(line)
60
67
 
61
- when :next_part
68
+ when :bmp
69
+ @state = :first_part if @target.parser.first_part(line)
62
70
 
63
- if @target.parser.next_part(line)
71
+ when :first_part
72
+ parse_first_part(line)
64
73
 
65
- elsif @target.parser.last_part(line)
66
- @state = :end
67
- else
68
- @target.parser.addline(line)
69
- end
70
- end
74
+ when :next_part
75
+ parse_next_part(line)
76
+ end
77
+ end
78
+
79
+ def parse(level: 0)
80
+ @level = level
81
+ return unless @lines
82
+
83
+ @lines.each do |line|
84
+ parse_line(line)
71
85
  end
72
86
  # Warning: recursive here
73
87
  @target.parser.parse(level: level)
@@ -183,6 +197,17 @@ module MIME
183
197
  @lines << line
184
198
  end
185
199
 
200
+ def parse_head(line)
201
+ if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
202
+ @target.hd[hmd[1].downcase] = hmd[2].strip
203
+ elsif /^#{CRLF}$/ =~ line
204
+ @state = :b
205
+ else
206
+ @target.content << line
207
+ @state = :b
208
+ end
209
+ end
210
+
186
211
  def parse(level: 0)
187
212
  return unless @lines
188
213
 
@@ -190,14 +215,7 @@ module MIME
190
215
  @lines.each do |line|
191
216
  case @state
192
217
  when :h
193
- if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
194
- @target.hd[hmd[1].downcase] = hmd[2].strip
195
- elsif /^#{CRLF}$/ =~ line
196
- @state = :b
197
- else
198
- @target.content << line
199
- @state = :b
200
- end
218
+ parse_head(line)
201
219
  when :b
202
220
  @target.content << line
203
221
  end
@@ -361,9 +379,7 @@ module MIME
361
379
  end
362
380
  end
363
381
  else
364
- @response.content = @content.map { |part|
365
- part.get_response(batcha)
366
- }
382
+ @response.content = @content.map { |prt| prt.get_response(batcha) }
367
383
  end
368
384
  @response
369
385
  end
@@ -448,7 +464,8 @@ module MIME
448
464
 
449
465
  # For application/http . Content is either a Request or a Response
450
466
  class Http < Media
451
- HTTP_R_RGX = %r{^(POST|GET|PUT|MERGE|PATCH)\s+(\S*)\s?(HTTP/1\.1)\s*$}.freeze
467
+ HTTP_MTHS_RGX = 'POST|GET|PUT|MERGE|PATCH'.freeze
468
+ HTTP_R_RGX = %r{^(#{HTTP_MTHS_RGX})\s+(\S*)\s?(HTTP/1\.1)\s*$}.freeze
452
469
  HEADER_RGX = %r{^([a-zA-Z\-]+):\s*([0-9a-zA-Z\-\/,\s]+;?\S*)\s*$}.freeze
453
470
  HTTP_RESP_RGX = %r{^HTTP/1\.1\s(\d+)\s}.freeze
454
471
 
@@ -465,34 +482,42 @@ module MIME
465
482
  @lines << line
466
483
  end
467
484
 
485
+ def parse_http(line)
486
+ if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
487
+ @target.hd[hmd[1].downcase] = hmd[2].strip
488
+ elsif (mdht = HTTP_R_RGX.match(line))
489
+ @state = :hd
490
+ @target.content = MIME::Content::Application::HttpReq.new
491
+ @target.content.http_method = mdht[1]
492
+ @target.content.uri = mdht[2]
493
+ # HTTP 1.1 status line --> HttpResp.new
494
+ elsif (mdht = HTTP_RESP_RGX.match(line))
495
+ @state = :hd
496
+ @target.content = MIME::Content::Application::HttpResp.new
497
+ @target.content.status = mdht[1]
498
+ end
499
+ end
500
+
501
+ def parse_head(line)
502
+ if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
503
+ @target.content.hd[hmd[1].downcase] = hmd[2].strip
504
+ elsif /^#{CRLF}$/ =~ line
505
+ @state = :b
506
+ else
507
+ @body_lines << line
508
+ @state = :b
509
+ end
510
+ end
511
+
468
512
  def parse(level: 0)
469
513
  return unless @lines
470
514
 
471
515
  @lines.each do |line|
472
516
  case @state
473
517
  when :http
474
- if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
475
- @target.hd[hmd[1].downcase] = hmd[2].strip
476
- elsif (mdht = HTTP_R_RGX.match(line))
477
- @state = :hd
478
- @target.content = MIME::Content::Application::HttpReq.new
479
- @target.content.http_method = mdht[1]
480
- @target.content.uri = mdht[2]
481
- # HTTP 1.1 status line --> HttpResp.new
482
- elsif (mdht = HTTP_RESP_RGX.match(line))
483
- @state = :hd
484
- @target.content = MIME::Content::Application::HttpResp.new
485
- @target.content.status = mdht[1]
486
- end
518
+ parse_http(line)
487
519
  when :hd
488
- if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
489
- @target.content.hd[hmd[1].downcase] = hmd[2].strip
490
- elsif /^#{CRLF}$/ =~ line
491
- @state = :b
492
- else
493
- @body_lines << line
494
- @state = :b
495
- end
520
+ parse_head(line)
496
521
  when :b
497
522
  @body_lines << line
498
523
  end
@@ -538,27 +563,3 @@ module MIME
538
563
  end
539
564
  end
540
565
 
541
- # @mimep = MIME::Media::Parser.new
542
-
543
- # @inpstr = File.open('../test/multipart/odata_changeset_1_body.txt','r')
544
- # @boundary = 'batch_48f8-3aea-2f04'
545
-
546
- # @inpstr = File.open('../test/multipart/odata_changeset_body.txt','r')
547
- # @boundary = 'batch_36522ad7-fc75-4b56-8c71-56071383e77b'
548
-
549
- # @mime = @mimep.hook_multipart('multipart/mixed', @boundary)
550
- # @mime = @mimep.parse_str(@inpstr)
551
-
552
- # require 'pry'
553
- # binding.pry
554
-
555
- # @inpstr.close
556
-
557
- # inp = File.open(ARGV[0], 'r') do |f|
558
- # f.readlines(CRLF)
559
- # end
560
- # x = MIME::Media::Parser.new
561
- # y = x.parsein(inp)
562
- # pp y
563
- # puts '-----------------------------------------------'
564
- # puts y.unparse
@@ -13,16 +13,25 @@ module OData
13
13
  end
14
14
 
15
15
  def value
16
- @entity.values[@name.to_sym]
16
+ # WARNING; this code is duplicated in entity.rb
17
+ # (and the inverted transformation is in test/client.rb)
18
+ # will require a more systematic solution some day
19
+ # WARNING 2... this require more work to handle the timezones topci
20
+ # currently it is just set to make some minimal testcase work
21
+ case (v = @entity.values[@name.to_sym])
22
+ when Time
23
+ # try to get back the database time zone and value
24
+ (v + v.gmt_offset).utc.to_datetime
25
+ else
26
+ v
27
+ end
17
28
  end
18
29
 
19
30
  def odata_get(req)
20
31
  if req.walker.raw_value
21
- [200, { 'Content-Type' => 'text/plain;charset=utf-8' },
22
- value.to_s]
23
- elsif req.accept?('application/json')
24
- [200, { 'Content-Type' => 'application/json;charset=utf-8' },
25
- to_odata_json(service: req.service)]
32
+ [200, CT_TEXT, value.to_s]
33
+ elsif req.accept?(APPJSON)
34
+ [200, CT_JSON, to_odata_json(service: req.service)]
26
35
  else # TODO: other formats
27
36
  406
28
37
  end
@@ -8,6 +8,7 @@ require 'safrano_core.rb'
8
8
  require 'odata/error.rb'
9
9
  require 'odata/collection_filter.rb'
10
10
  require 'odata/collection_order.rb'
11
+ require 'odata/url_parameters.rb'
11
12
 
12
13
  # small helper method
13
14
  # http://stackoverflow.com/
@@ -40,6 +41,8 @@ module OData
40
41
  # class methods. They Make heavy use of Sequel::Model functionality
41
42
  # we will add this to our Model classes with "extend" --> self is the Class
42
43
  module EntityClassBase
44
+ SINGLE_PK_URL_REGEXP = /\A\('?([\w\s]+)'?\)(.*)/.freeze
45
+
43
46
  attr_reader :nav_collection_url_regexp
44
47
  attr_reader :nav_entity_url_regexp
45
48
  attr_reader :entity_id_url_regexp
@@ -60,6 +63,10 @@ module OData
60
63
  # url params
61
64
  attr_reader :params
62
65
 
66
+ # url parameters processing object (mostly covert to sequel exprs).
67
+ # exposed for testing only
68
+ attr_reader :uparms
69
+
63
70
  # initialising block of code to be executed at end of
64
71
  # ServerApp.publish_service after all model classes have been registered
65
72
  # (without the associations/relationships)
@@ -94,42 +101,6 @@ module OData
94
101
  enty
95
102
  end
96
103
 
97
- def odata_get_apply_filter_w_sequel
98
- return unless @params['$filter']
99
-
100
- # Sequel requires a dataset to build some sql expressions, so we
101
- # need to pass one todo: use a dummy one ?
102
- # @fi = Filter.new_by_parse(@params['$filter'], @cx)
103
- return if @fi.parse_error?
104
-
105
- @cx = @fi.apply_to_dataset(@cx)
106
- @right_assocs.merge @fi.assocs
107
- end
108
-
109
- def odata_get_apply_order_w_sequel
110
- return unless @params['$orderby']
111
-
112
- fo = Order.new_by_parse(@params['$orderby'], @cx)
113
- @cx = fo.apply_to_dataset(@cx)
114
- @left_assocs.merge fo.assocs
115
- end
116
-
117
- def odata_get_do_assoc_joins_w_sequel
118
- return if @right_assocs.empty? && @left_assocs.empty?
119
-
120
- # Preparation: ensure that the left and right assocs sets are disjoint
121
- # by keeping the duplicates in the left assoc set and removing
122
- # them from the right assocs set. We can use Set difference...
123
- @right_assocs -= @left_assocs
124
-
125
- # this is for the filtering. Normally we can use inner join
126
- @right_assocs.each { |aj| @cx = @cx.association_join(aj) }
127
- # this is for the ordering.
128
- # we need left join otherwise we could miss records sometimes
129
- @left_assocs.each { |aj| @cx = @cx.association_left_join(aj) }
130
- @cx = @cx.select_all(entity_set_name.to_sym)
131
- end
132
-
133
104
  def odata_get_inlinecount_w_sequel
134
105
  return unless (icp = @params['$inlinecount'])
135
106
 
@@ -142,25 +113,29 @@ module OData
142
113
  end
143
114
  end
144
115
 
145
- def odata_get_apply_params_w_sequel
146
- @left_assocs = Set.new
147
- @right_assocs = Set.new
148
- odata_get_apply_filter_w_sequel
149
- odata_get_apply_order_w_sequel
150
- odata_get_do_assoc_joins_w_sequel
151
- odata_get_inlinecount_w_sequel
152
-
153
- @cx = @cx.offset(@params['$skip']) if @params['$skip']
154
- @cx = @cx.limit(@params['$top']) if @params['$top']
155
- @cx
156
- end
157
-
158
116
  def navigated_coll
159
117
  false
160
118
  end
161
119
 
120
+ def attrib_path_valid?(path)
121
+ @attribute_path_list.include? path
122
+ end
123
+
162
124
  def odata_get_apply_params
163
- odata_get_apply_params_w_sequel
125
+ begin
126
+ @cx = @uparms.apply_to_dataset(@cx)
127
+ rescue OData::Filter::Parser::ErrorWrongColumnName
128
+ @error = BadRequestFilterParseError
129
+ return
130
+ rescue OData::Filter::Parser::ErrorFunctionArgumentType
131
+ @error = BadRequestFilterParseError
132
+ return
133
+ end
134
+ odata_get_inlinecount_w_sequel
135
+
136
+ @cx = @cx.offset(@params['$skip']) if @params['$skip']
137
+ @cx = @cx.limit(@params['$top']) if @params['$top']
138
+ @cx
164
139
  end
165
140
 
166
141
  # url params validation methods.
@@ -192,33 +167,11 @@ module OData
192
167
  end
193
168
 
194
169
  def check_u_p_filter
195
- return unless @params['$filter']
196
-
197
- # Sequel requires a dataset to build some sql expressions, so we
198
- # need to pass one todo: use a dummy one ?
199
- @fi = Filter.new_by_parse(@params['$filter'], @cx)
200
- return BadRequestFilterParseError if @fi.parse_error?
201
-
202
- # nil is the expected return for no errors
203
- nil
170
+ @uparms.check_filter
204
171
  end
205
172
 
206
173
  def check_u_p_orderby
207
- # TODO: this should be moved into OData::Order somehow,
208
- # at least partly
209
- return unless @params['$orderby']
210
-
211
- pordlist = @params['$orderby'].dup
212
- pordlist.split(',').each do |pord|
213
- pord.strip!
214
- qualfn, dir = pord.split(/\s/)
215
- qualfn.strip!
216
- dir.strip! if dir
217
- return BadRequestError unless @attribute_path_list.include? qualfn
218
- return BadRequestError unless [nil, 'asc', 'desc'].include? dir
219
- end
220
- # nil is the expected return for no errors
221
- nil
174
+ @uparms.check_order
222
175
  end
223
176
 
224
177
  def build_attribute_path_list
@@ -248,14 +201,38 @@ module OData
248
201
  def check_url_params
249
202
  return nil unless @params
250
203
 
251
- check_u_p_top || check_u_p_skip || check_u_p_orderby || check_u_p_filter || check_u_p_inlinecount
204
+ check_u_p_top || check_u_p_skip || check_u_p_orderby ||
205
+ check_u_p_filter || check_u_p_inlinecount
252
206
  end
253
207
 
254
208
  def initialize_dataset
255
- # initialize dataset
256
209
  @cx = self
257
210
  @ax = nil
258
211
  @cx = navigated_dataset if @cx.navigated_coll
212
+ @model = if @cx.respond_to? :model
213
+ @cx.model
214
+ else
215
+ @cx
216
+ end
217
+ @jh = @model.join_by_paths_helper
218
+ @uparms = UrlParameters.new(@jh, @params)
219
+ end
220
+
221
+ # finally return the requested output according to format, options etc
222
+ def odata_get_output(req)
223
+ return @error.odata_get(req) if @error
224
+
225
+ if req.walker.do_count
226
+ [200, CT_TEXT, @cx.count.to_s]
227
+ elsif req.accept?(APPJSON)
228
+ if req.walker.do_links
229
+ [200, CT_JSON, to_odata_links_json(service: req.service)]
230
+ else
231
+ [200, CT_JSON, to_odata_json(service: req.service)]
232
+ end
233
+ else # TODO: other formats
234
+ 406
235
+ end
259
236
  end
260
237
 
261
238
  # on model class level we return the collection
@@ -264,24 +241,50 @@ module OData
264
241
  @uribase = req.uribase
265
242
  initialize_dataset
266
243
 
267
- if (perr = check_url_params)
268
- perr.odata_get(req)
244
+ if (@error = check_url_params)
245
+ @error.odata_get(req)
269
246
  else
270
247
  odata_get_apply_params
271
- if req.walker.do_count
272
- [200, { 'Content-Type' => 'text/plain;charset=utf-8' },
273
- @cx.count.to_s]
274
- elsif req.accept?('application/json')
275
- if req.walker.do_links
276
- [200, { 'Content-Type' => 'application/json;charset=utf-8' },
277
- to_odata_links_json(service: req.service)]
278
- else
279
- [200, { 'Content-Type' => 'application/json;charset=utf-8' },
280
- to_odata_json(service: req.service)]
281
- end
282
- else # TODO: other formats
283
- 406
248
+ odata_get_output(req)
249
+ end
250
+ end
251
+
252
+ # add metadata xml to the passed REXML schema object
253
+ def add_metadata_rexml(schema)
254
+ enty = schema.add_element('EntityType', 'Name' => to_s)
255
+ # with their properties
256
+ db_schema.each do |pnam, prop|
257
+ if prop[:primary_key] == true
258
+ enty.add_element('Key').add_element('PropertyRef',
259
+ 'Name' => pnam.to_s)
284
260
  end
261
+ attrs = { 'Name' => pnam.to_s,
262
+ 'Type' => OData.get_edm_type(db_type: prop[:db_type]) }
263
+ attrs['Nullable'] = 'false' if prop[:allow_null] == false
264
+ enty.add_element('Property', attrs)
265
+ end
266
+ enty
267
+ end
268
+
269
+ # metadata REXML data for a single Nav attribute
270
+ def metadata_nav_rexml_attribs(assoc, cmap, relman, xnamespace)
271
+ from = type_name
272
+ to = cmap[assoc.to_s].type_name
273
+ relman.get_metadata_xml_attribs(from,
274
+ to,
275
+ association_reflection(assoc)[:type],
276
+ xnamespace)
277
+ end
278
+
279
+ # and their Nav attributes == Sequel Model association
280
+ def add_metadata_navs_rexml(schema_enty, cmap, relman, xnamespace)
281
+ associations.each do |assoc|
282
+ # associated objects need to be in the map...
283
+ next unless cmap[assoc.to_s]
284
+
285
+ nattrs = metadata_nav_rexml_attribs(assoc, cmap, relman, xnamespace)
286
+
287
+ schema_enty.add_element('NavigationProperty', nattrs)
285
288
  end
286
289
  end
287
290
 
@@ -313,15 +316,13 @@ module OData
313
316
  return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
314
317
  end
315
318
 
316
- if req.accept?('application/json')
319
+ if req.accept?(APPJSON)
317
320
 
318
321
  new_entity = new_from_hson_h(data, in_changeset: req.in_changeset)
319
322
  req.register_content_id_ref(new_entity)
320
323
  new_entity.copy_request_infos(req)
321
324
 
322
- [201,
323
- { 'Content-Type' => 'application/json;charset=utf-8' },
324
- new_entity.to_odata_post_json(service: req.service)]
325
+ [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
325
326
  else # TODO: other formats
326
327
  415
327
328
  end
@@ -382,7 +383,7 @@ module OData
382
383
  @entity_id_url_regexp = /\A\(\s*(#{iuk.join(',\s*')})\s*\)(.*)/
383
384
  else
384
385
  @pk_names = [primary_key.to_s]
385
- @entity_id_url_regexp = /\A\('?([\w\s]+)'?\)(.*)/
386
+ @entity_id_url_regexp = SINGLE_PK_URL_REGEXP
386
387
  end
387
388
  end
388
389
 
@@ -396,12 +397,12 @@ module OData
396
397
  data.keys.map(&:to_sym).find { |ksym| !(@columns.include? ksym) }
397
398
  end
398
399
 
399
- # A regexp matching all allowed attributes of the Entity
400
- # (eg ID|name|size etc... )
401
- def attribute_url_regexp
400
+ ## A regexp matching all allowed attributes of the Entity
401
+ ## (eg ID|name|size etc... ) at start position and returning the rest
402
+ def transition_attribute_regexp
402
403
  # db_schema.map { |sch| sch[0] }.join('|')
403
404
  # @columns is from Sequel Model
404
- @columns.join('|')
405
+ %r{\A/(#{@columns.join('|')})(.*)\z}
405
406
  end
406
407
 
407
408
  # methods related to transitions to next state (cf. walker)
@@ -416,7 +417,7 @@ module OData
416
417
 
417
418
  def transition_id(match_result)
418
419
  if (id = match_result[1])
419
- if (y = find(id))
420
+ if (y = find_by_odata_key(id))
420
421
  [y, :run]
421
422
  else
422
423
  [nil, :error, ErrorNotFound]
@@ -439,12 +440,14 @@ module OData
439
440
  module EntityClassMultiPK
440
441
  include EntityClassBase
441
442
  # id is for composite key, something like fx='aas',fy_w='0001'
442
- def find(mid)
443
- # Note: @iuk_rgx is (needs to be) built on start with
443
+ def find_by_odata_key(mid)
444
+ # @iuk_rgx is (needs to be) built on start with
444
445
  # collklass.prepare_pk
445
446
  md = @iuk_rgx.match(mid).to_a
446
447
  md.shift # remove first element which is the whole match
447
- self[*md] # Sequel Model rulez
448
+ # amazingly this works as expected from an Entity.get_related(...) anonymous class
449
+ # without need to redefine primary_key_lookup (returns nil for valid but unrelated keys)
450
+ primary_key_lookup(md)
448
451
  end
449
452
  end
450
453
 
@@ -452,8 +455,8 @@ module OData
452
455
  module EntityClassSinglePK
453
456
  include EntityClassBase
454
457
  # id is really just the value of single pk
455
- def find(id)
456
- self[id]
458
+ def find_by_odata_key(id)
459
+ primary_key_lookup(id)
457
460
  end
458
461
  end
459
462
  end