safrano 0.3.4 → 0.4.4
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/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +15 -10
- data/lib/odata/batch.rb +17 -15
- data/lib/odata/collection.rb +141 -500
- data/lib/odata/collection_filter.rb +44 -37
- data/lib/odata/collection_media.rb +193 -43
- data/lib/odata/collection_order.rb +50 -37
- data/lib/odata/common_logger.rb +39 -12
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +201 -176
- data/lib/odata/error.rb +186 -33
- data/lib/odata/expand.rb +126 -0
- data/lib/odata/filter/base.rb +69 -0
- data/lib/odata/filter/error.rb +55 -6
- data/lib/odata/filter/parse.rb +38 -36
- data/lib/odata/filter/sequel.rb +121 -67
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +15 -11
- data/lib/odata/filter/tree.rb +110 -60
- data/lib/odata/function_import.rb +166 -0
- data/lib/odata/model_ext.rb +618 -0
- data/lib/odata/navigation_attribute.rb +50 -32
- data/lib/odata/relations.rb +7 -7
- data/lib/odata/select.rb +54 -0
- data/lib/{safrano_core.rb → odata/transition.rb} +14 -60
- data/lib/odata/url_parameters.rb +128 -37
- data/lib/odata/walker.rb +19 -11
- data/lib/safrano.rb +18 -28
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +43 -0
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/{multipart.rb → safrano/multipart.rb} +37 -41
- data/lib/safrano/rack_app.rb +175 -0
- data/lib/{odata_rack_builder.rb → safrano/rack_builder.rb} +18 -2
- data/lib/{request.rb → safrano/request.rb} +102 -50
- data/lib/{response.rb → safrano/response.rb} +5 -4
- data/lib/safrano/sequel_join_by_paths.rb +5 -0
- data/lib/{service.rb → safrano/service.rb} +257 -188
- data/lib/safrano/version.rb +5 -0
- data/lib/sequel/plugins/join_by_paths.rb +17 -29
- metadata +53 -17
- data/lib/rack_app.rb +0 -174
- data/lib/sequel_join_by_paths.rb +0 -5
- data/lib/version.rb +0 -4
data/lib/odata/common_logger.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class ODataCommonLogger < CommonLogger
|
3
5
|
def call(env)
|
@@ -5,21 +7,46 @@ module Rack
|
|
5
7
|
super
|
6
8
|
end
|
7
9
|
|
10
|
+
# Handle https://github.com/rack/rack/pull/1526
|
11
|
+
# new in Rack 2.2.2 : Format has now 11 placeholders instead of 10
|
12
|
+
|
13
|
+
MSG_FUNC = if FORMAT.count('%') == 10
|
14
|
+
lambda { |env, length, status, began_at|
|
15
|
+
FORMAT % [
|
16
|
+
env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
|
17
|
+
env['REMOTE_USER'] || '-',
|
18
|
+
Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
|
19
|
+
env[REQUEST_METHOD],
|
20
|
+
env[SCRIPT_NAME] + env[PATH_INFO],
|
21
|
+
env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
|
22
|
+
env[SERVER_PROTOCOL],
|
23
|
+
status.to_s[0..3],
|
24
|
+
length,
|
25
|
+
Utils.clock_time - began_at
|
26
|
+
]
|
27
|
+
}
|
28
|
+
elsif FORMAT.count('%') == 11
|
29
|
+
lambda { |env, length, status, began_at|
|
30
|
+
FORMAT % [
|
31
|
+
env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
|
32
|
+
env['REMOTE_USER'] || '-',
|
33
|
+
Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
|
34
|
+
env[REQUEST_METHOD],
|
35
|
+
env[SCRIPT_NAME],
|
36
|
+
env[PATH_INFO],
|
37
|
+
env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
|
38
|
+
env[SERVER_PROTOCOL],
|
39
|
+
status.to_s[0..3],
|
40
|
+
length,
|
41
|
+
Utils.clock_time - began_at
|
42
|
+
]
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
8
46
|
def batch_log(env, status, header, began_at)
|
9
47
|
length = extract_content_length(header)
|
10
48
|
|
11
|
-
msg =
|
12
|
-
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
|
13
|
-
env["REMOTE_USER"] || "-",
|
14
|
-
Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
|
15
|
-
env[REQUEST_METHOD],
|
16
|
-
env[PATH_INFO],
|
17
|
-
env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
|
18
|
-
env[SERVER_PROTOCOL],
|
19
|
-
status.to_s[0..3],
|
20
|
-
length,
|
21
|
-
Utils.clock_time - began_at
|
22
|
-
]
|
49
|
+
msg = MSG_FUNC.call(env, length, status, began_at)
|
23
50
|
|
24
51
|
logger = @logger || env[RACK_ERRORS]
|
25
52
|
# Standard library logger doesn't support write but it supports << which actually
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Safrano
|
4
|
+
module FunctionImport
|
5
|
+
class ResultDefinition
|
6
|
+
D = 'd'.freeze
|
7
|
+
DJ_OPEN = '{"d":'.freeze
|
8
|
+
DJ_CLOSE = '}'.freeze
|
9
|
+
METAK = '__metadata'.freeze
|
10
|
+
TYPEK = 'type'.freeze
|
11
|
+
VALUEK = 'value'.freeze
|
12
|
+
RESULTSK = 'results'.freeze
|
13
|
+
COLLECTION = 'Collection'.freeze
|
14
|
+
|
15
|
+
def initialize(klassmod)
|
16
|
+
@klassmod = klassmod
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_odata_json(result, _req)
|
20
|
+
"#{DJ_OPEN}#{result.odata_h.to_json}#{DJ_CLOSE}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def type_metadata
|
24
|
+
@klassmod.type_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class ResultAsComplexType < ResultDefinition
|
28
|
+
end
|
29
|
+
class ResultAsComplexTypeColl < ResultDefinition
|
30
|
+
def type_metadata
|
31
|
+
"Collection(#{@klassmod.type_name})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_odata_json(coll, _req)
|
35
|
+
"#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
class ResultAsEntity < ResultDefinition
|
39
|
+
def to_odata_json(result_entity, req)
|
40
|
+
result_entity.instance_exec do
|
41
|
+
copy_request_infos(req)
|
42
|
+
to_odata_json(request: req)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
class ResultAsEntityColl < ResultDefinition
|
47
|
+
def type_metadata
|
48
|
+
"Collection(#{@klassmod.type_name})"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_odata_json(result_dataset, req)
|
52
|
+
coll = Safrano::OData::Collection.new(@klassmod)
|
53
|
+
coll.instance_exec do
|
54
|
+
@params = req.params
|
55
|
+
initialize_dataset(result_dataset)
|
56
|
+
end
|
57
|
+
coll.to_odata_json(request: req)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
class ResultAsPrimitiveType < ResultDefinition
|
61
|
+
def to_odata_json(result, _req)
|
62
|
+
{ D => { METAK => { TYPEK => type_metadata },
|
63
|
+
VALUEK => @klassmod.odata_value(result) } }.to_json
|
64
|
+
end
|
65
|
+
end
|
66
|
+
class ResultAsPrimitiveTypeColl < ResultDefinition
|
67
|
+
def type_metadata
|
68
|
+
"Collection(#{@klassmod.type_name})"
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_odata_json(result, _req)
|
72
|
+
{ D => { METAK => { TYPEK => type_metadata },
|
73
|
+
RESULTSK => @klassmod.odata_collection(result) } }.to_json
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# a generic Struct like ruby's standard Struct, but implemented with a
|
79
|
+
# @values Hash, similar to Sequel models and
|
80
|
+
# with added OData functionality
|
81
|
+
class ComplexType
|
82
|
+
attr_reader :values
|
83
|
+
|
84
|
+
@namespace = nil
|
85
|
+
def self.namespace
|
86
|
+
@namespace
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.props
|
90
|
+
@props
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.type_name
|
94
|
+
"#{@namespace}.#{self.to_s}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def initialize
|
98
|
+
@values = {}
|
99
|
+
end
|
100
|
+
METAK = '__metadata'.freeze
|
101
|
+
TYPEK = 'type'.freeze
|
102
|
+
|
103
|
+
def odata_h
|
104
|
+
ret = { METAK => { TYPEK => self.class.type_name } }
|
105
|
+
@values.each { |k, v|
|
106
|
+
ret[k] = if v.respond_to? :odata_h
|
107
|
+
v.odata_h
|
108
|
+
else
|
109
|
+
v
|
110
|
+
end
|
111
|
+
}
|
112
|
+
ret
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.return_as_collection_descriptor
|
116
|
+
FunctionImport::ResultAsComplexTypeColl.new(self)
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.return_as_instance_descriptor
|
120
|
+
FunctionImport::ResultAsComplexType.new(self)
|
121
|
+
end
|
122
|
+
|
123
|
+
# add metadata xml to the passed REXML schema object
|
124
|
+
def self.add_metadata_rexml(schema)
|
125
|
+
ctty = schema.add_element('ComplexType', 'Name' => to_s)
|
126
|
+
|
127
|
+
# with their properties
|
128
|
+
@props.each do |prop, rbtype|
|
129
|
+
attrs = { 'Name' => prop.to_s,
|
130
|
+
'Type' => rbtype.type_name }
|
131
|
+
ctty.add_element('Property', attrs)
|
132
|
+
end
|
133
|
+
ctty
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def Safrano.ComplexType(**props)
|
138
|
+
Class.new(Safrano::ComplexType) do
|
139
|
+
@props = props
|
140
|
+
props.each { |a, klassmod|
|
141
|
+
asym = a.to_sym
|
142
|
+
define_method(asym) do @values[asym] end
|
143
|
+
define_method("#{a}=") do |val| @values[asym] = val end
|
144
|
+
}
|
145
|
+
define_method :initialize do |*p, **kwvals|
|
146
|
+
super()
|
147
|
+
p.zip(props.keys).each { |val, a| @values[a] = val } if p
|
148
|
+
kwvals.each { |a, val| @values[a] = val if props.key?(a) } if kwvals
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module Safrano
|
6
|
+
# Type mapping DB --> Edm
|
7
|
+
# TypeMap = {"INTEGER" => "Edm.Int32" , "TEXT" => "Edm.String",
|
8
|
+
# "STRING" => "Edm.String"}
|
9
|
+
# Todo: complete mapping... this is just for the most common ones
|
10
|
+
|
11
|
+
# TODO: use Sequel GENERIC_TYPES: -->
|
12
|
+
# Constants
|
13
|
+
# GENERIC_TYPES = %w'String Integer Float Numeric BigDecimal Date DateTime
|
14
|
+
# Time File TrueClass FalseClass'.freeze
|
15
|
+
# Classes specifying generic types that Sequel will convert to
|
16
|
+
# database-specific types.
|
17
|
+
DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
|
18
|
+
|
19
|
+
# used in $metadata
|
20
|
+
# cf. Sequel Database column_schema_default_to_ruby_value
|
21
|
+
# schema_column_type
|
22
|
+
# https://www.odata.org/documentation/odata-version-2-0/overview/
|
23
|
+
def self.default_edm_type(ruby_type:)
|
24
|
+
case ruby_type
|
25
|
+
when :integer
|
26
|
+
'Edm.Int32'
|
27
|
+
when :string
|
28
|
+
'Edm.String'
|
29
|
+
when :date, :datetime,
|
30
|
+
'Edm.DateTime'
|
31
|
+
when :time
|
32
|
+
'Edm.Time'
|
33
|
+
when :boolean
|
34
|
+
'Edm.Boolean'
|
35
|
+
when :float
|
36
|
+
'Edm.Double'
|
37
|
+
when :decimal
|
38
|
+
'Edm.Decimal'
|
39
|
+
when :blob
|
40
|
+
'Edm.Binary'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# use Edm twice so that we can do include Safrano::Edm and then
|
45
|
+
# have Edm::Int32 etc... availabe
|
46
|
+
# and we can have Edm::String different from ::String
|
47
|
+
module Edm
|
48
|
+
module Edm
|
49
|
+
module OutputClassMethods
|
50
|
+
def type_name
|
51
|
+
"Edm.#{name.split('::').last}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def odata_collection(array)
|
55
|
+
array
|
56
|
+
end
|
57
|
+
|
58
|
+
def odata_value(instance)
|
59
|
+
instance
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Null < NilClass
|
64
|
+
extend OutputClassMethods
|
65
|
+
# nil --> null convertion is done by to_json
|
66
|
+
def self.odata_value(instance)
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.convert_from_urlparam(v)
|
71
|
+
return Contract::NOK unless (v == 'null')
|
72
|
+
|
73
|
+
Contract.valid(nil)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Binary is a String with the BINARY encoding
|
78
|
+
class Binary < String
|
79
|
+
extend OutputClassMethods
|
80
|
+
|
81
|
+
def self.convert_from_urlparam(v)
|
82
|
+
Contract.valid(v.dup.force_encoding('BINARY'))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# an object alwys evaluates to
|
87
|
+
# true ([true, anything not false & not nil objs])
|
88
|
+
# or false([nil, false])
|
89
|
+
class Boolean < Object
|
90
|
+
extend OutputClassMethods
|
91
|
+
def Boolean.odata_value(instance)
|
92
|
+
instance ? true : false
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.odata_collection(array)
|
96
|
+
array.map { |v| odata_value(v) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.convert_from_urlparam(v)
|
100
|
+
return Contract::NOK unless ['true', 'false'].include?(v)
|
101
|
+
|
102
|
+
Contract.valid(v == 'true')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Bytes are usualy represented as Intger in ruby,
|
107
|
+
# eg.String.bytes --> Array of ints
|
108
|
+
class Byte < Integer
|
109
|
+
extend OutputClassMethods
|
110
|
+
|
111
|
+
def self.convert_from_urlparam(v)
|
112
|
+
return Contract::NOK unless ((bytev = v.to_i) < 256)
|
113
|
+
|
114
|
+
Contract.valid(bytev)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class DateTime < ::DateTime
|
119
|
+
extend OutputClassMethods
|
120
|
+
def DateTime.odata_value(instance)
|
121
|
+
instance.to_datetime
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.odata_collection(array)
|
125
|
+
array.map { |v| odata_value(v) }
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.convert_from_urlparam(v)
|
129
|
+
begin
|
130
|
+
Contract.valid(DateTime.parse(v))
|
131
|
+
rescue
|
132
|
+
return convertion_error(v)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class String < ::String
|
138
|
+
extend OutputClassMethods
|
139
|
+
|
140
|
+
def self.convert_from_urlparam(v)
|
141
|
+
Contract.valid(v)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class Int32 < Integer
|
146
|
+
extend OutputClassMethods
|
147
|
+
|
148
|
+
def self.convert_from_urlparam(v)
|
149
|
+
return Contract::NOK unless (ret = number_or_nil(v))
|
150
|
+
|
151
|
+
Contract.valid(ret)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class Int64 < Integer
|
156
|
+
extend OutputClassMethods
|
157
|
+
|
158
|
+
def self.convert_from_urlparam(v)
|
159
|
+
return Contract::NOK unless (ret = number_or_nil(v))
|
160
|
+
|
161
|
+
Contract.valid(ret)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class Double < Float
|
166
|
+
extend OutputClassMethods
|
167
|
+
|
168
|
+
def self.convert_from_urlparam(v)
|
169
|
+
begin
|
170
|
+
Contract.valid(v.to_f)
|
171
|
+
rescue
|
172
|
+
return Contract::NOK
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# include Safrano
|
181
|
+
|
182
|
+
# x = Edm::String.new('xxx')
|
183
|
+
|
184
|
+
# pp x
|
data/lib/odata/entity.rb
CHANGED
@@ -1,41 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'rexml/document'
|
3
5
|
require 'safrano.rb'
|
4
|
-
require 'odata/
|
6
|
+
require 'odata/model_ext.rb' # required for self.class.entity_type_name ??
|
7
|
+
require_relative 'navigation_attribute'
|
5
8
|
|
6
|
-
module
|
9
|
+
module Safrano
|
7
10
|
# this will be mixed in the Model classes (subclasses of Sequel Model)
|
8
11
|
module EntityBase
|
9
12
|
attr_reader :params
|
10
|
-
|
13
|
+
|
14
|
+
include Safrano::NavigationInfo
|
11
15
|
|
12
16
|
# methods related to transitions to next state (cf. walker)
|
13
17
|
module Transitions
|
14
18
|
def allowed_transitions
|
15
|
-
|
16
|
-
Safrano::TransitionEnd,
|
17
|
-
Safrano::TransitionCount,
|
18
|
-
Safrano::TransitionLinks,
|
19
|
-
Safrano::TransitionValue,
|
20
|
-
Safrano::Transition.new(self.class.transition_attribute_regexp,
|
21
|
-
trans: 'transition_attribute')
|
22
|
-
]
|
23
|
-
if (ncurgx = self.class.nav_collection_url_regexp)
|
24
|
-
alltr <<
|
25
|
-
Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z},
|
26
|
-
trans: 'transition_nav_collection')
|
27
|
-
|
28
|
-
end
|
29
|
-
if (neurgx = self.class.nav_entity_url_regexp)
|
30
|
-
alltr <<
|
31
|
-
Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z},
|
32
|
-
trans: 'transition_nav_entity')
|
33
|
-
end
|
34
|
-
alltr
|
19
|
+
self.class.entity_allowed_transitions
|
35
20
|
end
|
36
21
|
|
37
22
|
def transition_end(_match_result)
|
38
|
-
|
23
|
+
Safrano::Transition::RESULT_END
|
39
24
|
end
|
40
25
|
|
41
26
|
def transition_count(_match_result)
|
@@ -53,8 +38,7 @@ module OData
|
|
53
38
|
|
54
39
|
def transition_attribute(match_result)
|
55
40
|
attrib = match_result[1]
|
56
|
-
|
57
|
-
[OData::Attribute.new(self, attrib), :run]
|
41
|
+
[Safrano::Attribute.new(self, attrib), :run]
|
58
42
|
end
|
59
43
|
|
60
44
|
def transition_nav_collection(match_result)
|
@@ -66,17 +50,26 @@ module OData
|
|
66
50
|
attrib = match_result[1]
|
67
51
|
[get_related_entity(attrib), :run]
|
68
52
|
end
|
53
|
+
|
54
|
+
def transition_invalid_attribute(match_result)
|
55
|
+
invalid_attrib = match_result[1]
|
56
|
+
[nil, :error, Safrano::ErrorNotFoundSegment.new(invalid_attrib)]
|
57
|
+
end
|
69
58
|
end
|
70
59
|
|
71
60
|
include Transitions
|
72
61
|
|
62
|
+
# for testing only?
|
63
|
+
def ==(other)
|
64
|
+
((self.class.type_name == other.class.type_name) and (@values == other.values))
|
65
|
+
end
|
66
|
+
|
73
67
|
def nav_values
|
74
68
|
@nav_values = {}
|
75
69
|
|
76
70
|
self.class.nav_entity_attribs&.each_key do |na_str|
|
77
71
|
@nav_values[na_str.to_sym] = send(na_str)
|
78
72
|
end
|
79
|
-
|
80
73
|
@nav_values
|
81
74
|
end
|
82
75
|
|
@@ -88,43 +81,42 @@ module OData
|
|
88
81
|
@nav_coll
|
89
82
|
end
|
90
83
|
|
91
|
-
def uri
|
92
|
-
"
|
84
|
+
def uri
|
85
|
+
@odata_pk ||= "(#{pk_uri})"
|
86
|
+
"#{self.class.uri}#{@odata_pk}"
|
93
87
|
end
|
88
|
+
|
94
89
|
D = 'd'.freeze
|
95
|
-
|
96
|
-
|
90
|
+
DJ_OPEN = '{"d":'.freeze
|
91
|
+
DJ_CLOSE = '}'.freeze
|
92
|
+
|
97
93
|
# Json formatter for a single entity (probably OData V1/V2 like)
|
98
|
-
def to_odata_json(
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
"#{
|
94
|
+
def to_odata_json(request:)
|
95
|
+
template = self.class.output_template(expand_list: @uparms.expand.template,
|
96
|
+
select: @uparms.select)
|
97
|
+
innerj = request.service.get_entity_odata_h(entity: self,
|
98
|
+
template: template).to_json
|
99
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
104
100
|
end
|
105
101
|
|
106
|
-
#
|
107
|
-
|
108
|
-
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
else
|
118
|
-
v
|
119
|
-
end
|
120
|
-
}
|
102
|
+
# Json formatter for a single entity reached by navigation $links
|
103
|
+
def to_odata_onelink_json(service:)
|
104
|
+
innerj = service.get_entity_odata_link_h(entity: self).to_json
|
105
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def selected_values_for_odata(cols)
|
109
|
+
allvals = values_for_odata
|
110
|
+
selvals = {}
|
111
|
+
cols.map(&:to_sym).each { |k| selvals[k] = allvals[k] if allvals.key?(k) }
|
112
|
+
selvals
|
121
113
|
end
|
122
114
|
|
123
115
|
# post paylod expects the new entity in an array
|
124
116
|
def to_odata_post_json(service:)
|
125
117
|
innerj = service.get_coll_odata_h(array: [self],
|
126
|
-
|
127
|
-
"#{
|
118
|
+
template: self.class.default_template).to_json
|
119
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
128
120
|
end
|
129
121
|
|
130
122
|
def type_name
|
@@ -133,38 +125,59 @@ module OData
|
|
133
125
|
|
134
126
|
def copy_request_infos(req)
|
135
127
|
@params = req.params
|
136
|
-
@uribase = req.uribase
|
137
128
|
@do_links = req.walker.do_links
|
129
|
+
@uparms = UrlParameters4Single.new(self, @params)
|
138
130
|
end
|
139
131
|
|
140
|
-
|
141
|
-
def odata_get(req)
|
142
|
-
copy_request_infos(req)
|
143
|
-
|
132
|
+
def odata_get_output(req)
|
144
133
|
if req.walker.media_value
|
145
134
|
odata_media_value_get(req)
|
146
135
|
elsif req.accept?(APPJSON)
|
147
|
-
|
136
|
+
# json is default content type so we dont need to specify it here again
|
137
|
+
if req.walker.do_links
|
138
|
+
[200, EMPTY_HASH, [to_odata_onelink_json(service: req.service)]]
|
139
|
+
else
|
140
|
+
[200, EMPTY_HASH, [to_odata_json(request: req)]]
|
141
|
+
end
|
148
142
|
else # TODO: other formats
|
149
143
|
415
|
150
144
|
end
|
151
145
|
end
|
152
146
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
147
|
+
# Finally Process REST verbs...
|
148
|
+
def odata_get(req)
|
149
|
+
copy_request_infos(req)
|
150
|
+
@uparms.check_all.tap_valid { return odata_get_output(req) }
|
151
|
+
.tap_error { |e| return e.odata_get(req) }
|
152
|
+
end
|
153
|
+
|
154
|
+
DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
|
155
|
+
Safrano.remove_nav_relation(assoc, parent)
|
156
|
+
entity.destroy(transaction: false)
|
157
|
+
end
|
158
|
+
|
159
|
+
def odata_delete_relation_and_entity(req, assoc, parent)
|
160
|
+
if parent
|
161
|
+
if req.in_changeset
|
162
|
+
# in-changeset requests get their own transaction
|
163
|
+
DELETE_REL_AND_ENTY.call(self, assoc, parent)
|
164
|
+
else
|
165
|
+
db.transaction do
|
166
|
+
DELETE_REL_AND_ENTY.call(self, assoc, parent)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
else
|
170
|
+
destroy(transaction: false)
|
159
171
|
end
|
172
|
+
rescue StandardError => e
|
173
|
+
raise SequelAdapterError.new(e)
|
160
174
|
end
|
161
175
|
|
162
176
|
# TODO: differentiate between POST/PUT/PATCH/MERGE
|
163
177
|
def odata_post(req)
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
if req.accept?(APPJSON)
|
178
|
+
if req.walker.media_value
|
179
|
+
odata_media_value_put(req)
|
180
|
+
elsif req.accept?(APPJSON)
|
168
181
|
data.delete('__metadata')
|
169
182
|
|
170
183
|
if req.in_changeset
|
@@ -174,7 +187,7 @@ module OData
|
|
174
187
|
update_fields(data, self.class.data_fields, missing: :skip)
|
175
188
|
end
|
176
189
|
|
177
|
-
[202,
|
190
|
+
[202, EMPTY_HASH, to_odata_post_json(service: req.service)]
|
178
191
|
else # TODO: other formats
|
179
192
|
415
|
180
193
|
end
|
@@ -183,23 +196,20 @@ module OData
|
|
183
196
|
def odata_put(req)
|
184
197
|
if req.walker.media_value
|
185
198
|
odata_media_value_put(req)
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
@uribase = req.uribase
|
190
|
-
data.delete('__metadata')
|
191
|
-
|
192
|
-
if req.in_changeset
|
193
|
-
set_fields(data, self.class.data_fields, missing: :skip)
|
194
|
-
save(transaction: false)
|
195
|
-
else
|
196
|
-
update_fields(data, self.class.data_fields, missing: :skip)
|
197
|
-
end
|
199
|
+
elsif req.accept?(APPJSON)
|
200
|
+
data = JSON.parse(req.body.read)
|
201
|
+
data.delete('__metadata')
|
198
202
|
|
199
|
-
|
200
|
-
|
201
|
-
|
203
|
+
if req.in_changeset
|
204
|
+
set_fields(data, self.class.data_fields, missing: :skip)
|
205
|
+
save(transaction: false)
|
206
|
+
else
|
207
|
+
update_fields(data, self.class.data_fields, missing: :skip)
|
202
208
|
end
|
209
|
+
|
210
|
+
ARY_204_EMPTY_HASH_ARY
|
211
|
+
else # TODO: other formats
|
212
|
+
415
|
203
213
|
end
|
204
214
|
end
|
205
215
|
|
@@ -209,14 +219,12 @@ module OData
|
|
209
219
|
|
210
220
|
# validate payload column names
|
211
221
|
if (invalid = self.class.invalid_hash_data?(data))
|
212
|
-
::
|
213
|
-
return [422,
|
222
|
+
::Safrano::Request::ON_CGST_ERROR.call(req)
|
223
|
+
return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
|
214
224
|
end
|
215
225
|
# TODO: check values/types
|
216
226
|
|
217
227
|
my_data_fields = self.class.data_fields
|
218
|
-
@uribase = req.uribase
|
219
|
-
# if req.accept?('application/json')
|
220
228
|
|
221
229
|
if req.in_changeset
|
222
230
|
set_fields(data, my_data_fields, missing: :skip)
|
@@ -225,68 +233,15 @@ module OData
|
|
225
233
|
update_fields(data, my_data_fields, missing: :skip)
|
226
234
|
end
|
227
235
|
# patch should return 204 + no content
|
228
|
-
|
236
|
+
ARY_204_EMPTY_HASH_ARY
|
229
237
|
end
|
230
238
|
end
|
231
239
|
|
232
|
-
#
|
233
|
-
#
|
234
|
-
module NavigationRedefinitions
|
235
|
-
def all
|
236
|
-
@child_method.call
|
237
|
-
end
|
238
|
-
|
239
|
-
def count
|
240
|
-
@child_method.call.count
|
241
|
-
end
|
242
|
-
|
243
|
-
def dataset
|
244
|
-
@child_dataset_method.call
|
245
|
-
end
|
246
|
-
|
247
|
-
def navigated_dataset
|
248
|
-
@child_dataset_method.call
|
249
|
-
end
|
250
|
-
|
251
|
-
def each
|
252
|
-
y = @child_method.call
|
253
|
-
y.each { |enty| yield enty }
|
254
|
-
end
|
255
|
-
|
256
|
-
def type_name
|
257
|
-
superclass.type_name
|
258
|
-
end
|
259
|
-
|
260
|
-
def media_handler
|
261
|
-
superclass.media_handler
|
262
|
-
end
|
263
|
-
|
264
|
-
def to_a
|
265
|
-
y = @child_method.call
|
266
|
-
y.to_a
|
267
|
-
end
|
268
|
-
end
|
269
|
-
# GetRelated that returns a anonymous Class (ie. representing a collection)
|
270
|
-
# subtype of the related object Class ( childklass )
|
240
|
+
# GetRelated that returns a collection object representing
|
241
|
+
# wrapping the related object Class ( childklass )
|
271
242
|
# (...to_many relationship )
|
272
243
|
def get_related(childattrib)
|
273
|
-
|
274
|
-
childklass = self.class.nav_collection_attribs[childattrib]
|
275
|
-
Class.new(childklass) do
|
276
|
-
# this makes use of Sequel's Model relationships; eg this is
|
277
|
-
# 'Race[12].Edition'
|
278
|
-
# where Race[12] would be our self and 'Edition' is the
|
279
|
-
# childattrib(collection)
|
280
|
-
@child_method = parent.method(childattrib.to_sym)
|
281
|
-
@child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
|
282
|
-
@nav_parent = parent
|
283
|
-
@navattr_reflection = parent.class.association_reflections[childattrib.to_sym]
|
284
|
-
prepare_pk
|
285
|
-
prepare_fields
|
286
|
-
# Now in this anonymous Class we can refine the "all, count and []
|
287
|
-
# methods, to take into account the relationship
|
288
|
-
extend NavigationRedefinitions
|
289
|
-
end
|
244
|
+
Safrano::OData::NavigatedCollection.new(childattrib, self)
|
290
245
|
end
|
291
246
|
|
292
247
|
# GetRelatedEntity that returns an single related Entity
|
@@ -301,69 +256,125 @@ module OData
|
|
301
256
|
# then we return a Nil... wrapper object. This object then
|
302
257
|
# allows to receive a POST operation that would actually create the nav attribute entity
|
303
258
|
|
304
|
-
ret = method(childattrib.to_sym).call ||
|
305
|
-
|
259
|
+
ret = method(childattrib.to_sym).call || Safrano::NilNavigationAttribute.new
|
260
|
+
|
261
|
+
ret.set_relation_info(self, childattrib)
|
306
262
|
|
307
263
|
ret
|
308
264
|
end
|
309
265
|
end
|
310
|
-
# end of module
|
266
|
+
# end of module SafranoEntity
|
311
267
|
module Entity
|
312
268
|
include EntityBase
|
313
269
|
end
|
314
270
|
|
315
271
|
module NonMediaEntity
|
316
272
|
# non media entity metadata for json h
|
317
|
-
def metadata_h
|
318
|
-
{ uri: uri
|
273
|
+
def metadata_h
|
274
|
+
{ uri: uri,
|
319
275
|
type: type_name }
|
320
276
|
end
|
321
277
|
|
322
278
|
def values_for_odata
|
323
|
-
values
|
279
|
+
values
|
280
|
+
end
|
281
|
+
|
282
|
+
def odata_delete(req)
|
283
|
+
if req.accept?(APPJSON)
|
284
|
+
# delete
|
285
|
+
begin
|
286
|
+
odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
|
287
|
+
[200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
|
288
|
+
rescue SequelAdapterError => e
|
289
|
+
BadRequestSequelAdapterError.new(e).odata_get(req)
|
290
|
+
end
|
291
|
+
else # TODO: other formats
|
292
|
+
415
|
293
|
+
end
|
324
294
|
end
|
325
295
|
|
326
296
|
# in case of a non media entity, we have to return an error on $value request
|
327
|
-
def odata_media_value_get(
|
328
|
-
|
297
|
+
def odata_media_value_get(_req)
|
298
|
+
BadRequestNonMediaValue.odata_get
|
329
299
|
end
|
330
300
|
|
331
301
|
# in case of a non media entity, we have to return an error on $value PUT
|
332
|
-
def odata_media_value_put(
|
333
|
-
|
302
|
+
def odata_media_value_put(_req)
|
303
|
+
BadRequestNonMediaValue.odata_get
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
module MappingBeforeOutput
|
308
|
+
# needed for proper datetime output
|
309
|
+
def casted_values(cols = nil)
|
310
|
+
vals = case cols
|
311
|
+
when nil
|
312
|
+
# we need to dup the model values as we need to change it before passing to_json,
|
313
|
+
# but we dont want to interfere with Sequel's owned data
|
314
|
+
# (eg because then in worst case it could happen that we write back changed values to DB)
|
315
|
+
values_for_odata.dup
|
316
|
+
else
|
317
|
+
selected_values_for_odata(cols)
|
318
|
+
end
|
319
|
+
self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
|
320
|
+
vals
|
321
|
+
end
|
322
|
+
end
|
323
|
+
module NoMappingBeforeOutput
|
324
|
+
# current model does not have eg. Time fields--> no special mapping, just to_json is fine
|
325
|
+
# --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
|
326
|
+
# need to change it, just output as is
|
327
|
+
def casted_values(cols = nil)
|
328
|
+
case cols
|
329
|
+
when nil
|
330
|
+
values_for_odata
|
331
|
+
else
|
332
|
+
selected_values_for_odata(cols)
|
333
|
+
end
|
334
334
|
end
|
335
335
|
end
|
336
336
|
|
337
337
|
module MediaEntity
|
338
338
|
# media entity metadata for json h
|
339
|
-
def metadata_h
|
340
|
-
{ uri: uri
|
339
|
+
def metadata_h
|
340
|
+
{ uri: uri,
|
341
341
|
type: type_name,
|
342
|
-
media_src: media_src
|
343
|
-
edit_media:
|
342
|
+
media_src: media_src,
|
343
|
+
edit_media: edit_media,
|
344
344
|
content_type: @values[:content_type] }
|
345
345
|
end
|
346
346
|
|
347
|
-
def media_src
|
348
|
-
|
347
|
+
def media_src
|
348
|
+
version = self.class.media_handler.ressource_version(self)
|
349
|
+
"#{uri}/$value?version=#{version}"
|
349
350
|
end
|
350
351
|
|
351
|
-
|
352
|
-
|
353
|
-
type_name
|
352
|
+
def edit_media
|
353
|
+
"#{uri}/$value"
|
354
354
|
end
|
355
355
|
|
356
|
-
# # this is just ModelKlass/pk as a single string
|
357
|
-
# def qualified_media_path_id
|
358
|
-
# "#{self.class}/#{media_path_id}"
|
359
|
-
# end
|
360
|
-
|
361
356
|
def values_for_odata
|
362
357
|
ret = values.dup
|
363
358
|
ret.delete(:content_type)
|
364
359
|
ret
|
365
360
|
end
|
366
361
|
|
362
|
+
def odata_delete(req)
|
363
|
+
if req.accept?(APPJSON)
|
364
|
+
# delete the MR
|
365
|
+
# delegate to the media handler on collection(ie class) level
|
366
|
+
# TODO error handling
|
367
|
+
|
368
|
+
self.class.media_handler.odata_delete(entity: self)
|
369
|
+
# delete the relation(s) to parent(s) (if any) and then entity
|
370
|
+
odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
|
371
|
+
# result
|
372
|
+
[200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
|
373
|
+
else # TODO: other formats
|
374
|
+
415
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
367
378
|
# real implementation for returning $value for a media entity
|
368
379
|
def odata_media_value_get(req)
|
369
380
|
# delegate to the media handler on collection(ie class) level
|
@@ -374,17 +385,20 @@ module OData
|
|
374
385
|
def odata_media_value_put(req)
|
375
386
|
model = self.class
|
376
387
|
req.with_media_data do |data, mimetype, filename|
|
377
|
-
emdata = { :
|
388
|
+
emdata = { content_type: mimetype }
|
378
389
|
if req.in_changeset
|
379
390
|
set_fields(emdata, model.data_fields, missing: :skip)
|
380
391
|
save(transaction: false)
|
381
392
|
else
|
393
|
+
|
382
394
|
update_fields(emdata, model.data_fields, missing: :skip)
|
395
|
+
|
383
396
|
end
|
384
397
|
model.media_handler.replace_file(data: data,
|
385
398
|
entity: self,
|
386
399
|
filename: filename)
|
387
|
-
|
400
|
+
|
401
|
+
ARY_204_EMPTY_HASH_ARY
|
388
402
|
end
|
389
403
|
end
|
390
404
|
end
|
@@ -399,18 +413,29 @@ module OData
|
|
399
413
|
def media_path_id
|
400
414
|
pk.to_s
|
401
415
|
end
|
416
|
+
|
417
|
+
def media_path_ids
|
418
|
+
[pk]
|
419
|
+
end
|
402
420
|
end
|
403
421
|
|
404
422
|
# for multiple key
|
405
423
|
module EntityMultiPK
|
406
424
|
include Entity
|
407
425
|
def pk_uri
|
408
|
-
|
409
|
-
self.
|
426
|
+
pku = +''
|
427
|
+
self.class.odata_upk_parts.each_with_index { |upart, i|
|
428
|
+
pku = "#{pku}#{upart}#{pk[i]}"
|
429
|
+
}
|
430
|
+
pku
|
410
431
|
end
|
411
432
|
|
412
433
|
def media_path_id
|
413
|
-
|
434
|
+
pk_hash.values.join(SPACE)
|
435
|
+
end
|
436
|
+
|
437
|
+
def media_path_ids
|
438
|
+
pk_hash.values
|
414
439
|
end
|
415
440
|
end
|
416
441
|
end
|