safrano 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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