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 +4 -4
- data/lib/multipart.rb +92 -91
- data/lib/odata/attribute.rb +15 -6
- data/lib/odata/collection.rb +109 -106
- data/lib/odata/collection_filter.rb +14 -485
- data/lib/odata/collection_order.rb +15 -22
- data/lib/odata/entity.rb +31 -41
- data/lib/odata/filter/error.rb +53 -0
- data/lib/odata/filter/parse.rb +171 -0
- data/lib/odata/filter/sequel.rb +208 -0
- data/lib/odata/filter/token.rb +59 -0
- data/lib/odata/filter/tree.rb +368 -0
- data/lib/odata/relations.rb +36 -0
- data/lib/odata/url_parameters.rb +58 -0
- data/lib/odata/walker.rb +55 -42
- data/lib/request.rb +1 -3
- data/lib/safrano.rb +17 -0
- data/lib/safrano_core.rb +30 -7
- data/lib/sequel/plugins/join_by_paths.rb +239 -0
- data/lib/sequel_join_by_paths.rb +5 -0
- data/lib/service.rb +84 -112
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffa3b9e1f282380dcdd32162bca490faa4749e05a1db11d65dc2b0c25c5ee76c
|
4
|
+
data.tar.gz: eae063a3541b8eec09bea8d98adf04dc4ad6bed16bdfe6dc3c06fd2df7bb0baf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
28
|
-
@
|
29
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
42
|
+
elsif @target.parser.last_part(line)
|
43
|
+
@state = :end
|
44
|
+
else
|
45
|
+
@target.parser.addline(line)
|
46
|
+
end
|
47
|
+
end
|
40
48
|
|
41
|
-
|
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
|
-
|
44
|
-
|
53
|
+
elsif /^#{CRLF}$/ =~ line
|
54
|
+
@target_ct = @target_hd['content-type'] || 'text/plain'
|
55
|
+
@state = new_content
|
45
56
|
|
46
|
-
|
57
|
+
end
|
58
|
+
end
|
47
59
|
|
48
|
-
|
60
|
+
def parse_line(line)
|
61
|
+
case @state
|
62
|
+
when :h
|
63
|
+
parse_head(line)
|
49
64
|
|
50
|
-
|
51
|
-
|
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
|
-
|
68
|
+
when :bmp
|
69
|
+
@state = :first_part if @target.parser.first_part(line)
|
62
70
|
|
63
|
-
|
71
|
+
when :first_part
|
72
|
+
parse_first_part(line)
|
64
73
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
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 { |
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/odata/attribute.rb
CHANGED
@@ -13,16 +13,25 @@ module OData
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def value
|
16
|
-
|
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,
|
22
|
-
|
23
|
-
|
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
|
data/lib/odata/collection.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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 ||
|
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 (
|
268
|
-
|
244
|
+
if (@error = check_url_params)
|
245
|
+
@error.odata_get(req)
|
269
246
|
else
|
270
247
|
odata_get_apply_params
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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?(
|
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 =
|
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
|
-
|
400
|
-
|
401
|
-
def
|
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 =
|
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
|
443
|
-
#
|
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
|
-
|
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
|
456
|
-
|
458
|
+
def find_by_odata_key(id)
|
459
|
+
primary_key_lookup(id)
|
457
460
|
end
|
458
461
|
end
|
459
462
|
end
|