safrano 0.3.2 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/odata/attribute.rb +1 -1
- data/lib/odata/batch.rb +24 -10
- data/lib/odata/collection.rb +242 -96
- data/lib/odata/collection_filter.rb +40 -9
- data/lib/odata/collection_media.rb +279 -0
- data/lib/odata/collection_order.rb +46 -36
- data/lib/odata/common_logger.rb +59 -0
- data/lib/odata/entity.rb +268 -54
- data/lib/odata/error.rb +58 -17
- data/lib/odata/expand.rb +123 -0
- data/lib/odata/filter/error.rb +6 -0
- data/lib/odata/filter/parse.rb +4 -12
- data/lib/odata/filter/sequel.rb +11 -13
- data/lib/odata/filter/tree.rb +11 -15
- data/lib/odata/navigation_attribute.rb +150 -0
- data/lib/odata/relations.rb +2 -2
- data/lib/odata/select.rb +42 -0
- data/lib/odata/url_parameters.rb +51 -36
- data/lib/odata/walker.rb +12 -4
- data/lib/safrano.rb +23 -12
- data/lib/{safrano_core.rb → safrano/core.rb} +14 -3
- data/lib/{multipart.rb → safrano/multipart.rb} +51 -29
- data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +1 -1
- data/lib/{rack_app.rb → safrano/rack_app.rb} +15 -10
- data/lib/{request.rb → safrano/request.rb} +21 -8
- data/lib/{response.rb → safrano/response.rb} +1 -2
- data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
- data/lib/{service.rb → safrano/service.rb} +93 -97
- data/lib/safrano/version.rb +3 -0
- data/lib/sequel/plugins/join_by_paths.rb +11 -10
- metadata +34 -15
data/lib/odata/error.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
require 'json'
|
4
2
|
require 'rexml/document'
|
5
3
|
require 'safrano.rb'
|
@@ -14,6 +12,13 @@ module OData
|
|
14
12
|
super("class #{name} is not a Sequel Model", name)
|
15
13
|
end
|
16
14
|
end
|
15
|
+
# when published class as media does not have the mandatory media fields
|
16
|
+
class MediaModelError < NameError
|
17
|
+
def initialize(name)
|
18
|
+
super("Model #{name} does not have the mandatory media attributes content_type/media_src", name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
17
22
|
# when published association was not defined on Sequel level
|
18
23
|
class ModelAssociationNameError < NameError
|
19
24
|
def initialize(klass, symb)
|
@@ -24,22 +29,33 @@ module OData
|
|
24
29
|
end
|
25
30
|
end
|
26
31
|
|
27
|
-
# base module for HTTP errors
|
28
|
-
module
|
32
|
+
# base module for HTTP errors, when used as a Error Class
|
33
|
+
module ErrorClass
|
29
34
|
def odata_get(req)
|
30
|
-
if req.accept?(
|
31
|
-
[const_get(:HTTP_CODE),
|
32
|
-
{ 'Content-Type' => 'application/json;charset=utf-8' },
|
35
|
+
if req.accept?(APPJSON)
|
36
|
+
[const_get(:HTTP_CODE), CT_JSON,
|
33
37
|
{ 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
|
34
38
|
else
|
35
|
-
[const_get(:HTTP_CODE),
|
36
|
-
{ 'Content-Type' => 'text/plain;charset=utf-8' }, @msg]
|
39
|
+
[const_get(:HTTP_CODE), CT_TEXT, @msg]
|
37
40
|
end
|
38
41
|
end
|
39
42
|
end
|
43
|
+
|
44
|
+
# base module for HTTP errors, when used as an Error instance
|
45
|
+
module ErrorInstance
|
46
|
+
def odata_get(req)
|
47
|
+
if req.accept?(APPJSON)
|
48
|
+
[self.class.const_get(:HTTP_CODE), CT_JSON,
|
49
|
+
{ 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
|
50
|
+
else
|
51
|
+
[self.class.const_get(:HTTP_CODE), CT_TEXT, @msg]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
40
56
|
# http Bad Req.
|
41
57
|
class BadRequestError
|
42
|
-
extend
|
58
|
+
extend ErrorClass
|
43
59
|
HTTP_CODE = 400
|
44
60
|
@msg = 'Bad Request Error'
|
45
61
|
end
|
@@ -49,11 +65,36 @@ module OData
|
|
49
65
|
@msg = 'Bad Request: Failed changeset '
|
50
66
|
end
|
51
67
|
|
68
|
+
# $value request for a non-media entity
|
69
|
+
class BadRequestNonMediaValue < BadRequestError
|
70
|
+
HTTP_CODE = 400
|
71
|
+
@msg = 'Bad Request: $value request for a non-media entity'
|
72
|
+
end
|
73
|
+
class BadRequestSequelAdapterError < BadRequestError
|
74
|
+
include ErrorInstance
|
75
|
+
def initialize(err)
|
76
|
+
@msg = err.inner.message
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
52
80
|
# for Syntax error in Filtering
|
53
81
|
class BadRequestFilterParseError < BadRequestError
|
54
82
|
HTTP_CODE = 400
|
55
|
-
@msg = 'Bad Request: Syntax error in
|
83
|
+
@msg = 'Bad Request: Syntax error in $filter'
|
84
|
+
end
|
85
|
+
|
86
|
+
# for Syntax error in $expand param
|
87
|
+
class BadRequestExpandParseError < BadRequestError
|
88
|
+
HTTP_CODE = 400
|
89
|
+
@msg = 'Bad Request: Syntax error in $expand'
|
90
|
+
end
|
91
|
+
|
92
|
+
# for Syntax error in $orderby param
|
93
|
+
class BadRequestOrderParseError < BadRequestError
|
94
|
+
HTTP_CODE = 400
|
95
|
+
@msg = 'Bad Request: Syntax error in $orderby'
|
56
96
|
end
|
97
|
+
|
57
98
|
# for $inlinecount error
|
58
99
|
class BadRequestInlineCountParamError < BadRequestError
|
59
100
|
HTTP_CODE = 400
|
@@ -61,36 +102,36 @@ module OData
|
|
61
102
|
end
|
62
103
|
# http not found
|
63
104
|
class ErrorNotFound
|
64
|
-
extend
|
105
|
+
extend ErrorClass
|
65
106
|
HTTP_CODE = 404
|
66
107
|
@msg = 'The requested ressource was not found'
|
67
108
|
end
|
68
109
|
# Transition error (Safrano specific)
|
69
110
|
class ServerTransitionError
|
70
|
-
extend
|
111
|
+
extend ErrorClass
|
71
112
|
HTTP_CODE = 500
|
72
113
|
@msg = 'Server error: Segment could not be parsed'
|
73
114
|
end
|
74
115
|
# generic http 500 server err
|
75
116
|
class ServerError
|
76
|
-
extend
|
117
|
+
extend ErrorClass
|
77
118
|
HTTP_CODE = 500
|
78
119
|
@msg = 'Server error'
|
79
120
|
end
|
80
121
|
# not implemented (Safrano specific)
|
81
122
|
class NotImplementedError
|
82
|
-
extend
|
123
|
+
extend ErrorClass
|
83
124
|
HTTP_CODE = 501
|
84
125
|
end
|
85
126
|
# batch not implemented (Safrano specific)
|
86
127
|
class BatchNotImplementedError
|
87
|
-
extend
|
128
|
+
extend ErrorClass
|
88
129
|
HTTP_CODE = 501
|
89
130
|
@msg = 'Not implemented: OData batch'
|
90
131
|
end
|
91
132
|
# error in filter parsing (Safrano specific)
|
92
133
|
class FilterParseError < BadRequestError
|
93
|
-
extend
|
134
|
+
extend ErrorClass
|
94
135
|
HTTP_CODE = 400
|
95
136
|
end
|
96
137
|
end
|
data/lib/odata/expand.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'odata/error.rb'
|
2
|
+
|
3
|
+
# all dataset expanding related classes in our OData module
|
4
|
+
# ie do eager loading
|
5
|
+
module OData
|
6
|
+
# base class for expanding
|
7
|
+
class ExpandBase
|
8
|
+
EmptyExpand = new # re-useable empty expanding (idempotent)
|
9
|
+
EMPTYH = {}.freeze
|
10
|
+
|
11
|
+
def self.factory(expandstr)
|
12
|
+
expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr)
|
13
|
+
end
|
14
|
+
|
15
|
+
# output template
|
16
|
+
attr_reader :template
|
17
|
+
|
18
|
+
def apply_to_dataset(dtcx)
|
19
|
+
dtcx
|
20
|
+
end
|
21
|
+
|
22
|
+
def empty?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_error?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def template
|
31
|
+
EMPTYH
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# single expand
|
36
|
+
class Expand < ExpandBase
|
37
|
+
# sequel eager arg.
|
38
|
+
attr_reader :arg
|
39
|
+
attr_reader :template
|
40
|
+
|
41
|
+
# used for Sequel eager argument
|
42
|
+
# Recursive array to deep hash
|
43
|
+
# [1,2,3,4] --> {1=>{2=>{3=>4}}}
|
44
|
+
# [1] --> 1
|
45
|
+
DEEPH_0 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_0.call(inp[1..-1]) } : inp[0] }
|
46
|
+
|
47
|
+
# used for building output template
|
48
|
+
# Recursive array to deep hash
|
49
|
+
# [1,2,3,4] --> {1=>{2=>{3=>4}}}
|
50
|
+
# [1] --> { 1 => {} }
|
51
|
+
DEEPH_1 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_1.call(inp[1..-1]) } : { inp[0] => {} } }
|
52
|
+
|
53
|
+
NODESEP = '/'.freeze
|
54
|
+
|
55
|
+
def initialize(exstr)
|
56
|
+
exstr.strip!
|
57
|
+
@expandp = exstr
|
58
|
+
@nodes = @expandp.split(NODESEP)
|
59
|
+
build_arg
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply_to_dataset(dtcx)
|
63
|
+
dtcx
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_arg
|
67
|
+
# 'a/b/c/d' ==> {a: {b:{c: :d}}}
|
68
|
+
# 'xy' ==> :xy
|
69
|
+
@arg = DEEPH_0.call(@nodes.map(&:to_sym))
|
70
|
+
@template = DEEPH_1.call(@nodes)
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_error?
|
74
|
+
# todo
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def empty?
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Multi expanding logic
|
84
|
+
class MultiExpand < ExpandBase
|
85
|
+
COMASPLIT = /\s*,\s*/.freeze
|
86
|
+
attr_reader :template
|
87
|
+
|
88
|
+
def initialize(expandstr)
|
89
|
+
expandstr.strip!
|
90
|
+
@expandp = expandstr
|
91
|
+
@exlist = []
|
92
|
+
|
93
|
+
@exlist = expandstr.split(COMASPLIT).map { |exstr| Expand.new(exstr) }
|
94
|
+
build_template
|
95
|
+
end
|
96
|
+
|
97
|
+
def apply_to_dataset(dtcx)
|
98
|
+
# use eager loading for each used association
|
99
|
+
@exlist.each { |exp| dtcx = dtcx.eager(exp.arg) }
|
100
|
+
dtcx
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_template
|
104
|
+
# 'a/b/c/d,xy' ==> [ {'a' =>{ 'b' => {'c' => {'d' => {} } }}},
|
105
|
+
# { 'xy' => {} }]
|
106
|
+
#
|
107
|
+
@template = @exlist.map(&:template)
|
108
|
+
|
109
|
+
# { 'a' => { 'b' => {'c' => 'd' }},
|
110
|
+
# 'xy' => {} }
|
111
|
+
@template = @template.inject({}) { |mrg, elmt| mrg.merge elmt }
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_error?
|
115
|
+
# todo
|
116
|
+
false
|
117
|
+
end
|
118
|
+
|
119
|
+
def empty?
|
120
|
+
false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/odata/filter/error.rb
CHANGED
data/lib/odata/filter/parse.rb
CHANGED
@@ -68,24 +68,18 @@ module OData
|
|
68
68
|
each_typed_token(@input) do |tok, typ|
|
69
69
|
case typ
|
70
70
|
when :FuncTree
|
71
|
-
with_accepted(tok, typ)
|
72
|
-
grow_at_cursor(FuncTree.new(tok))
|
73
|
-
end
|
71
|
+
with_accepted(tok, typ) { grow_at_cursor(FuncTree.new(tok)) }
|
74
72
|
when :Delimiter
|
75
73
|
case tok
|
76
74
|
when '('
|
77
75
|
with_accepted(tok, typ) do
|
78
|
-
unless @cursor.is_a? FuncTree
|
79
|
-
grow_at_cursor(IdentityFuncTree.new)
|
80
|
-
end
|
76
|
+
grow_at_cursor(IdentityFuncTree.new) unless @cursor.is_a? FuncTree
|
81
77
|
openarg = ArgTree.new('(')
|
82
78
|
@stack << openarg
|
83
79
|
grow_at_cursor(openarg)
|
84
80
|
end
|
85
81
|
when ')'
|
86
|
-
unless (@cursor = @stack.pop)
|
87
|
-
break invalid_closing_delimiter_error(tok, typ)
|
88
|
-
end
|
82
|
+
break invalid_closing_delimiter_error(tok, typ) unless (@cursor = @stack.pop)
|
89
83
|
|
90
84
|
with_accepted(tok, typ) do
|
91
85
|
@cursor.update_state(tok, typ)
|
@@ -94,9 +88,7 @@ module OData
|
|
94
88
|
end
|
95
89
|
|
96
90
|
when :Separator
|
97
|
-
unless (@cursor = @stack.last)
|
98
|
-
break invalid_separator_error(tok, typ)
|
99
|
-
end
|
91
|
+
break invalid_separator_error(tok, typ) unless (@cursor = @stack.last)
|
100
92
|
|
101
93
|
with_accepted(tok, typ) { @cursor.update_state(tok, typ) }
|
102
94
|
|
data/lib/odata/filter/sequel.rb
CHANGED
@@ -13,7 +13,7 @@ module OData
|
|
13
13
|
class RootTree
|
14
14
|
def apply_to_dataset(dtcx, jh)
|
15
15
|
filtexpr = @children.first.leuqes(jh)
|
16
|
-
|
16
|
+
jh.dataset(dtcx).where(filtexpr).select_all(jh.start_model.table_name)
|
17
17
|
end
|
18
18
|
|
19
19
|
def sequel_expr(jh)
|
@@ -38,21 +38,21 @@ module OData
|
|
38
38
|
when :substringof
|
39
39
|
|
40
40
|
# there are multiple possible argument types (but all should return edm.string)
|
41
|
-
if
|
41
|
+
if args[0].is_a?(QString)
|
42
42
|
# substringof('Rhum', name) -->
|
43
43
|
# name contains substr 'Rhum'
|
44
44
|
Sequel.like(args[1].leuqes(jh),
|
45
45
|
args[0].leuqes_substringof_sig1(jh))
|
46
46
|
# special non standard (ui5 client) case ?
|
47
|
-
elsif
|
47
|
+
elsif args[0].is_a?(Literal) && args[1].is_a?(Literal)
|
48
48
|
Sequel.like(args[1].leuqes(jh),
|
49
49
|
args[0].leuqes_substringof_sig1(jh))
|
50
|
-
elsif
|
50
|
+
elsif args[1].is_a?(QString)
|
51
51
|
# substringof(name, '__Route du Rhum__') -->
|
52
52
|
# '__Route du Rhum__' contains name as a substring
|
53
53
|
# TODO... check if the database supports instr (how?)
|
54
54
|
# othewise use substr(postgresql) or whatevr?
|
55
|
-
instr_substr_func = if
|
55
|
+
instr_substr_func = if Sequel::Model.db.adapter_scheme == :postgres
|
56
56
|
Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
|
57
57
|
else
|
58
58
|
Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
|
@@ -167,26 +167,24 @@ module OData
|
|
167
167
|
end
|
168
168
|
|
169
169
|
def leuqes_starts_like(_jh)
|
170
|
-
"#{@value
|
170
|
+
"#{@value}%"
|
171
171
|
end
|
172
172
|
|
173
173
|
def leuqes_ends_like(_jh)
|
174
|
-
"%#{@value
|
174
|
+
"%#{@value}"
|
175
175
|
end
|
176
176
|
|
177
177
|
def leuqes_substringof_sig1(_jh)
|
178
|
-
"%#{@value
|
178
|
+
"%#{@value}%"
|
179
179
|
end
|
180
180
|
end
|
181
181
|
|
182
182
|
# Literals are unquoted words
|
183
183
|
class Literal
|
184
184
|
def leuqes(jh)
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
raise OData::Filter::Parser::ErrorWrongColumnName
|
189
|
-
end
|
185
|
+
raise OData::Filter::Parser::ErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
|
186
|
+
|
187
|
+
Sequel[jh.start_model.table_name][@value.to_sym]
|
190
188
|
end
|
191
189
|
|
192
190
|
# non stantard extensions to support things like
|
data/lib/odata/filter/tree.rb
CHANGED
@@ -238,7 +238,7 @@ module OData
|
|
238
238
|
end
|
239
239
|
end
|
240
240
|
|
241
|
-
# TODO different num types?
|
241
|
+
# TODO: different num types?
|
242
242
|
def edm_type
|
243
243
|
:any
|
244
244
|
end
|
@@ -279,12 +279,10 @@ module OData
|
|
279
279
|
when :Separator
|
280
280
|
if @value == '(' && tok == ',' && @state == :val
|
281
281
|
true
|
282
|
+
elsif @state == :sep
|
283
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
282
284
|
else
|
283
|
-
|
284
|
-
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
285
|
-
else
|
286
|
-
true
|
287
|
-
end
|
285
|
+
true
|
288
286
|
end
|
289
287
|
when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
|
290
288
|
if (@state == :open) || (@state == :sep)
|
@@ -350,22 +348,20 @@ module OData
|
|
350
348
|
def initialize(val)
|
351
349
|
super(val)
|
352
350
|
# split into path + attrib
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
raise Parser::Error.new(self, Qualit)
|
358
|
-
end
|
351
|
+
raise Parser::Error.new(self, Qualit) unless (md = REGEXP.match(val))
|
352
|
+
|
353
|
+
@path = md[1].chomp('/')
|
354
|
+
@attrib = md[2]
|
359
355
|
end
|
360
356
|
end
|
361
357
|
|
362
358
|
# Quoted Strings
|
363
359
|
class QString < Leave
|
364
|
-
|
365
|
-
|
360
|
+
DBL_QO = "''".freeze
|
361
|
+
SI_QO = "'".freeze
|
366
362
|
def initialize(val)
|
367
363
|
# unescape double quotes
|
368
|
-
super(val.gsub(
|
364
|
+
super(val.gsub(DBL_QO, SI_QO))
|
369
365
|
end
|
370
366
|
|
371
367
|
def accept?(tok, typ)
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative '../safrano/core.rb'
|
3
|
+
require_relative './entity.rb'
|
4
|
+
|
5
|
+
module OData
|
6
|
+
# remove the relation between entity and parent by clearing
|
7
|
+
# the FK field(s) (if allowed)
|
8
|
+
def self.remove_nav_relation(assoc, parent)
|
9
|
+
return unless assoc
|
10
|
+
|
11
|
+
return unless assoc[:type] == :many_to_one
|
12
|
+
|
13
|
+
# removes/clear the FK values in parent
|
14
|
+
# thus deleting the "link" between the entity and the parent
|
15
|
+
# Note: This is called if we have to delete the child--> can only be
|
16
|
+
# done after removing the FK in parent (if allowed!)
|
17
|
+
lks = [assoc[:key]].flatten
|
18
|
+
lks.each do |lk|
|
19
|
+
parent.set(lk => nil)
|
20
|
+
parent.save(transaction: false)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# link newly created entities(child) to an existing parent
|
25
|
+
# by following the association_reflection rules
|
26
|
+
def self.create_nav_relation(child, assoc, parent)
|
27
|
+
return unless assoc
|
28
|
+
|
29
|
+
# Note: this coding shares some bits from our sequel/plugins/join_by_paths,
|
30
|
+
# method build_unique_join_segments
|
31
|
+
# eventually there is an opportunity to have more reusable code here
|
32
|
+
case assoc[:type]
|
33
|
+
when :one_to_many, :one_to_one
|
34
|
+
# sets the FK values in child to corresponding related parent key-values
|
35
|
+
# thus creating the "link" between the new entity and the parent
|
36
|
+
# if a FK value is already set (not nil/NULL) then only check the
|
37
|
+
# consistency with the corresponding parent key-value
|
38
|
+
# If the FK value and the parent key value are different, then it's a
|
39
|
+
# a Bad Request error
|
40
|
+
|
41
|
+
leftm = assoc[:model] # should be same as parent.class
|
42
|
+
lks = [leftm.primary_key].flatten
|
43
|
+
rks = [assoc[:key]].flatten
|
44
|
+
join_cond = rks.zip(lks).to_h
|
45
|
+
join_cond.each do |rk, lk|
|
46
|
+
if child.values[rk] # FK in new entity from payload not nil, only check consistency
|
47
|
+
# with the parent - id(s)
|
48
|
+
# if (child.values[rk] != parent.pk_hash[lk]) # error...
|
49
|
+
# TODO
|
50
|
+
# end
|
51
|
+
else # we can set the FK value, thus creating the "link"
|
52
|
+
child.set(rk => parent.pk_hash[lk])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
when :many_to_one
|
56
|
+
# sets the FK values in parent to corresponding related child key-values
|
57
|
+
# thus creating the "link" between the new entity and the parent
|
58
|
+
# Per design, this can only be called when the FK value is nil
|
59
|
+
# from NilNavigationAttribute.odata_post
|
60
|
+
lks = [assoc[:key]].flatten
|
61
|
+
rks = [child.class.primary_key].flatten
|
62
|
+
join_cond = rks.zip(lks).to_h
|
63
|
+
join_cond.each do |rk, lk|
|
64
|
+
if parent.values[lk] # FK in parent not nil, only check consistency
|
65
|
+
# with the child - id(s)
|
66
|
+
# if parent.values[lk] != child.pk_hash[rk] # error...
|
67
|
+
# TODO
|
68
|
+
# end
|
69
|
+
else # we can set the FK value, thus creating the "link"
|
70
|
+
parent.set(lk => child.pk_hash[rk])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module EntityBase
|
77
|
+
module NavigationInfo
|
78
|
+
attr_reader :nav_parent
|
79
|
+
attr_reader :navattr_reflection
|
80
|
+
attr_reader :nav_name
|
81
|
+
def set_relation_info(parent, name)
|
82
|
+
@nav_parent = parent
|
83
|
+
@nav_name = name
|
84
|
+
@navattr_reflection = parent.class.association_reflections[name.to_sym]
|
85
|
+
@nav_klass = @navattr_reflection[:class_name].constantize
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Represents a named but nil-valued navigation-attribute of an Entity
|
91
|
+
# (usually resulting from a NULL FK db value)
|
92
|
+
class NilNavigationAttribute
|
93
|
+
include EntityBase::NavigationInfo
|
94
|
+
def odata_get(req)
|
95
|
+
if req.walker.media_value
|
96
|
+
OData::ErrorNotFound.odata_get
|
97
|
+
elsif req.accept?(APPJSON)
|
98
|
+
[200, CT_JSON, to_odata_json(service: req.service)]
|
99
|
+
else # TODO: other formats
|
100
|
+
415
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# create the nav. entity
|
105
|
+
def odata_post(req)
|
106
|
+
# delegate to the class method
|
107
|
+
@nav_klass.odata_create_entity_and_relation(req,
|
108
|
+
@navattr_reflection,
|
109
|
+
@nav_parent)
|
110
|
+
end
|
111
|
+
|
112
|
+
# create the nav. entity
|
113
|
+
def odata_put(req)
|
114
|
+
# if req.walker.raw_value
|
115
|
+
# delegate to the class method
|
116
|
+
@nav_klass.odata_create_entity_and_relation(req,
|
117
|
+
@navattr_reflection,
|
118
|
+
@nav_parent)
|
119
|
+
# else
|
120
|
+
# end
|
121
|
+
end
|
122
|
+
|
123
|
+
# empty output as OData json (v2)
|
124
|
+
def to_odata_json(*)
|
125
|
+
{ 'd' => EMPTY_HASH }.to_json
|
126
|
+
end
|
127
|
+
|
128
|
+
# for testing purpose (assert_equal ...)
|
129
|
+
def ==(other)
|
130
|
+
(@nav_parent == other.nav_parent) && (@nav_name == other.nav_name)
|
131
|
+
end
|
132
|
+
|
133
|
+
# methods related to transitions to next state (cf. walker)
|
134
|
+
module Transitions
|
135
|
+
def transition_end(_match_result)
|
136
|
+
[nil, :end]
|
137
|
+
end
|
138
|
+
|
139
|
+
def transition_value(_match_result)
|
140
|
+
[self, :end_with_value]
|
141
|
+
end
|
142
|
+
|
143
|
+
def allowed_transitions
|
144
|
+
[Safrano::TransitionEnd,
|
145
|
+
Safrano::TransitionValue]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
include Transitions
|
149
|
+
end
|
150
|
+
end
|