safrano 0.4.1 → 0.4.6
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 +15 -13
- data/lib/odata/collection.rb +144 -535
- data/lib/odata/collection_filter.rb +47 -40
- data/lib/odata/collection_media.rb +155 -99
- data/lib/odata/collection_order.rb +50 -37
- data/lib/odata/common_logger.rb +36 -34
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +183 -216
- data/lib/odata/error.rb +195 -31
- data/lib/odata/expand.rb +126 -0
- data/lib/odata/filter/base.rb +74 -0
- data/lib/odata/filter/error.rb +49 -6
- data/lib/odata/filter/parse.rb +44 -36
- data/lib/odata/filter/sequel.rb +136 -67
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +26 -19
- data/lib/odata/filter/tree.rb +113 -63
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +639 -0
- data/lib/odata/navigation_attribute.rb +44 -61
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +54 -0
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +128 -37
- data/lib/odata/walker.rb +20 -10
- data/lib/safrano.rb +17 -37
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +29 -104
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +39 -43
- data/lib/safrano/rack_app.rb +68 -67
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
- data/lib/safrano/request.rb +102 -51
- data/lib/safrano/response.rb +5 -3
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +274 -219
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +17 -29
- metadata +34 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37c9ae21ef2d9a7cd8cc216719fcebc353a2588c2c2c9947b2490b7f7efa4e37
|
4
|
+
data.tar.gz: 3fa80a8985eee84c193783c4202efdb41aa64e77c173dc3a969164bdc5d0e832
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f17f10577a0296d0077e16271f4ebd5db84cafaf72a6c01ea445045e9be92b4d3d07b25a51e72d4174d4aa3ecc9d1aee92987ebbb46b7d2037ceb8843631876e
|
7
|
+
data.tar.gz: 68ce7bbad19ae58dcc96e9aa84db0eefb56f5e1f5ff8b02fbb3ae1d562362f29b08616b239a5adf6dfb768ce8da58d503a8a330095420d1c762bafe212a1b83a
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# needed for ruby < 2.5
|
4
|
+
module Safrano
|
5
|
+
module CoreExt
|
6
|
+
module Dir
|
7
|
+
module Iter
|
8
|
+
def each_child(dir)
|
9
|
+
::Dir.foreach(dir) do |x|
|
10
|
+
next if (x == '.') || (x == '..')
|
11
|
+
|
12
|
+
yield x
|
13
|
+
end
|
14
|
+
end unless ::Dir.respond_to? :each_child
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# picked from activsupport; needed for ruby < 2.5
|
2
|
+
# Destructively converts all keys using the +block+ operations.
|
3
|
+
# Same as +transform_keys+ but modifies +self+.
|
4
|
+
module Safrano
|
5
|
+
module CoreIncl
|
6
|
+
module Hash
|
7
|
+
module Transform
|
8
|
+
def transform_keys!
|
9
|
+
keys.each do |key|
|
10
|
+
self[yield(key)] = delete(key)
|
11
|
+
end
|
12
|
+
self
|
13
|
+
end unless method_defined? :transform_keys!
|
14
|
+
|
15
|
+
def symbolize_keys!
|
16
|
+
transform_keys! { |key| key.to_sym rescue key }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Safrano
|
2
|
+
module CoreIncl
|
3
|
+
module REXML
|
4
|
+
module Document
|
5
|
+
module Output
|
6
|
+
def to_pretty_xml
|
7
|
+
formatter = ::REXML::Formatters::Pretty.new(2)
|
8
|
+
formatter.compact = true
|
9
|
+
formatter.write(root, strio = '')
|
10
|
+
strio
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Safrano
|
4
|
+
module CoreIncl
|
5
|
+
module String
|
6
|
+
module Convert
|
7
|
+
# thanks https://stackoverflow.com/questions/1448670/ruby-stringto-class
|
8
|
+
def constantize
|
9
|
+
names = split('::')
|
10
|
+
names.shift if names.empty? || names.first.empty?
|
11
|
+
|
12
|
+
const = Object
|
13
|
+
names.each do |name|
|
14
|
+
const = if const.const_defined?(name)
|
15
|
+
const.const_get(name)
|
16
|
+
else
|
17
|
+
const.const_missing(name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
const
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/core_ext/dir.rb
ADDED
data/lib/odata/attribute.rb
CHANGED
@@ -1,27 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require_relative '../safrano/core.rb'
|
3
5
|
require_relative './entity.rb'
|
4
6
|
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
# Represents a named and valued attribute of an Entity
|
7
9
|
class Attribute
|
8
10
|
attr_reader :name
|
9
11
|
attr_reader :entity
|
12
|
+
|
10
13
|
def initialize(entity, name)
|
11
14
|
@entity = entity
|
12
15
|
@name = name
|
13
16
|
end
|
14
17
|
|
15
18
|
def value
|
16
|
-
# WARNING
|
17
|
-
# (and the inverted transformation is in test/client.rb)
|
18
|
-
# will require a more systematic solution some day
|
19
|
-
# WARNING 2... this require more work to handle the timezones topci
|
19
|
+
# WARNING ... this require more work to handle the timezones topci
|
20
20
|
# currently it is just set to make some minimal testcase work
|
21
21
|
case (v = @entity.values[@name.to_sym])
|
22
22
|
when Time
|
23
23
|
# try to get back the database time zone and value
|
24
|
-
(v + v.gmt_offset).utc.to_datetime
|
24
|
+
# (v + v.gmt_offset).utc.to_datetime
|
25
|
+
v.iso8601
|
26
|
+
|
25
27
|
else
|
26
28
|
v
|
27
29
|
end
|
@@ -31,7 +33,8 @@ module OData
|
|
31
33
|
if req.walker.raw_value
|
32
34
|
[200, CT_TEXT, value.to_s]
|
33
35
|
elsif req.accept?(APPJSON)
|
34
|
-
|
36
|
+
# json is default content type so we dont need to specify it here again
|
37
|
+
[200, EMPTY_HASH, to_odata_json(service: req.service)]
|
35
38
|
else # TODO: other formats
|
36
39
|
406
|
37
40
|
end
|
@@ -50,16 +53,18 @@ module OData
|
|
50
53
|
# methods related to transitions to next state (cf. walker)
|
51
54
|
module Transitions
|
52
55
|
def transition_end(_match_result)
|
53
|
-
|
56
|
+
Transition::RESULT_END
|
54
57
|
end
|
55
58
|
|
56
59
|
def transition_value(_match_result)
|
57
60
|
[self, :end_with_value]
|
58
61
|
end
|
59
62
|
|
63
|
+
ALLOWED_TRANSITIONS = [Safrano::TransitionEnd,
|
64
|
+
Safrano::TransitionValue].freeze
|
65
|
+
|
60
66
|
def allowed_transitions
|
61
|
-
|
62
|
-
Safrano::TransitionValue]
|
67
|
+
Transitions::ALLOWED_TRANSITIONS
|
63
68
|
end
|
64
69
|
end
|
65
70
|
include Transitions
|
data/lib/odata/batch.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../safrano/rack_app.rb'
|
2
4
|
require_relative '../safrano/core.rb'
|
3
5
|
require 'rack/body_proxy'
|
4
6
|
require_relative './common_logger.rb'
|
5
7
|
|
6
|
-
module
|
8
|
+
module Safrano
|
7
9
|
# Support for OData multipart $batch Requests
|
8
10
|
class Request
|
9
11
|
def create_batch_app
|
@@ -20,7 +22,7 @@ module OData
|
|
20
22
|
|
21
23
|
module Batch
|
22
24
|
# Mayonaise
|
23
|
-
class MyOApp <
|
25
|
+
class MyOApp < Safrano::ServerApp
|
24
26
|
attr_reader :full_req
|
25
27
|
attr_reader :response
|
26
28
|
attr_reader :db
|
@@ -41,8 +43,8 @@ module OData
|
|
41
43
|
env = batch_env(part_req)
|
42
44
|
env['HTTP_HOST'] = @full_req.env['HTTP_HOST']
|
43
45
|
began_at = Rack::Utils.clock_time
|
44
|
-
@request =
|
45
|
-
@response =
|
46
|
+
@request = Safrano::Request.new(env)
|
47
|
+
@response = Safrano::Response.new
|
46
48
|
|
47
49
|
if part_req.level == 2
|
48
50
|
@request.in_changeset = true
|
@@ -59,7 +61,7 @@ module OData
|
|
59
61
|
# TODO: test ?
|
60
62
|
if (logga = @full_req.env['safrano.logger_mw'])
|
61
63
|
logga.batch_log(env, status, header, began_at)
|
62
|
-
# TODO check why/if we need Rack::Utils::HeaderHash.new(header)
|
64
|
+
# TODO: check why/if we need Rack::Utils::HeaderHash.new(header)
|
63
65
|
# and Rack::BodyProxy.new(body) ?
|
64
66
|
end
|
65
67
|
[status, header, body]
|
@@ -71,7 +73,7 @@ module OData
|
|
71
73
|
|
72
74
|
headers.each do |name, value|
|
73
75
|
env_key = name.upcase.tr('-', '_')
|
74
|
-
env_key =
|
76
|
+
env_key = "HTTP_#{env_key}" unless env_key == 'CONTENT_TYPE'
|
75
77
|
converted_headers[env_key] = value
|
76
78
|
end
|
77
79
|
|
@@ -98,17 +100,17 @@ module OData
|
|
98
100
|
end
|
99
101
|
|
100
102
|
def transition_end(_match_result)
|
101
|
-
|
103
|
+
Safrano::Transition::RESULT_END
|
102
104
|
end
|
103
105
|
end
|
104
106
|
# jaune d'oeuf
|
105
107
|
class DisabledHandler < HandlerBase
|
106
108
|
def odata_post(_req)
|
107
|
-
[404,
|
109
|
+
[404, EMPTY_HASH, '$batch is not enabled ']
|
108
110
|
end
|
109
111
|
|
110
112
|
def odata_get(_req)
|
111
|
-
[404,
|
113
|
+
[404, EMPTY_HASH, '$batch is not enabled ']
|
112
114
|
end
|
113
115
|
end
|
114
116
|
# battre le tout
|
@@ -126,24 +128,24 @@ module OData
|
|
126
128
|
def odata_post(req)
|
127
129
|
@request = req
|
128
130
|
|
129
|
-
if @request.media_type ==
|
131
|
+
if @request.media_type == Safrano::MP_MIXED
|
130
132
|
|
131
133
|
batcha = @request.create_batch_app
|
132
134
|
@mult_request = @request.parse_multipart
|
133
135
|
|
134
136
|
@mult_request.prepare_content_id_refs
|
135
|
-
@mult_response =
|
137
|
+
@mult_response = Safrano::Response.new
|
136
138
|
|
137
139
|
resp_hdrs, @mult_response.body = @mult_request.get_http_resp(batcha)
|
138
140
|
|
139
141
|
[202, resp_hdrs, @mult_response.body[0]]
|
140
142
|
else
|
141
|
-
[415,
|
143
|
+
[415, EMPTY_HASH, 'Unsupported Media Type']
|
142
144
|
end
|
143
145
|
end
|
144
146
|
|
145
147
|
def odata_get(_req)
|
146
|
-
[405,
|
148
|
+
[405, EMPTY_HASH, 'You cant GET $batch, POST it ']
|
147
149
|
end
|
148
150
|
end
|
149
151
|
end
|
data/lib/odata/collection.rb
CHANGED
@@ -1,596 +1,205 @@
|
|
1
|
-
#
|
2
|
-
# somehow the character of an array (Enumerable)
|
3
|
-
# Thus Below we have called that "EntityClass". It's meant as "Collection"
|
4
|
-
|
5
|
-
require 'json'
|
6
|
-
require 'rexml/document'
|
7
|
-
require_relative '../safrano/core.rb'
|
8
|
-
require_relative 'error.rb'
|
9
|
-
require_relative 'collection_filter.rb'
|
10
|
-
require_relative 'collection_order.rb'
|
11
|
-
require_relative 'url_parameters.rb'
|
12
|
-
require_relative 'collection_media.rb'
|
13
|
-
|
14
|
-
# small helper method
|
15
|
-
# http://stackoverflow.com/
|
16
|
-
# questions/24980295/strictly-convert-string-to-integer-or-nil
|
17
|
-
def number_or_nil(str)
|
18
|
-
num = str.to_i
|
19
|
-
num if num.to_s == str
|
20
|
-
end
|
1
|
+
# frozen_string_literal: true
|
21
2
|
|
22
|
-
|
23
|
-
# thanks https://stackoverflow.com/questions/1448670/ruby-stringto-class
|
24
|
-
class String
|
25
|
-
def constantize
|
26
|
-
names = split('::')
|
27
|
-
names.shift if names.empty? || names.first.empty?
|
28
|
-
|
29
|
-
const = Object
|
30
|
-
names.each do |name|
|
31
|
-
const = if const.const_defined?(name)
|
32
|
-
const.const_get(name)
|
33
|
-
else
|
34
|
-
const.const_missing(name)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
const
|
38
|
-
end
|
39
|
-
end
|
3
|
+
require_relative 'model_ext'
|
40
4
|
|
41
|
-
module
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
SINGLE_PK_URL_REGEXP = /\A\(\s*'?([\w\s]+)'?\s*\)(.*)/.freeze
|
46
|
-
ONLY_INTEGER_RGX = /\A[+-]?\d+\z/
|
47
|
-
|
48
|
-
attr_reader :nav_collection_url_regexp
|
49
|
-
attr_reader :nav_entity_url_regexp
|
50
|
-
attr_reader :entity_id_url_regexp
|
51
|
-
attr_reader :attrib_paths_url_regexp
|
52
|
-
attr_reader :nav_collection_attribs
|
53
|
-
attr_reader :nav_entity_attribs
|
54
|
-
attr_reader :data_fields
|
55
|
-
attr_reader :inlinecount
|
56
|
-
|
57
|
-
# Sequel associations pointing to this model. Sequel provides association
|
58
|
-
# reflection information on the "from" side. But in some cases
|
59
|
-
# we will need the reverted way
|
60
|
-
# finally not needed and not used yet
|
61
|
-
# attr_accessor :assocs_to
|
62
|
-
|
63
|
-
# set to parent entity in case the collection is a nav.collection
|
64
|
-
# nil otherwise
|
65
|
-
attr_reader :nav_parent
|
66
|
-
|
67
|
-
attr_accessor :namespace
|
68
|
-
|
69
|
-
# dataset
|
70
|
-
attr_accessor :cx
|
71
|
-
|
72
|
-
# array of the objects --> dataset.to_a
|
73
|
-
attr_accessor :ax
|
74
|
-
|
75
|
-
# url params
|
76
|
-
attr_reader :params
|
77
|
-
|
78
|
-
# url parameters processing object (mostly covert to sequel exprs).
|
79
|
-
# exposed for testing only
|
80
|
-
attr_reader :uparms
|
81
|
-
|
82
|
-
# initialising block of code to be executed at end of
|
83
|
-
# ServerApp.publish_service after all model classes have been registered
|
84
|
-
# (without the associations/relationships)
|
85
|
-
# typically the block should contain the publication of the associations
|
86
|
-
attr_accessor :deferred_iblock
|
87
|
-
|
88
|
-
# convention: entityType is the Ruby Model class --> name is just to_s
|
89
|
-
# Warning: for handling Navigation relations, we use anonymous collection classes
|
90
|
-
# dynamically subtyped from a Model class, and in such an anonymous class
|
91
|
-
# the class-name is not the OData Type. In these subclass we redefine "type_name"
|
92
|
-
# thus when we need the Odata type name, we shall use this method instead of just the collection class name
|
93
|
-
def type_name
|
94
|
-
to_s
|
95
|
-
end
|
5
|
+
module Safrano
|
6
|
+
module OData
|
7
|
+
class Collection
|
8
|
+
attr_accessor :cx
|
96
9
|
|
97
|
-
|
98
|
-
|
99
|
-
@entity_set_name = (@entity_set_name || type_name)
|
100
|
-
end
|
10
|
+
# url params
|
11
|
+
attr_reader :params
|
101
12
|
|
102
|
-
|
103
|
-
#
|
104
|
-
|
105
|
-
@entity_set_name = nil
|
106
|
-
@uparms = nil
|
107
|
-
@params = nil
|
108
|
-
@ax = nil
|
109
|
-
@cx = nil
|
110
|
-
end
|
13
|
+
# url parameters processing object (mostly convert to sequel exprs).
|
14
|
+
# exposed for testing only
|
15
|
+
attr_reader :uparms
|
111
16
|
|
112
|
-
|
113
|
-
instance_eval { @deferred_iblock.call } if @deferred_iblock
|
114
|
-
end
|
17
|
+
attr_reader :inlinecount
|
115
18
|
|
116
|
-
|
117
|
-
def new_from_hson_h(hash)
|
118
|
-
enty = new
|
119
|
-
enty.set_fields(hash, @data_fields, missing: :skip)
|
120
|
-
enty
|
121
|
-
end
|
19
|
+
attr_reader :modelk
|
122
20
|
|
123
|
-
|
124
|
-
|
125
|
-
case assoc[:type]
|
126
|
-
when :one_to_many, :one_to_one
|
127
|
-
OData.create_nav_relation(new_entity, assoc, parent)
|
128
|
-
new_entity.save(transaction: false)
|
129
|
-
when :many_to_one
|
130
|
-
new_entity.save(transaction: false)
|
131
|
-
OData.create_nav_relation(new_entity, assoc, parent)
|
132
|
-
parent.save(transaction: false)
|
133
|
-
else
|
134
|
-
# not supported
|
21
|
+
def initialize(modelk)
|
22
|
+
@modelk = modelk
|
135
23
|
end
|
136
|
-
end
|
137
|
-
def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
138
|
-
if req.in_changeset
|
139
|
-
# in-changeset requests get their own transaction
|
140
|
-
CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
|
141
|
-
else
|
142
|
-
db.transaction do
|
143
|
-
CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
|
144
|
-
end
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def odata_get_inlinecount_w_sequel
|
149
|
-
return unless (icp = @params['$inlinecount'])
|
150
24
|
|
151
|
-
|
152
|
-
|
153
|
-
@cx.count
|
154
|
-
else
|
155
|
-
@cx.dataset.count
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def attrib_path_valid?(path)
|
161
|
-
@attribute_path_list.include? path
|
162
|
-
end
|
163
|
-
|
164
|
-
def odata_get_apply_params
|
165
|
-
begin
|
166
|
-
@cx = @uparms.apply_to_dataset(@cx)
|
167
|
-
rescue OData::Filter::Parser::ErrorWrongColumnName
|
168
|
-
@error = BadRequestFilterParseError
|
169
|
-
return
|
170
|
-
rescue OData::Filter::Parser::ErrorFunctionArgumentType
|
171
|
-
@error = BadRequestFilterParseError
|
172
|
-
return
|
25
|
+
def allowed_transitions
|
26
|
+
@modelk.allowed_transitions
|
173
27
|
end
|
174
|
-
odata_get_inlinecount_w_sequel
|
175
|
-
|
176
|
-
@cx = @cx.offset(@params['$skip']) if @params['$skip']
|
177
|
-
@cx = @cx.limit(@params['$top']) if @params['$top']
|
178
|
-
@cx
|
179
|
-
end
|
180
|
-
|
181
|
-
# url params validation methods.
|
182
|
-
# nil is the expected return for no errors
|
183
|
-
# an error class is returned in case of errors
|
184
|
-
# this way we can combine multiple validation calls with logical ||
|
185
|
-
def check_u_p_top
|
186
|
-
return unless @params['$top']
|
187
|
-
|
188
|
-
itop = number_or_nil(@params['$top'])
|
189
|
-
return BadRequestError if itop.nil? || itop.zero?
|
190
|
-
end
|
191
|
-
|
192
|
-
def check_u_p_skip
|
193
|
-
return unless @params['$skip']
|
194
|
-
|
195
|
-
iskip = number_or_nil(@params['$skip'])
|
196
|
-
return BadRequestError if iskip.nil? || iskip.negative?
|
197
|
-
end
|
198
|
-
|
199
|
-
def check_u_p_inlinecount
|
200
|
-
return unless (icp = @params['$inlinecount'])
|
201
28
|
|
202
|
-
|
203
|
-
|
29
|
+
def transition_end(_match_result)
|
30
|
+
Safrano::Transition::RESULT_END
|
204
31
|
end
|
205
32
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
def check_u_p_filter
|
210
|
-
@uparms.check_filter
|
211
|
-
end
|
212
|
-
|
213
|
-
def check_u_p_orderby
|
214
|
-
@uparms.check_order
|
215
|
-
end
|
216
|
-
|
217
|
-
def build_attribute_path_list
|
218
|
-
@attribute_path_list = attribute_path_list
|
219
|
-
@attrib_paths_url_regexp = @attribute_path_list.join('|')
|
220
|
-
end
|
221
|
-
|
222
|
-
def attribute_path_list(nodes = Set.new)
|
223
|
-
# break circles
|
224
|
-
return [] if nodes.include?(entity_set_name)
|
225
|
-
|
226
|
-
ret = @columns.map(&:to_s)
|
227
|
-
nodes.add entity_set_name
|
228
|
-
if @nav_entity_attribs
|
229
|
-
@nav_entity_attribs.each do |a, k|
|
230
|
-
ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
|
231
|
-
end
|
232
|
-
end
|
233
|
-
if @nav_collection_attribs
|
234
|
-
@nav_collection_attribs.each do |a, k|
|
235
|
-
ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
|
236
|
-
end
|
33
|
+
def transition_count(_match_result)
|
34
|
+
[self, :end_with_count]
|
237
35
|
end
|
238
|
-
ret
|
239
|
-
end
|
240
|
-
|
241
|
-
def check_url_params
|
242
|
-
return nil unless @params
|
243
|
-
|
244
|
-
check_u_p_top || check_u_p_skip || check_u_p_orderby ||
|
245
|
-
check_u_p_filter || check_u_p_inlinecount
|
246
|
-
end
|
247
36
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
@cx.model
|
254
|
-
else
|
255
|
-
@cx
|
256
|
-
end
|
257
|
-
@jh = @model.join_by_paths_helper
|
258
|
-
@uparms = UrlParameters.new(@jh, @params)
|
259
|
-
end
|
260
|
-
|
261
|
-
# finally return the requested output according to format, options etc
|
262
|
-
def odata_get_output(req)
|
263
|
-
return @error.odata_get(req) if @error
|
37
|
+
###########################################################
|
38
|
+
# this is redefined in NavigatedCollection
|
39
|
+
def dataset
|
40
|
+
@modelk.dataset
|
41
|
+
end
|
264
42
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
43
|
+
def transition_id(match_result)
|
44
|
+
if (rawid = match_result[1])
|
45
|
+
@modelk.parse_odata_key(rawid).tap_error do
|
46
|
+
return Safrano::Transition::RESULT_BAD_REQ_ERR
|
47
|
+
end.if_valid do |casted_id|
|
48
|
+
(y = find_by_odata_key(casted_id)) ? [y, :run] : Safrano::Transition::RESULT_NOT_FOUND_ERR
|
49
|
+
end
|
270
50
|
else
|
271
|
-
|
51
|
+
Safrano::Transition::RESULT_SERVER_TR_ERR
|
272
52
|
end
|
273
|
-
else # TODO: other formats
|
274
|
-
406
|
275
53
|
end
|
276
|
-
end
|
277
54
|
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
if (@error = check_url_params)
|
285
|
-
@error.odata_get(req)
|
286
|
-
else
|
287
|
-
odata_get_apply_params
|
288
|
-
odata_get_output(req)
|
55
|
+
# pkid can be a single value for single-pk models, or an array.
|
56
|
+
# type checking/convertion is done in check_odata_key_type
|
57
|
+
def find_by_odata_key(pkid)
|
58
|
+
lkup = @modelk.pk_lookup_expr(pkid)
|
59
|
+
dataset[lkup]
|
289
60
|
end
|
290
|
-
end
|
291
|
-
|
292
|
-
def odata_post(req)
|
293
|
-
odata_create_entity_and_relation(req, @navattr_reflection, @nav_parent)
|
294
|
-
end
|
295
61
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
schema.add_element('EntityType', 'Name' => to_s, 'HasStream' => 'true' )
|
300
|
-
else
|
301
|
-
schema.add_element('EntityType', 'Name' => to_s)
|
62
|
+
def initialize_dataset(dtset = nil)
|
63
|
+
@cx = dtset || @modelk
|
64
|
+
@uparms = UrlParameters4Coll.new(@cx, @params)
|
302
65
|
end
|
303
|
-
# with their properties
|
304
|
-
db_schema.each do |pnam, prop|
|
305
|
-
if prop[:primary_key] == true
|
306
|
-
enty.add_element('Key').add_element('PropertyRef',
|
307
|
-
'Name' => pnam.to_s)
|
308
|
-
end
|
309
|
-
attrs = { 'Name' => pnam.to_s,
|
310
|
-
'Type' => OData.get_edm_type(db_type: prop[:db_type]) }
|
311
|
-
attrs['Nullable'] = 'false' if prop[:allow_null] == false
|
312
|
-
enty.add_element('Property', attrs)
|
313
|
-
end
|
314
|
-
enty
|
315
|
-
end
|
316
66
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
assoc)
|
326
|
-
|
327
|
-
end
|
67
|
+
def odata_get_apply_params
|
68
|
+
@uparms.apply_to_dataset(@cx).map_result! do |dataset|
|
69
|
+
@cx = dataset
|
70
|
+
odata_get_inlinecount_w_sequel
|
71
|
+
if (skipp = @params['$skip'])
|
72
|
+
@cx = @cx.offset(skipp) if skipp != '0'
|
73
|
+
end
|
74
|
+
@cx = @cx.limit(@params['$top']) if @params['$top']
|
328
75
|
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
@nav_entity_attribs.each{|ne,klass|
|
333
|
-
nattr = metadata_nav_rexml_attribs(ne,
|
334
|
-
klass,
|
335
|
-
relman,
|
336
|
-
xnamespace)
|
337
|
-
schema_enty.add_element('NavigationProperty', nattr)
|
338
|
-
} if @nav_entity_attribs
|
339
|
-
|
340
|
-
|
341
|
-
@nav_collection_attribs.each{|nc,klass|
|
342
|
-
nattr = metadata_nav_rexml_attribs(nc,
|
343
|
-
klass,
|
344
|
-
relman,
|
345
|
-
xnamespace)
|
346
|
-
schema_enty.add_element('NavigationProperty', nattr)
|
347
|
-
} if @nav_collection_attribs
|
348
|
-
|
349
|
-
end
|
76
|
+
@cx
|
77
|
+
end
|
78
|
+
end
|
350
79
|
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
innerj = service.get_coll_odata_links_h(array: get_a,
|
356
|
-
uribase: @uribase,
|
357
|
-
icount: @inlinecount).to_json
|
358
|
-
"#{DJopen}#{innerj}#{DJclose}"
|
359
|
-
end
|
80
|
+
D = 'd'
|
81
|
+
DJ_OPEN = '{"d":'
|
82
|
+
DJ_CLOSE = '}'
|
83
|
+
EMPTYH = {}.freeze
|
360
84
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
85
|
+
def to_odata_json(request:)
|
86
|
+
template = @modelk.output_template(expand_list: @uparms.expand.template,
|
87
|
+
select: @uparms.select)
|
88
|
+
# TODO: Error handling if database contains binary BLOB data that cant be
|
89
|
+
# interpreted as UTF-8 then JSON will fail here
|
90
|
+
innerj = request.service.get_coll_odata_h(array: @cx.all,
|
91
|
+
template: template,
|
92
|
+
icount: @inlinecount).to_json
|
368
93
|
|
369
|
-
|
370
|
-
@ax.nil? ? @cx.to_a : @ax
|
371
|
-
end
|
372
|
-
|
373
|
-
# this functionally similar to the Sequel Rels (many_to_one etc)
|
374
|
-
# We need to base this on the Sequel rels, or extend them
|
375
|
-
def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
|
376
|
-
@nav_collection_attribs = (@nav_collection_attribs || {})
|
377
|
-
# @assocs_to = ( @assocs_to || [] )
|
378
|
-
# DONE: Error handling. This requires that associations
|
379
|
-
# have been properly defined with Sequel before
|
380
|
-
assoc = all_association_reflections.find do |a|
|
381
|
-
a[:name] == assoc_symb && a[:model] == self
|
94
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
382
95
|
end
|
383
|
-
|
384
|
-
|
96
|
+
|
97
|
+
def to_odata_links_json(service:)
|
98
|
+
innerj = service.get_coll_odata_links_h(array: @cx.all,
|
99
|
+
icount: @inlinecount).to_json
|
100
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
385
101
|
end
|
386
102
|
|
387
|
-
|
388
|
-
|
389
|
-
@nav_collection_attribs[lattr_name_str] = attr_class
|
390
|
-
@nav_collection_url_regexp = @nav_collection_attribs.keys.join('|')
|
391
|
-
end
|
103
|
+
def odata_get_inlinecount_w_sequel
|
104
|
+
return unless (icp = @params['$inlinecount'])
|
392
105
|
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
106
|
+
@inlinecount = if icp == 'allpages'
|
107
|
+
if @modelk.is_a? Sequel::Model::ClassMethods
|
108
|
+
@cx.count
|
109
|
+
else
|
110
|
+
@cx.dataset.count
|
111
|
+
end
|
112
|
+
end
|
400
113
|
end
|
401
|
-
|
402
|
-
|
114
|
+
|
115
|
+
# finally return the requested output according to format, options etc
|
116
|
+
def odata_get_output(req)
|
117
|
+
output = if req.walker.do_count
|
118
|
+
[200, CT_TEXT, @cx.count.to_s]
|
119
|
+
elsif req.accept?(APPJSON)
|
120
|
+
# json is default content type so we dont need to specify it here again
|
121
|
+
if req.walker.do_links
|
122
|
+
[200, EMPTYH, [to_odata_links_json(service: req.service)]]
|
123
|
+
else
|
124
|
+
[200, EMPTYH, [to_odata_json(request: req)]]
|
125
|
+
end
|
126
|
+
else # TODO: other formats
|
127
|
+
406
|
128
|
+
end
|
129
|
+
Contract.valid(output)
|
403
130
|
end
|
404
131
|
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
# attr_class.assocs_to = ( attr_class.assocs_to || [] )
|
410
|
-
# attr_class.assocs_to << assoc
|
411
|
-
end
|
132
|
+
# on model class level we return the collection
|
133
|
+
def odata_get(req)
|
134
|
+
@params = req.params
|
135
|
+
initialize_dataset
|
412
136
|
|
413
|
-
|
414
|
-
|
415
|
-
|
137
|
+
@uparms.check_all.if_valid { |_ret|
|
138
|
+
odata_get_apply_params.if_valid { |_ret|
|
139
|
+
odata_get_output(req)
|
140
|
+
}
|
141
|
+
}.tap_error { |e| return e.odata_get(req) }.result
|
142
|
+
end
|
416
143
|
|
417
|
-
|
418
|
-
|
419
|
-
@media_handler.register(self) if @media_handler
|
420
|
-
|
421
|
-
# and finally build the path list
|
422
|
-
build_attribute_path_list
|
423
|
-
end
|
424
|
-
|
425
|
-
def prepare_pk
|
426
|
-
if primary_key.is_a? Array
|
427
|
-
@pk_names = []
|
428
|
-
primary_key.each { |pk| @pk_names << pk.to_s }
|
429
|
-
# TODO: better handle quotes based on type
|
430
|
-
# (stringlike--> quote, int-like --> no quotes)
|
431
|
-
|
432
|
-
iuk = @pk_names.map { |pk| "#{pk}='?(\\w+)'?" }
|
433
|
-
@iuk_rgx = /\A#{iuk.join(',\s*')}\z/
|
434
|
-
|
435
|
-
iuk = @pk_names.map { |pk| "#{pk}='?\\w+'?" }
|
436
|
-
@entity_id_url_regexp = /\A\(\s*(#{iuk.join(',\s*')})\s*\)(.*)/
|
437
|
-
else
|
438
|
-
@pk_names = [primary_key.to_s]
|
439
|
-
@entity_id_url_regexp = SINGLE_PK_URL_REGEXP
|
144
|
+
def odata_post(req)
|
145
|
+
@modelk.odata_create_entity_and_relation(req)
|
440
146
|
end
|
441
147
|
end
|
442
148
|
|
443
|
-
|
444
|
-
|
445
|
-
cattr[:primary_key] ? nil : col
|
446
|
-
end.select { |col| col }
|
447
|
-
end
|
149
|
+
class NavigatedCollection < Collection
|
150
|
+
include Safrano::NavigationInfo
|
448
151
|
|
449
|
-
|
450
|
-
|
451
|
-
end
|
152
|
+
def initialize(childattrib, parent)
|
153
|
+
childklass = parent.class.nav_collection_attribs[childattrib]
|
452
154
|
|
453
|
-
|
454
|
-
|
455
|
-
def transition_attribute_regexp
|
456
|
-
# db_schema.map { |sch| sch[0] }.join('|')
|
457
|
-
# @columns is from Sequel Model
|
458
|
-
%r{\A/(#{@columns.join('|')})(.*)\z}
|
459
|
-
end
|
155
|
+
super(childklass)
|
156
|
+
@parent = parent
|
460
157
|
|
461
|
-
|
462
|
-
# type checking/convertion is done in check_odata_key_type
|
463
|
-
def find_by_odata_key(pkid)
|
464
|
-
# amazingly this works as expected from an Entity.get_related(...) anonymous class
|
465
|
-
# without need to redefine primary_key_lookup (returns nil for valid but unrelated keys)
|
466
|
-
primary_key_lookup(pkid)
|
467
|
-
end
|
158
|
+
set_relation_info(@parent, childattrib)
|
468
159
|
|
469
|
-
|
470
|
-
|
471
|
-
case type
|
472
|
-
when :integer
|
473
|
-
if (val =~ ONLY_INTEGER_RGX)
|
474
|
-
[true, Integer(val)]
|
475
|
-
else
|
476
|
-
[false, val]
|
477
|
-
end
|
478
|
-
when :string
|
479
|
-
[true, val]
|
480
|
-
else
|
481
|
-
[true, val] # todo...
|
482
|
-
end
|
483
|
-
rescue StandardError
|
484
|
-
[false, val]
|
485
|
-
end
|
160
|
+
@child_method = parent.method(childattrib.to_sym)
|
161
|
+
@child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
|
486
162
|
|
487
|
-
|
488
|
-
module Transitions
|
489
|
-
def transition_end(_match_result)
|
490
|
-
[nil, :end]
|
163
|
+
@cx = navigated_dataset
|
491
164
|
end
|
492
165
|
|
493
|
-
def
|
494
|
-
|
166
|
+
def odata_post(req)
|
167
|
+
@modelk.odata_create_entity_and_relation(req,
|
168
|
+
@navattr_reflection,
|
169
|
+
@nav_parent)
|
495
170
|
end
|
496
171
|
|
497
|
-
def
|
498
|
-
|
499
|
-
|
500
|
-
ck, casted_id = check_odata_key(id)
|
501
|
-
|
502
|
-
if ck
|
503
|
-
if (y = find_by_odata_key(casted_id))
|
504
|
-
[y, :run]
|
505
|
-
else
|
506
|
-
[nil, :error, ErrorNotFound]
|
507
|
-
end
|
508
|
-
else
|
509
|
-
[nil, :error, BadRequestError]
|
510
|
-
end
|
511
|
-
else
|
512
|
-
[nil, :error, ServerTransitionError]
|
513
|
-
end
|
172
|
+
def initialize_dataset(dtset = nil)
|
173
|
+
@cx = dtset || navigated_dataset
|
174
|
+
@uparms = UrlParameters4Coll.new(@cx, @params)
|
514
175
|
end
|
176
|
+
# redefinitions of the main methods for a navigated collection
|
177
|
+
# (eg. all Books of Author[2] is Author[2].Books.all )
|
515
178
|
|
516
|
-
def
|
517
|
-
|
518
|
-
Safrano::TransitionCount,
|
519
|
-
Safrano::Transition.new(entity_id_url_regexp,
|
520
|
-
trans: 'transition_id')]
|
179
|
+
def all
|
180
|
+
@child_method.call
|
521
181
|
end
|
522
|
-
end
|
523
|
-
include Transitions
|
524
|
-
end
|
525
182
|
|
526
|
-
|
527
|
-
|
528
|
-
include EntityClassBase
|
529
|
-
# input fx='aas',fy_w='0001'
|
530
|
-
# output true, ['aas', '0001'] ... or false when typ-error
|
531
|
-
def check_odata_key(mid)
|
532
|
-
# @iuk_rgx is (needs to be) built on start with
|
533
|
-
# collklass.prepare_pk
|
534
|
-
md = @iuk_rgx.match(mid).to_a
|
535
|
-
md.shift # remove first element which is the whole match
|
536
|
-
mdc = []
|
537
|
-
error = false
|
538
|
-
primary_key.each_with_index { |pk, i|
|
539
|
-
ck, casted = check_odata_val_type(md[i], db_schema[pk][:type])
|
540
|
-
if ck
|
541
|
-
mdc << casted
|
542
|
-
else
|
543
|
-
error = true
|
544
|
-
break
|
545
|
-
end
|
546
|
-
}
|
547
|
-
if error
|
548
|
-
[false, md]
|
549
|
-
else
|
550
|
-
[true, mdc]
|
183
|
+
def count
|
184
|
+
@child_method.call.count
|
551
185
|
end
|
552
|
-
end
|
553
|
-
end
|
554
186
|
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
def check_odata_key(id)
|
560
|
-
check_odata_val_type(id, db_schema[primary_key][:type])
|
561
|
-
end
|
562
|
-
end
|
187
|
+
def dataset
|
188
|
+
@child_dataset_method.call
|
189
|
+
end
|
563
190
|
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
# 1. Create and add entity from payload
|
568
|
-
# 2. Create relationship if needed
|
569
|
-
def odata_create_entity_and_relation(req, assoc, parent)
|
570
|
-
# TODO: this is for v2 only...
|
571
|
-
req.with_parsed_data do |data|
|
572
|
-
data.delete('__metadata')
|
573
|
-
# validate payload column names
|
574
|
-
if (invalid = invalid_hash_data?(data))
|
575
|
-
::OData::Request::ON_CGST_ERROR.call(req)
|
576
|
-
return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
|
577
|
-
end
|
191
|
+
def navigated_dataset
|
192
|
+
@child_dataset_method.call
|
193
|
+
end
|
578
194
|
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
else
|
584
|
-
# in-changeset requests get their own transaction
|
585
|
-
new_entity.save(transaction: !req.in_changeset)
|
586
|
-
end
|
587
|
-
req.register_content_id_ref(new_entity)
|
588
|
-
new_entity.copy_request_infos(req)
|
195
|
+
def each
|
196
|
+
y = @child_method.call
|
197
|
+
y.each { |enty| yield enty }
|
198
|
+
end
|
589
199
|
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
end
|
200
|
+
def to_a
|
201
|
+
y = @child_method.call
|
202
|
+
y.to_a
|
594
203
|
end
|
595
204
|
end
|
596
205
|
end
|