safrano 0.3.2 → 0.3.3
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 +40 -18
- data/lib/odata/batch.rb +17 -3
- data/lib/odata/collection.rb +97 -46
- data/lib/odata/collection_media.rb +148 -0
- data/lib/odata/common_logger.rb +34 -0
- data/lib/odata/entity.rb +159 -38
- data/lib/odata/error.rb +16 -5
- data/lib/odata/navigation_attribute.rb +119 -0
- data/lib/odata/walker.rb +12 -2
- data/lib/odata_rack_builder.rb +1 -1
- data/lib/rack_app.rb +12 -7
- data/lib/request.rb +15 -3
- data/lib/safrano.rb +1 -0
- data/lib/safrano_core.rb +2 -1
- data/lib/service.rb +41 -12
- data/lib/version.rb +4 -0
- metadata +6 -2
data/lib/odata/error.rb
CHANGED
@@ -14,6 +14,13 @@ module OData
|
|
14
14
|
super("class #{name} is not a Sequel Model", name)
|
15
15
|
end
|
16
16
|
end
|
17
|
+
# when published class as media does not have the mandatory media fields
|
18
|
+
class MediaModelError < NameError
|
19
|
+
def initialize(name)
|
20
|
+
super("Model #{name} does not have the mandatory media attributes content_type/media_src", name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
17
24
|
# when published association was not defined on Sequel level
|
18
25
|
class ModelAssociationNameError < NameError
|
19
26
|
def initialize(klass, symb)
|
@@ -27,13 +34,11 @@ module OData
|
|
27
34
|
# base module for HTTP errors
|
28
35
|
module Error
|
29
36
|
def odata_get(req)
|
30
|
-
if req.accept?(
|
31
|
-
[const_get(:HTTP_CODE),
|
32
|
-
{ 'Content-Type' => 'application/json;charset=utf-8' },
|
37
|
+
if req.accept?(APPJSON)
|
38
|
+
[const_get(:HTTP_CODE), CT_JSON,
|
33
39
|
{ 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
|
34
40
|
else
|
35
|
-
[const_get(:HTTP_CODE),
|
36
|
-
{ 'Content-Type' => 'text/plain;charset=utf-8' }, @msg]
|
41
|
+
[const_get(:HTTP_CODE), CT_TEXT, @msg]
|
37
42
|
end
|
38
43
|
end
|
39
44
|
end
|
@@ -49,6 +54,12 @@ module OData
|
|
49
54
|
@msg = 'Bad Request: Failed changeset '
|
50
55
|
end
|
51
56
|
|
57
|
+
# $value request for a non-media entity
|
58
|
+
class BadRequestNonMediaValue < BadRequestError
|
59
|
+
HTTP_CODE = 400
|
60
|
+
@msg = 'Bad Request: $value request for a non-media entity'
|
61
|
+
end
|
62
|
+
|
52
63
|
# for Syntax error in Filtering
|
53
64
|
class BadRequestFilterParseError < BadRequestError
|
54
65
|
HTTP_CODE = 400
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative '../safrano_core.rb'
|
3
|
+
require_relative './entity.rb'
|
4
|
+
|
5
|
+
module OData
|
6
|
+
# link newly created entities(child) to an existing parent
|
7
|
+
# by following the association_reflection rules
|
8
|
+
def OData.create_nav_relation(child, assoc, parent)
|
9
|
+
return unless assoc
|
10
|
+
|
11
|
+
# Note: this coding shares some bits from our sequel/plugins/join_by_paths,
|
12
|
+
# method build_unique_join_segments
|
13
|
+
# eventually there is an opportunity to have more reusable code here
|
14
|
+
case assoc[:type]
|
15
|
+
when :one_to_many, :one_to_one
|
16
|
+
# sets the FK values in child to corresponding related parent key-values
|
17
|
+
# thus creating the "link" between the new entity and the parent
|
18
|
+
# if a FK value is already set (not nil/NULL) then only check the
|
19
|
+
# consistency with the corresponding parent key-value
|
20
|
+
# If the FK value and the parent key value are different, then it's a
|
21
|
+
# a Bad Request error
|
22
|
+
|
23
|
+
leftm = assoc[:model] # should be same as parent.class
|
24
|
+
lks = [leftm.primary_key].flatten
|
25
|
+
rks = [assoc[:key]].flatten
|
26
|
+
join_cond = rks.zip(lks).to_h
|
27
|
+
join_cond.each { |rk, lk|
|
28
|
+
if child.values[rk] # FK in new entity from payload not nil, only check consistency
|
29
|
+
# with the parent - id(s)
|
30
|
+
if (child.values[rk] != parent.pk_hash[lk]) # error...
|
31
|
+
# TODO
|
32
|
+
end
|
33
|
+
else # we can set the FK value, thus creating the "link"
|
34
|
+
child.set(rk => parent.pk_hash[lk])
|
35
|
+
end
|
36
|
+
}
|
37
|
+
when :many_to_one
|
38
|
+
# sets the FK values in parent to corresponding related child key-values
|
39
|
+
# thus creating the "link" between the new entity and the parent
|
40
|
+
# Per design, this can only be called when the FK value is nil
|
41
|
+
# from NilNavigationAttribute.odata_post
|
42
|
+
lks = [assoc[:key]].flatten
|
43
|
+
rks = [child.class.primary_key].flatten
|
44
|
+
join_cond = rks.zip(lks).to_h
|
45
|
+
join_cond.each { |rk, lk|
|
46
|
+
if parent.values[lk] # FK in parent not nil, only check consistency
|
47
|
+
# with the child - id(s)
|
48
|
+
if (parent.values[lk] != child.pk_hash[rk]) # error...
|
49
|
+
# TODO
|
50
|
+
end
|
51
|
+
else # we can set the FK value, thus creating the "link"
|
52
|
+
parent.set(lk => child.pk_hash[rk])
|
53
|
+
end
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Represents a named but nil-valued navigation-attribute of an Entity
|
59
|
+
# (usually resulting from a NULL FK db value)
|
60
|
+
class NilNavigationAttribute
|
61
|
+
attr_reader :name
|
62
|
+
attr_reader :parent
|
63
|
+
def initialize(parent, name)
|
64
|
+
@parent = parent
|
65
|
+
@name = name
|
66
|
+
@navattr_reflection = parent.class.association_reflections[name.to_sym]
|
67
|
+
@klass = @navattr_reflection[:class_name].constantize
|
68
|
+
end
|
69
|
+
|
70
|
+
def odata_get(req)
|
71
|
+
if req.walker.media_value
|
72
|
+
OData::ErrorNotFound.odata_get
|
73
|
+
elsif req.accept?(APPJSON)
|
74
|
+
[200, CT_JSON, to_odata_json(service: req.service)]
|
75
|
+
else # TODO: other formats
|
76
|
+
415
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# create the nav. entity
|
81
|
+
def odata_post(req)
|
82
|
+
# delegate to the class method
|
83
|
+
@klass.odata_create_entity_and_relation(req, @navattr_reflection, @parent)
|
84
|
+
end
|
85
|
+
|
86
|
+
# create the nav. entity
|
87
|
+
def odata_put(req)
|
88
|
+
# delegate to the class method
|
89
|
+
@klass.odata_create_entity_and_relation(req, @navattr_reflection, @parent)
|
90
|
+
end
|
91
|
+
|
92
|
+
# empty output as OData json (v2)
|
93
|
+
def to_odata_json(*)
|
94
|
+
{ 'd' => {} }.to_json
|
95
|
+
end
|
96
|
+
|
97
|
+
# for testing purpose (assert_equal ...)
|
98
|
+
def ==(other)
|
99
|
+
(@parent == other.parent) && (@name == other.name)
|
100
|
+
end
|
101
|
+
|
102
|
+
# methods related to transitions to next state (cf. walker)
|
103
|
+
module Transitions
|
104
|
+
def transition_end(_match_result)
|
105
|
+
[nil, :end]
|
106
|
+
end
|
107
|
+
|
108
|
+
def transition_value(_match_result)
|
109
|
+
[self, :end_with_value]
|
110
|
+
end
|
111
|
+
|
112
|
+
def allowed_transitions
|
113
|
+
[Safrano::TransitionEnd,
|
114
|
+
Safrano::TransitionValue]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
include Transitions
|
118
|
+
end
|
119
|
+
end
|
data/lib/odata/walker.rb
CHANGED
@@ -21,9 +21,12 @@ module OData
|
|
21
21
|
# is $count requested?
|
22
22
|
attr_accessor :do_count
|
23
23
|
|
24
|
-
# is $value requested?
|
24
|
+
# is $value (of attribute) requested?
|
25
25
|
attr_reader :raw_value
|
26
26
|
|
27
|
+
# is $value (of media entity) requested?
|
28
|
+
attr_reader :media_value
|
29
|
+
|
27
30
|
# are $links requested ?
|
28
31
|
attr_reader :do_links
|
29
32
|
|
@@ -35,9 +38,11 @@ module OData
|
|
35
38
|
@content_id_refs = content_id_refs
|
36
39
|
|
37
40
|
@contexts = [@context]
|
41
|
+
|
38
42
|
@path_start = @path_remain = if service
|
39
43
|
unprefixed(service.xpath_prefix, path)
|
40
44
|
else # This is for batch function
|
45
|
+
|
41
46
|
path
|
42
47
|
end
|
43
48
|
@path_done = ''
|
@@ -51,7 +56,9 @@ module OData
|
|
51
56
|
if (prefix == '') || (prefix == '/')
|
52
57
|
path
|
53
58
|
else
|
54
|
-
path.sub!(/\A#{prefix}/, '')
|
59
|
+
# path.sub!(/\A#{prefix}/, '')
|
60
|
+
# TODO check
|
61
|
+
path.sub(/\A#{prefix}/, '')
|
55
62
|
end
|
56
63
|
end
|
57
64
|
|
@@ -107,6 +114,9 @@ module OData
|
|
107
114
|
when :end_with_value
|
108
115
|
@raw_value = true
|
109
116
|
@status = :end
|
117
|
+
when :end_with_media_value
|
118
|
+
@media_value = true
|
119
|
+
@status = :end
|
110
120
|
when :run_with_links
|
111
121
|
@do_links = true
|
112
122
|
@status = :run
|
data/lib/odata_rack_builder.rb
CHANGED
data/lib/rack_app.rb
CHANGED
@@ -26,12 +26,7 @@ module OData
|
|
26
26
|
return @walker.error.odata_get(@request) unless @walker.error.nil?
|
27
27
|
|
28
28
|
# this is too critical; raise a real Exception
|
29
|
-
# begin
|
30
29
|
raise 'Walker construction failed with a unknown Error '
|
31
|
-
# rescue StandardError
|
32
|
-
# binding.pry
|
33
|
-
# end
|
34
|
-
# [500, {}, 'Server Error']
|
35
30
|
end
|
36
31
|
|
37
32
|
def odata_delete
|
@@ -42,6 +37,14 @@ module OData
|
|
42
37
|
end
|
43
38
|
end
|
44
39
|
|
40
|
+
def odata_put
|
41
|
+
if @walker.status == :end
|
42
|
+
@walker.end_context.odata_put(@request)
|
43
|
+
else
|
44
|
+
odata_error
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
45
48
|
def odata_patch
|
46
49
|
if @walker.status == :end
|
47
50
|
@walker.end_context.odata_patch(@request)
|
@@ -67,7 +70,7 @@ module OData
|
|
67
70
|
end
|
68
71
|
|
69
72
|
def odata_head
|
70
|
-
[200, {}, '']
|
73
|
+
[200, {}, ['']]
|
71
74
|
end
|
72
75
|
end
|
73
76
|
|
@@ -105,7 +108,9 @@ module OData
|
|
105
108
|
odata_delete
|
106
109
|
when 'OPTIONS'
|
107
110
|
odata_options
|
108
|
-
when '
|
111
|
+
when 'PUT'
|
112
|
+
odata_put
|
113
|
+
when 'PATCH', 'MERGE'
|
109
114
|
odata_patch
|
110
115
|
else
|
111
116
|
raise Error
|
data/lib/request.rb
CHANGED
@@ -10,6 +10,7 @@ module OData
|
|
10
10
|
HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze
|
11
11
|
HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'.freeze
|
12
12
|
HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
|
13
|
+
ON_CGST_ERROR = (proc { |r| raise(Sequel::Rollback) if r.in_changeset })
|
13
14
|
|
14
15
|
# borowed from Sinatra
|
15
16
|
class AcceptEntry
|
@@ -94,7 +95,7 @@ module OData
|
|
94
95
|
end
|
95
96
|
|
96
97
|
def create_odata_walker
|
97
|
-
@walker = Walker.new(@service, path_info, @content_id_references)
|
98
|
+
@env['safrano.walker'] = @walker = Walker.new(@service, path_info, @content_id_references)
|
98
99
|
end
|
99
100
|
|
100
101
|
def accept
|
@@ -132,13 +133,24 @@ module OData
|
|
132
133
|
end
|
133
134
|
end
|
134
135
|
|
135
|
-
def
|
136
|
+
def with_media_data
|
137
|
+
if (filename = @env['HTTP_SLUG'])
|
138
|
+
|
139
|
+
yield @env['rack.input'], content_type.split(';').first, filename
|
140
|
+
|
141
|
+
else
|
142
|
+
ON_CGST_ERROR.call(self)
|
143
|
+
return [400, {}, ['File upload error: Missing SLUG']]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def with_parsed_data
|
136
148
|
if content_type == APPJSON
|
137
149
|
# Parse json payload
|
138
150
|
begin
|
139
151
|
data = JSON.parse(body.read)
|
140
152
|
rescue JSON::ParserError => e
|
141
|
-
|
153
|
+
ON_CGST_ERROR.call(self)
|
142
154
|
return [400, {}, ['JSON Parser Error while parsing payload : ',
|
143
155
|
e.message]]
|
144
156
|
end
|
data/lib/safrano.rb
CHANGED
data/lib/safrano_core.rb
CHANGED
@@ -5,10 +5,11 @@ module OData
|
|
5
5
|
# some prominent constants... probably already defined elsewhere eg in Rack
|
6
6
|
# but lets KISS
|
7
7
|
CONTENT_TYPE = 'Content-Type'.freeze
|
8
|
+
CTT_TYPE_LC = 'content-type'.freeze
|
8
9
|
TEXTPLAIN_UTF8 = 'text/plain;charset=utf-8'.freeze
|
9
10
|
APPJSON = 'application/json'.freeze
|
10
11
|
APPXML = 'application/xml'.freeze
|
11
|
-
|
12
|
+
MP_MIXED = 'multipart/mixed'.freeze
|
12
13
|
APPXML_UTF8 = 'application/xml;charset=utf-8'.freeze
|
13
14
|
APPATOMXML_UTF8 = 'application/atomsvc+xml;charset=utf-8'.freeze
|
14
15
|
APPJSON_UTF8 = 'application/json;charset=utf-8'.freeze
|
data/lib/service.rb
CHANGED
@@ -103,12 +103,9 @@ module OData
|
|
103
103
|
end
|
104
104
|
|
105
105
|
def get_entity_odata_h(entity:, expand: nil, uribase:)
|
106
|
-
hres =
|
106
|
+
hres = entity.casted_values
|
107
107
|
hres['__metadata'] = entity.metadata_h(uribase: uribase)
|
108
108
|
|
109
|
-
# hres.merge!(entity.values)
|
110
|
-
hres.merge!(entity.casted_values)
|
111
|
-
|
112
109
|
nav_values_h = {}
|
113
110
|
nav_coll_h = {}
|
114
111
|
|
@@ -203,6 +200,7 @@ module OData
|
|
203
200
|
|
204
201
|
DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
|
205
202
|
TRAILING_SLASH = %r{/\z}.freeze
|
203
|
+
DEFAULT_PATH_PREFIX = '/'
|
206
204
|
|
207
205
|
# input is the DataServiceVersion request header string, eg.
|
208
206
|
# '2.0;blabla' ---> Version -> 2
|
@@ -260,18 +258,38 @@ module OData
|
|
260
258
|
other
|
261
259
|
end
|
262
260
|
|
263
|
-
def register_model(modelklass, entity_set_name = nil)
|
261
|
+
def register_model(modelklass, entity_set_name = nil, is_media = false)
|
264
262
|
# check that the provided klass is a Sequel Model
|
265
263
|
unless modelklass.is_a? Sequel::Model::ClassMethods
|
266
264
|
raise OData::API::ModelNameError, modelklass
|
267
265
|
end
|
268
266
|
|
269
|
-
if modelklass.
|
270
|
-
|
271
|
-
|
267
|
+
if modelklass.ancestors.include? OData::Entity
|
268
|
+
# modules were already added previously;
|
269
|
+
# cleanup state to avoid having data from previous calls
|
270
|
+
# mostly usefull for testing (eg API)
|
271
|
+
modelklass.reset
|
272
|
+
else # first API call... (normal non-testing case)
|
273
|
+
if modelklass.primary_key.is_a?(Array)
|
274
|
+
modelklass.extend OData::EntityClassMultiPK
|
275
|
+
modelklass.include OData::EntityMultiPK
|
276
|
+
else
|
277
|
+
modelklass.extend OData::EntityClassSinglePK
|
278
|
+
modelklass.include OData::EntitySinglePK
|
279
|
+
end
|
280
|
+
end
|
281
|
+
# Media/Non-media
|
282
|
+
if is_media
|
283
|
+
modelklass.extend OData::EntityClassMedia
|
284
|
+
# set default media handler . Can be overridden later with the
|
285
|
+
# "use HandlerKlass, options" API
|
286
|
+
|
287
|
+
modelklass.set_default_media_handler
|
288
|
+
modelklass.api_check_media_fields
|
289
|
+
modelklass.include OData::MediaEntity
|
272
290
|
else
|
273
|
-
modelklass.extend OData::
|
274
|
-
modelklass.include OData::
|
291
|
+
modelklass.extend OData::EntityClassNonMedia
|
292
|
+
modelklass.include OData::NonMediaEntity
|
275
293
|
end
|
276
294
|
|
277
295
|
modelklass.prepare_pk
|
@@ -290,6 +308,14 @@ module OData
|
|
290
308
|
modelklass.deferred_iblock = block if block_given?
|
291
309
|
end
|
292
310
|
|
311
|
+
def publish_media_model(modelklass, entity_set_name = nil, &block)
|
312
|
+
register_model(modelklass, entity_set_name, true)
|
313
|
+
# we need to execute the passed block in a deferred step
|
314
|
+
# after all models have been registered (due to rel. dependancies)
|
315
|
+
# modelklass.instance_eval(&block) if block_given?
|
316
|
+
modelklass.deferred_iblock = block if block_given?
|
317
|
+
end
|
318
|
+
|
293
319
|
def cmap=(imap)
|
294
320
|
@cmap = imap
|
295
321
|
set_collections_sorted(@cmap.values)
|
@@ -319,6 +345,9 @@ module OData
|
|
319
345
|
# now that we know all model klasses we can handle relationships
|
320
346
|
execute_deferred_iblocks
|
321
347
|
|
348
|
+
# set default path prefix if path_prefix was not called
|
349
|
+
path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
|
350
|
+
|
322
351
|
# and finally build the path list
|
323
352
|
@collections.each(&:build_attribute_path_list)
|
324
353
|
end
|
@@ -497,7 +526,7 @@ module OData
|
|
497
526
|
# Documents MUST be
|
498
527
|
# identified with the "application/atomsvc+xml" media type (see
|
499
528
|
# [RFC5023] section 8).
|
500
|
-
[200, CT_ATOMXML, service_xml(req)]
|
529
|
+
[200, CT_ATOMXML, [service_xml(req)]]
|
501
530
|
else
|
502
531
|
# this is returned by http://services.odata.org/V2/OData/OData.svc
|
503
532
|
415
|
@@ -570,7 +599,7 @@ module OData
|
|
570
599
|
|
571
600
|
def odata_get(req)
|
572
601
|
if req.accept?(APPXML)
|
573
|
-
[200, CT_APPXML, @service.metadata_xml(req)]
|
602
|
+
[200, CT_APPXML, [@service.metadata_xml(req)]]
|
574
603
|
else
|
575
604
|
415
|
576
605
|
end
|
data/lib/version.rb
ADDED