safrano 0.3.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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