safrano 0.3.2 → 0.4.2
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/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
|