safrano 0.4.3 → 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 +6 -2
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +136 -642
- data/lib/odata/collection_filter.rb +16 -40
- data/lib/odata/collection_media.rb +56 -37
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +53 -117
- data/lib/odata/error.rb +142 -37
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +4 -1
- data/lib/odata/filter/error.rb +43 -27
- data/lib/odata/filter/parse.rb +33 -25
- data/lib/odata/filter/sequel.rb +97 -56
- data/lib/odata/filter/sequel_function_adapter.rb +50 -49
- data/lib/odata/filter/token.rb +10 -10
- data/lib/odata/filter/tree.rb +75 -41
- data/lib/odata/function_import.rb +166 -0
- data/lib/odata/model_ext.rb +618 -0
- data/lib/odata/navigation_attribute.rb +9 -24
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +17 -5
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +100 -24
- data/lib/odata/walker.rb +15 -7
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +12 -94
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +25 -20
- data/lib/safrano/rack_app.rb +61 -62
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
- data/lib/safrano/request.rb +95 -37
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +132 -94
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +24 -5
@@ -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,15 +1,17 @@
|
|
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 ??
|
5
7
|
require_relative 'navigation_attribute'
|
6
8
|
|
7
|
-
module
|
9
|
+
module Safrano
|
8
10
|
# this will be mixed in the Model classes (subclasses of Sequel Model)
|
9
11
|
module EntityBase
|
10
12
|
attr_reader :params
|
11
13
|
|
12
|
-
include
|
14
|
+
include Safrano::NavigationInfo
|
13
15
|
|
14
16
|
# methods related to transitions to next state (cf. walker)
|
15
17
|
module Transitions
|
@@ -18,7 +20,7 @@ module OData
|
|
18
20
|
end
|
19
21
|
|
20
22
|
def transition_end(_match_result)
|
21
|
-
|
23
|
+
Safrano::Transition::RESULT_END
|
22
24
|
end
|
23
25
|
|
24
26
|
def transition_count(_match_result)
|
@@ -36,8 +38,7 @@ module OData
|
|
36
38
|
|
37
39
|
def transition_attribute(match_result)
|
38
40
|
attrib = match_result[1]
|
39
|
-
|
40
|
-
[OData::Attribute.new(self, attrib), :run]
|
41
|
+
[Safrano::Attribute.new(self, attrib), :run]
|
41
42
|
end
|
42
43
|
|
43
44
|
def transition_nav_collection(match_result)
|
@@ -49,10 +50,20 @@ module OData
|
|
49
50
|
attrib = match_result[1]
|
50
51
|
[get_related_entity(attrib), :run]
|
51
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
|
52
58
|
end
|
53
59
|
|
54
60
|
include Transitions
|
55
61
|
|
62
|
+
# for testing only?
|
63
|
+
def ==(other)
|
64
|
+
((self.class.type_name == other.class.type_name) and (@values == other.values))
|
65
|
+
end
|
66
|
+
|
56
67
|
def nav_values
|
57
68
|
@nav_values = {}
|
58
69
|
|
@@ -80,10 +91,11 @@ module OData
|
|
80
91
|
DJ_CLOSE = '}'.freeze
|
81
92
|
|
82
93
|
# Json formatter for a single entity (probably OData V1/V2 like)
|
83
|
-
def to_odata_json(
|
84
|
-
template = self.class.output_template(@uparms
|
85
|
-
|
86
|
-
|
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
|
87
99
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
88
100
|
end
|
89
101
|
|
@@ -114,27 +126,33 @@ module OData
|
|
114
126
|
def copy_request_infos(req)
|
115
127
|
@params = req.params
|
116
128
|
@do_links = req.walker.do_links
|
117
|
-
@uparms = UrlParameters4Single.new(@params)
|
129
|
+
@uparms = UrlParameters4Single.new(self, @params)
|
118
130
|
end
|
119
131
|
|
120
|
-
|
121
|
-
def odata_get(req)
|
122
|
-
copy_request_infos(req)
|
132
|
+
def odata_get_output(req)
|
123
133
|
if req.walker.media_value
|
124
134
|
odata_media_value_get(req)
|
125
135
|
elsif req.accept?(APPJSON)
|
136
|
+
# json is default content type so we dont need to specify it here again
|
126
137
|
if req.walker.do_links
|
127
|
-
[200,
|
138
|
+
[200, EMPTY_HASH, [to_odata_onelink_json(service: req.service)]]
|
128
139
|
else
|
129
|
-
[200,
|
140
|
+
[200, EMPTY_HASH, [to_odata_json(request: req)]]
|
130
141
|
end
|
131
142
|
else # TODO: other formats
|
132
143
|
415
|
133
144
|
end
|
134
145
|
end
|
135
146
|
|
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
|
+
|
136
154
|
DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
|
137
|
-
|
155
|
+
Safrano.remove_nav_relation(assoc, parent)
|
138
156
|
entity.destroy(transaction: false)
|
139
157
|
end
|
140
158
|
|
@@ -201,7 +219,7 @@ module OData
|
|
201
219
|
|
202
220
|
# validate payload column names
|
203
221
|
if (invalid = self.class.invalid_hash_data?(data))
|
204
|
-
::
|
222
|
+
::Safrano::Request::ON_CGST_ERROR.call(req)
|
205
223
|
return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
|
206
224
|
end
|
207
225
|
# TODO: check values/types
|
@@ -219,89 +237,11 @@ module OData
|
|
219
237
|
end
|
220
238
|
end
|
221
239
|
|
222
|
-
#
|
223
|
-
#
|
224
|
-
module NavigationRedefinitions
|
225
|
-
def all
|
226
|
-
@child_method.call
|
227
|
-
end
|
228
|
-
|
229
|
-
def count
|
230
|
-
@child_method.call.count
|
231
|
-
end
|
232
|
-
|
233
|
-
def dataset
|
234
|
-
@child_dataset_method.call
|
235
|
-
end
|
236
|
-
|
237
|
-
def navigated_dataset
|
238
|
-
@child_dataset_method.call
|
239
|
-
end
|
240
|
-
|
241
|
-
def each
|
242
|
-
y = @child_method.call
|
243
|
-
y.each { |enty| yield enty }
|
244
|
-
end
|
245
|
-
|
246
|
-
# TODO: design... this is not DRY
|
247
|
-
def slug_field
|
248
|
-
superclass.slug_field
|
249
|
-
end
|
250
|
-
|
251
|
-
def type_name
|
252
|
-
superclass.type_name
|
253
|
-
end
|
254
|
-
|
255
|
-
def time_cols
|
256
|
-
superclass.time_cols
|
257
|
-
end
|
258
|
-
|
259
|
-
def media_handler
|
260
|
-
superclass.media_handler
|
261
|
-
end
|
262
|
-
|
263
|
-
def uri
|
264
|
-
superclass.uri
|
265
|
-
end
|
266
|
-
|
267
|
-
def default_template
|
268
|
-
superclass.default_template
|
269
|
-
end
|
270
|
-
|
271
|
-
def allowed_transitions
|
272
|
-
superclass.allowed_transitions
|
273
|
-
end
|
274
|
-
|
275
|
-
def entity_allowed_transitions
|
276
|
-
superclass.entity_allowed_transitions
|
277
|
-
end
|
278
|
-
|
279
|
-
def to_a
|
280
|
-
y = @child_method.call
|
281
|
-
y.to_a
|
282
|
-
end
|
283
|
-
end
|
284
|
-
# GetRelated that returns a anonymous Class (ie. representing a collection)
|
285
|
-
# subtype of the related object Class ( childklass )
|
240
|
+
# GetRelated that returns a collection object representing
|
241
|
+
# wrapping the related object Class ( childklass )
|
286
242
|
# (...to_many relationship )
|
287
243
|
def get_related(childattrib)
|
288
|
-
|
289
|
-
childklass = self.class.nav_collection_attribs[childattrib]
|
290
|
-
Class.new(childklass) do
|
291
|
-
# this makes use of Sequel's Model relationships; eg this is
|
292
|
-
# 'Race[12].Edition'
|
293
|
-
# where Race[12] would be our self and 'Edition' is the
|
294
|
-
# childattrib(collection)
|
295
|
-
@child_method = parent.method(childattrib.to_sym)
|
296
|
-
@child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
|
297
|
-
@nav_parent = parent
|
298
|
-
@navattr_reflection = parent.class.association_reflections[childattrib.to_sym]
|
299
|
-
prepare_pk
|
300
|
-
prepare_fields
|
301
|
-
# Now in this anonymous Class we can refine the "all, count and []
|
302
|
-
# methods, to take into account the relationship
|
303
|
-
extend NavigationRedefinitions
|
304
|
-
end
|
244
|
+
Safrano::OData::NavigatedCollection.new(childattrib, self)
|
305
245
|
end
|
306
246
|
|
307
247
|
# GetRelatedEntity that returns an single related Entity
|
@@ -316,14 +256,14 @@ module OData
|
|
316
256
|
# then we return a Nil... wrapper object. This object then
|
317
257
|
# allows to receive a POST operation that would actually create the nav attribute entity
|
318
258
|
|
319
|
-
ret = method(childattrib.to_sym).call ||
|
259
|
+
ret = method(childattrib.to_sym).call || Safrano::NilNavigationAttribute.new
|
320
260
|
|
321
261
|
ret.set_relation_info(self, childattrib)
|
322
262
|
|
323
263
|
ret
|
324
264
|
end
|
325
265
|
end
|
326
|
-
# end of module
|
266
|
+
# end of module SafranoEntity
|
327
267
|
module Entity
|
328
268
|
include EntityBase
|
329
269
|
end
|
@@ -344,7 +284,7 @@ module OData
|
|
344
284
|
# delete
|
345
285
|
begin
|
346
286
|
odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
|
347
|
-
[200,
|
287
|
+
[200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
|
348
288
|
rescue SequelAdapterError => e
|
349
289
|
BadRequestSequelAdapterError.new(e).odata_get(req)
|
350
290
|
end
|
@@ -375,7 +315,7 @@ module OData
|
|
375
315
|
values_for_odata.dup
|
376
316
|
else
|
377
317
|
selected_values_for_odata(cols)
|
378
|
-
|
318
|
+
end
|
379
319
|
self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
|
380
320
|
vals
|
381
321
|
end
|
@@ -390,7 +330,7 @@ module OData
|
|
390
330
|
values_for_odata
|
391
331
|
else
|
392
332
|
selected_values_for_odata(cols)
|
393
|
-
|
333
|
+
end
|
394
334
|
end
|
395
335
|
end
|
396
336
|
|
@@ -413,16 +353,6 @@ module OData
|
|
413
353
|
"#{uri}/$value"
|
414
354
|
end
|
415
355
|
|
416
|
-
# directory where to put/find the media files for this entity-type
|
417
|
-
def klass_dir
|
418
|
-
type_name
|
419
|
-
end
|
420
|
-
|
421
|
-
# # this is just ModelKlass/pk as a single string
|
422
|
-
# def qualified_media_path_id
|
423
|
-
# "#{self.class}/#{media_path_id}"
|
424
|
-
# end
|
425
|
-
|
426
356
|
def values_for_odata
|
427
357
|
ret = values.dup
|
428
358
|
ret.delete(:content_type)
|
@@ -439,7 +369,7 @@ module OData
|
|
439
369
|
# delete the relation(s) to parent(s) (if any) and then entity
|
440
370
|
odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
|
441
371
|
# result
|
442
|
-
[200,
|
372
|
+
[200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
|
443
373
|
else # TODO: other formats
|
444
374
|
415
|
445
375
|
end
|
@@ -460,11 +390,14 @@ module OData
|
|
460
390
|
set_fields(emdata, model.data_fields, missing: :skip)
|
461
391
|
save(transaction: false)
|
462
392
|
else
|
393
|
+
|
463
394
|
update_fields(emdata, model.data_fields, missing: :skip)
|
395
|
+
|
464
396
|
end
|
465
397
|
model.media_handler.replace_file(data: data,
|
466
398
|
entity: self,
|
467
399
|
filename: filename)
|
400
|
+
|
468
401
|
ARY_204_EMPTY_HASH_ARY
|
469
402
|
end
|
470
403
|
end
|
@@ -490,8 +423,11 @@ module OData
|
|
490
423
|
module EntityMultiPK
|
491
424
|
include Entity
|
492
425
|
def pk_uri
|
493
|
-
|
494
|
-
|
426
|
+
pku = +''
|
427
|
+
self.class.odata_upk_parts.each_with_index { |upart, i|
|
428
|
+
pku = "#{pku}#{upart}#{pk[i]}"
|
429
|
+
}
|
430
|
+
pku
|
495
431
|
end
|
496
432
|
|
497
433
|
def media_path_id
|