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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88f62ce611a5030382ea19c2e7c22b47f454b85c51261e5bad060d82e4680dff
|
4
|
+
data.tar.gz: '088e66ed4a107a3bef8dd6db68c8ca626f7d3acef6ed2e58eb53d871e6349613'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d719e317178a4baebc2e40cc50b95559921fe2936a6a03437f7f92714d173ef6160b10620ce5476ea54a7433b3dd1366b49ec37a48b121b8f808ad030e2c364
|
7
|
+
data.tar.gz: 535ee0f1d4fed30a0c94918c31149a2e95fce90051548a2ccca6bffb71930c0767543255695496c3623249070b5efb1b6a203a1dae6f868c1e4da9a126a85346
|
@@ -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
|
-
require_relative '../
|
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
|
@@ -57,9 +60,11 @@ module OData
|
|
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
|
+
ALLOWED_TRANSITIONS
|
63
68
|
end
|
64
69
|
end
|
65
70
|
include Transitions
|
data/lib/odata/batch.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../safrano/rack_app.rb'
|
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,561 +1,202 @@
|
|
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 'safrano_core.rb'
|
8
|
-
require 'odata/error.rb'
|
9
|
-
require 'odata/collection_filter.rb'
|
10
|
-
require 'odata/collection_order.rb'
|
11
|
-
require 'odata/url_parameters.rb'
|
12
|
-
require 'odata/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
|
-
# set to parent entity in case the collection is a nav.collection
|
57
|
-
# nil otherwise
|
58
|
-
attr_reader :nav_parent
|
59
|
-
|
60
|
-
attr_accessor :namespace
|
61
|
-
|
62
|
-
# dataset
|
63
|
-
attr_accessor :cx
|
64
|
-
|
65
|
-
# array of the objects --> dataset.to_a
|
66
|
-
attr_accessor :ax
|
67
|
-
|
68
|
-
# url params
|
69
|
-
attr_reader :params
|
70
|
-
|
71
|
-
# url parameters processing object (mostly covert to sequel exprs).
|
72
|
-
# exposed for testing only
|
73
|
-
attr_reader :uparms
|
74
|
-
|
75
|
-
# initialising block of code to be executed at end of
|
76
|
-
# ServerApp.publish_service after all model classes have been registered
|
77
|
-
# (without the associations/relationships)
|
78
|
-
# typically the block should contain the publication of the associations
|
79
|
-
attr_accessor :deferred_iblock
|
80
|
-
|
81
|
-
# convention: entityType is the Ruby Model class --> name is just to_s
|
82
|
-
# Warning: for handling Navigation relations, we use anonymous collection classes
|
83
|
-
# dynamically subtyped from a Model class, and in such an anonymous class
|
84
|
-
# the class-name is not the OData Type. In these subclass we redefine "type_name"
|
85
|
-
# thus when we need the Odata type name, we shall use this method instead of just the collection class name
|
86
|
-
def type_name
|
87
|
-
to_s
|
88
|
-
end
|
5
|
+
module Safrano
|
6
|
+
module OData
|
7
|
+
class Collection
|
8
|
+
attr_accessor :cx
|
89
9
|
|
90
|
-
|
91
|
-
|
92
|
-
@entity_set_name = (@entity_set_name || type_name)
|
93
|
-
end
|
10
|
+
# url params
|
11
|
+
attr_reader :params
|
94
12
|
|
95
|
-
|
96
|
-
#
|
97
|
-
|
98
|
-
@entity_set_name = nil
|
99
|
-
@uparms = nil
|
100
|
-
@params = nil
|
101
|
-
@ax = nil
|
102
|
-
@cx = nil
|
103
|
-
end
|
13
|
+
# url parameters processing object (mostly convert to sequel exprs).
|
14
|
+
# exposed for testing only
|
15
|
+
attr_reader :uparms
|
104
16
|
|
105
|
-
|
106
|
-
instance_eval { @deferred_iblock.call } if @deferred_iblock
|
107
|
-
end
|
17
|
+
attr_reader :inlinecount
|
108
18
|
|
109
|
-
|
110
|
-
def new_from_hson_h(hash)
|
111
|
-
enty = new
|
112
|
-
enty.set_fields(hash, @data_fields, missing: :skip)
|
113
|
-
enty
|
114
|
-
end
|
19
|
+
attr_reader :modelk
|
115
20
|
|
116
|
-
|
117
|
-
|
118
|
-
case assoc[:type]
|
119
|
-
when :one_to_many, :one_to_one
|
120
|
-
OData.create_nav_relation(new_entity, assoc, parent)
|
121
|
-
new_entity.save(transaction: false)
|
122
|
-
when :many_to_one
|
123
|
-
new_entity.save(transaction: false)
|
124
|
-
OData.create_nav_relation(new_entity, assoc, parent)
|
125
|
-
parent.save(transaction: false)
|
126
|
-
else
|
127
|
-
# not supported
|
21
|
+
def initialize(modelk)
|
22
|
+
@modelk = modelk
|
128
23
|
end
|
129
|
-
end
|
130
|
-
def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
131
|
-
if req.in_changeset
|
132
|
-
# in-changeset requests get their own transaction
|
133
|
-
CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
|
134
|
-
else
|
135
|
-
db.transaction do
|
136
|
-
CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def odata_get_inlinecount_w_sequel
|
142
|
-
return unless (icp = @params['$inlinecount'])
|
143
24
|
|
144
|
-
|
145
|
-
|
146
|
-
@cx.count
|
147
|
-
else
|
148
|
-
@cx.dataset.count
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def attrib_path_valid?(path)
|
154
|
-
@attribute_path_list.include? path
|
155
|
-
end
|
156
|
-
|
157
|
-
def odata_get_apply_params
|
158
|
-
begin
|
159
|
-
@cx = @uparms.apply_to_dataset(@cx)
|
160
|
-
rescue OData::Filter::Parser::ErrorWrongColumnName
|
161
|
-
@error = BadRequestFilterParseError
|
162
|
-
return
|
163
|
-
rescue OData::Filter::Parser::ErrorFunctionArgumentType
|
164
|
-
@error = BadRequestFilterParseError
|
165
|
-
return
|
25
|
+
def allowed_transitions
|
26
|
+
@modelk.allowed_transitions
|
166
27
|
end
|
167
|
-
odata_get_inlinecount_w_sequel
|
168
28
|
|
169
|
-
|
170
|
-
|
171
|
-
@cx
|
172
|
-
end
|
173
|
-
|
174
|
-
# url params validation methods.
|
175
|
-
# nil is the expected return for no errors
|
176
|
-
# an error class is returned in case of errors
|
177
|
-
# this way we can combine multiple validation calls with logical ||
|
178
|
-
def check_u_p_top
|
179
|
-
return unless @params['$top']
|
180
|
-
|
181
|
-
itop = number_or_nil(@params['$top'])
|
182
|
-
return BadRequestError if itop.nil? || itop.zero?
|
183
|
-
end
|
184
|
-
|
185
|
-
def check_u_p_skip
|
186
|
-
return unless @params['$skip']
|
187
|
-
|
188
|
-
iskip = number_or_nil(@params['$skip'])
|
189
|
-
return BadRequestError if iskip.nil? || iskip.negative?
|
190
|
-
end
|
191
|
-
|
192
|
-
def check_u_p_inlinecount
|
193
|
-
return unless (icp = @params['$inlinecount'])
|
194
|
-
|
195
|
-
unless (icp == 'allpages') || (icp == 'none')
|
196
|
-
return BadRequestInlineCountParamError
|
29
|
+
def transition_end(_match_result)
|
30
|
+
Safrano::Transition::RESULT_END
|
197
31
|
end
|
198
32
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
def check_u_p_filter
|
203
|
-
@uparms.check_filter
|
204
|
-
end
|
205
|
-
|
206
|
-
def check_u_p_orderby
|
207
|
-
@uparms.check_order
|
208
|
-
end
|
209
|
-
|
210
|
-
def build_attribute_path_list
|
211
|
-
@attribute_path_list = attribute_path_list
|
212
|
-
@attrib_paths_url_regexp = @attribute_path_list.join('|')
|
213
|
-
end
|
214
|
-
|
215
|
-
def attribute_path_list(nodes = Set.new)
|
216
|
-
# break circles
|
217
|
-
return [] if nodes.include?(entity_set_name)
|
218
|
-
|
219
|
-
ret = @columns.map(&:to_s)
|
220
|
-
nodes.add entity_set_name
|
221
|
-
if @nav_entity_attribs
|
222
|
-
@nav_entity_attribs.each do |a, k|
|
223
|
-
ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
|
224
|
-
end
|
225
|
-
end
|
226
|
-
if @nav_collection_attribs
|
227
|
-
@nav_collection_attribs.each do |a, k|
|
228
|
-
ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
|
229
|
-
end
|
33
|
+
def transition_count(_match_result)
|
34
|
+
[self, :end_with_count]
|
230
35
|
end
|
231
|
-
ret
|
232
|
-
end
|
233
|
-
|
234
|
-
def check_url_params
|
235
|
-
return nil unless @params
|
236
36
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
@cx = self
|
243
|
-
@ax = nil
|
244
|
-
@cx = navigated_dataset if @cx.nav_parent
|
245
|
-
@model = if @cx.respond_to? :model
|
246
|
-
@cx.model
|
247
|
-
else
|
248
|
-
@cx
|
249
|
-
end
|
250
|
-
@jh = @model.join_by_paths_helper
|
251
|
-
@uparms = UrlParameters.new(@jh, @params)
|
252
|
-
end
|
253
|
-
|
254
|
-
# finally return the requested output according to format, options etc
|
255
|
-
def odata_get_output(req)
|
256
|
-
return @error.odata_get(req) if @error
|
37
|
+
###########################################################
|
38
|
+
# this is redefined in NavigatedCollection
|
39
|
+
def dataset
|
40
|
+
@modelk.dataset
|
41
|
+
end
|
257
42
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
263
50
|
else
|
264
|
-
|
51
|
+
Safrano::Transition::RESULT_SERVER_TR_ERR
|
265
52
|
end
|
266
|
-
else # TODO: other formats
|
267
|
-
406
|
268
53
|
end
|
269
|
-
end
|
270
54
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
if (@error = check_url_params)
|
278
|
-
@error.odata_get(req)
|
279
|
-
else
|
280
|
-
odata_get_apply_params
|
281
|
-
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]
|
282
60
|
end
|
283
|
-
end
|
284
|
-
|
285
|
-
def odata_post(req)
|
286
|
-
odata_create_entity_and_relation(req, @navattr_reflection, @nav_parent)
|
287
|
-
end
|
288
61
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
# with their properties
|
293
|
-
db_schema.each do |pnam, prop|
|
294
|
-
if prop[:primary_key] == true
|
295
|
-
enty.add_element('Key').add_element('PropertyRef',
|
296
|
-
'Name' => pnam.to_s)
|
297
|
-
end
|
298
|
-
attrs = { 'Name' => pnam.to_s,
|
299
|
-
'Type' => OData.get_edm_type(db_type: prop[:db_type]) }
|
300
|
-
attrs['Nullable'] = 'false' if prop[:allow_null] == false
|
301
|
-
enty.add_element('Property', attrs)
|
62
|
+
def initialize_dataset(dtset = nil)
|
63
|
+
@cx = dtset || @modelk
|
64
|
+
@uparms = UrlParameters4Coll.new(@cx, @params)
|
302
65
|
end
|
303
|
-
enty
|
304
|
-
end
|
305
66
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
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']
|
315
75
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
# associated objects need to be in the map...
|
320
|
-
next unless cmap[assoc.to_s]
|
76
|
+
@cx
|
77
|
+
end
|
78
|
+
end
|
321
79
|
|
322
|
-
|
80
|
+
D = 'd'.freeze
|
81
|
+
DJ_OPEN = '{"d":'.freeze
|
82
|
+
DJ_CLOSE = '}'.freeze
|
83
|
+
EMPTYH = {}.freeze
|
323
84
|
|
324
|
-
|
85
|
+
def to_odata_json(request:)
|
86
|
+
template = @modelk.output_template(expand_list: @uparms.expand.template,
|
87
|
+
select: @uparms.select)
|
88
|
+
innerj = request.service.get_coll_odata_h(array: @cx.all,
|
89
|
+
template: template,
|
90
|
+
icount: @inlinecount).to_json
|
91
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
325
92
|
end
|
326
|
-
end
|
327
93
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
uribase: @uribase,
|
334
|
-
icount: @inlinecount).to_json
|
335
|
-
"#{DJopen}#{innerj}#{DJclose}"
|
336
|
-
end
|
337
|
-
|
338
|
-
def to_odata_json(service:)
|
339
|
-
innerj = service.get_coll_odata_h(array: get_a,
|
340
|
-
expand: @params['$expand'],
|
341
|
-
uribase: @uribase,
|
342
|
-
icount: @inlinecount).to_json
|
343
|
-
"#{DJopen}#{innerj}#{DJclose}"
|
344
|
-
end
|
94
|
+
def to_odata_links_json(service:)
|
95
|
+
innerj = service.get_coll_odata_links_h(array: @cx.all,
|
96
|
+
icount: @inlinecount).to_json
|
97
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
98
|
+
end
|
345
99
|
|
346
|
-
|
347
|
-
|
348
|
-
end
|
100
|
+
def odata_get_inlinecount_w_sequel
|
101
|
+
return unless (icp = @params['$inlinecount'])
|
349
102
|
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
a[:name] == assoc_symb && a[:model] == self
|
103
|
+
@inlinecount = if icp == 'allpages'
|
104
|
+
if @modelk.is_a? Sequel::Model::ClassMethods
|
105
|
+
@cx.count
|
106
|
+
else
|
107
|
+
@cx.dataset.count
|
108
|
+
end
|
109
|
+
end
|
358
110
|
end
|
359
|
-
|
360
|
-
|
111
|
+
|
112
|
+
# finally return the requested output according to format, options etc
|
113
|
+
def odata_get_output(req)
|
114
|
+
output = if req.walker.do_count
|
115
|
+
[200, CT_TEXT, @cx.count.to_s]
|
116
|
+
elsif req.accept?(APPJSON)
|
117
|
+
# json is default content type so we dont need to specify it here again
|
118
|
+
if req.walker.do_links
|
119
|
+
[200, EMPTYH, [to_odata_links_json(service: req.service)]]
|
120
|
+
else
|
121
|
+
[200, EMPTYH, [to_odata_json(request: req)]]
|
122
|
+
end
|
123
|
+
else # TODO: other formats
|
124
|
+
406
|
125
|
+
end
|
126
|
+
Contract.valid(output)
|
361
127
|
end
|
362
128
|
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
end
|
129
|
+
# on model class level we return the collection
|
130
|
+
def odata_get(req)
|
131
|
+
@params = req.params
|
132
|
+
initialize_dataset
|
368
133
|
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
a[:name] == assoc_symb && a[:model] == self
|
375
|
-
end
|
376
|
-
unless assoc
|
377
|
-
raise OData::API::ModelAssociationNameError.new(self, assoc_symb)
|
134
|
+
@uparms.check_all.if_valid { |_ret|
|
135
|
+
odata_get_apply_params.if_valid { |_ret|
|
136
|
+
odata_get_output(req)
|
137
|
+
}
|
138
|
+
}.tap_error { |e| return e.odata_get(req) }.result
|
378
139
|
end
|
379
140
|
|
380
|
-
|
381
|
-
|
382
|
-
@nav_entity_attribs[lattr_name_str] = attr_class
|
383
|
-
@nav_entity_url_regexp = @nav_entity_attribs.keys.join('|')
|
384
|
-
end
|
385
|
-
|
386
|
-
# old names...
|
387
|
-
# alias_method :add_nav_prop_collection, :addNavCollectionAttrib
|
388
|
-
# alias_method :add_nav_prop_single, :addNavEntityAttrib
|
389
|
-
|
390
|
-
def prepare_pk
|
391
|
-
if primary_key.is_a? Array
|
392
|
-
@pk_names = []
|
393
|
-
primary_key.each { |pk| @pk_names << pk.to_s }
|
394
|
-
# TODO: better handle quotes based on type
|
395
|
-
# (stringlike--> quote, int-like --> no quotes)
|
396
|
-
|
397
|
-
iuk = @pk_names.map { |pk| "#{pk}='?(\\w+)'?" }
|
398
|
-
@iuk_rgx = /\A#{iuk.join(',\s*')}\z/
|
399
|
-
|
400
|
-
iuk = @pk_names.map { |pk| "#{pk}='?\\w+'?" }
|
401
|
-
@entity_id_url_regexp = /\A\(\s*(#{iuk.join(',\s*')})\s*\)(.*)/
|
402
|
-
else
|
403
|
-
@pk_names = [primary_key.to_s]
|
404
|
-
@entity_id_url_regexp = SINGLE_PK_URL_REGEXP
|
141
|
+
def odata_post(req)
|
142
|
+
@modelk.odata_create_entity_and_relation(req)
|
405
143
|
end
|
406
144
|
end
|
407
145
|
|
408
|
-
|
409
|
-
|
410
|
-
cattr[:primary_key] ? nil : col
|
411
|
-
end.select { |col| col }
|
412
|
-
end
|
146
|
+
class NavigatedCollection < Collection
|
147
|
+
include Safrano::NavigationInfo
|
413
148
|
|
414
|
-
|
415
|
-
|
416
|
-
end
|
149
|
+
def initialize(childattrib, parent)
|
150
|
+
childklass = parent.class.nav_collection_attribs[childattrib]
|
417
151
|
|
418
|
-
|
419
|
-
|
420
|
-
def transition_attribute_regexp
|
421
|
-
# db_schema.map { |sch| sch[0] }.join('|')
|
422
|
-
# @columns is from Sequel Model
|
423
|
-
%r{\A/(#{@columns.join('|')})(.*)\z}
|
424
|
-
end
|
152
|
+
super(childklass)
|
153
|
+
@parent = parent
|
425
154
|
|
426
|
-
|
427
|
-
# type checking/convertion is done in check_odata_key_type
|
428
|
-
def find_by_odata_key(pkid)
|
429
|
-
# amazingly this works as expected from an Entity.get_related(...) anonymous class
|
430
|
-
# without need to redefine primary_key_lookup (returns nil for valid but unrelated keys)
|
431
|
-
primary_key_lookup(pkid)
|
432
|
-
end
|
155
|
+
set_relation_info(@parent, childattrib)
|
433
156
|
|
434
|
-
|
435
|
-
|
436
|
-
case type
|
437
|
-
when :integer
|
438
|
-
if (val =~ ONLY_INTEGER_RGX)
|
439
|
-
[true, Integer(val)]
|
440
|
-
else
|
441
|
-
[false, val]
|
442
|
-
end
|
443
|
-
when :string
|
444
|
-
[true, val]
|
445
|
-
else
|
446
|
-
[true, val] # todo...
|
447
|
-
end
|
448
|
-
rescue StandardError
|
449
|
-
[false, val]
|
450
|
-
end
|
157
|
+
@child_method = parent.method(childattrib.to_sym)
|
158
|
+
@child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
|
451
159
|
|
452
|
-
|
453
|
-
module Transitions
|
454
|
-
def transition_end(_match_result)
|
455
|
-
[nil, :end]
|
160
|
+
@cx = navigated_dataset
|
456
161
|
end
|
457
162
|
|
458
|
-
def
|
459
|
-
|
163
|
+
def odata_post(req)
|
164
|
+
@modelk.odata_create_entity_and_relation(req,
|
165
|
+
@navattr_reflection,
|
166
|
+
@nav_parent)
|
460
167
|
end
|
461
168
|
|
462
|
-
def
|
463
|
-
|
464
|
-
|
465
|
-
ck, casted_id = check_odata_key(id)
|
466
|
-
|
467
|
-
if ck
|
468
|
-
if (y = find_by_odata_key(casted_id))
|
469
|
-
[y, :run]
|
470
|
-
else
|
471
|
-
[nil, :error, ErrorNotFound]
|
472
|
-
end
|
473
|
-
else
|
474
|
-
[nil, :error, BadRequestError]
|
475
|
-
end
|
476
|
-
else
|
477
|
-
[nil, :error, ServerTransitionError]
|
478
|
-
end
|
169
|
+
def initialize_dataset(dtset = nil)
|
170
|
+
@cx = dtset || navigated_dataset
|
171
|
+
@uparms = UrlParameters4Coll.new(@cx, @params)
|
479
172
|
end
|
173
|
+
# redefinitions of the main methods for a navigated collection
|
174
|
+
# (eg. all Books of Author[2] is Author[2].Books.all )
|
480
175
|
|
481
|
-
def
|
482
|
-
|
483
|
-
Safrano::TransitionCount,
|
484
|
-
Safrano::Transition.new(entity_id_url_regexp,
|
485
|
-
trans: 'transition_id')]
|
176
|
+
def all
|
177
|
+
@child_method.call
|
486
178
|
end
|
487
|
-
end
|
488
|
-
include Transitions
|
489
|
-
end
|
490
179
|
|
491
|
-
|
492
|
-
|
493
|
-
include EntityClassBase
|
494
|
-
# input fx='aas',fy_w='0001'
|
495
|
-
# output true, ['aas', '0001'] ... or false when typ-error
|
496
|
-
def check_odata_key(mid)
|
497
|
-
# @iuk_rgx is (needs to be) built on start with
|
498
|
-
# collklass.prepare_pk
|
499
|
-
md = @iuk_rgx.match(mid).to_a
|
500
|
-
md.shift # remove first element which is the whole match
|
501
|
-
mdc = []
|
502
|
-
error = false
|
503
|
-
primary_key.each_with_index { |pk, i|
|
504
|
-
ck, casted = check_odata_val_type(md[i], db_schema[pk][:type])
|
505
|
-
if ck
|
506
|
-
mdc << casted
|
507
|
-
else
|
508
|
-
error = true
|
509
|
-
break
|
510
|
-
end
|
511
|
-
}
|
512
|
-
if error
|
513
|
-
[false, md]
|
514
|
-
else
|
515
|
-
[true, mdc]
|
180
|
+
def count
|
181
|
+
@child_method.call.count
|
516
182
|
end
|
517
|
-
end
|
518
|
-
end
|
519
183
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
def check_odata_key(id)
|
525
|
-
check_odata_val_type(id, db_schema[primary_key][:type])
|
526
|
-
end
|
527
|
-
end
|
184
|
+
def dataset
|
185
|
+
@child_dataset_method.call
|
186
|
+
end
|
528
187
|
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
# 1. Create and add entity from payload
|
533
|
-
# 2. Create relationship if needed
|
534
|
-
def odata_create_entity_and_relation(req, assoc, parent)
|
535
|
-
# TODO: this is for v2 only...
|
536
|
-
req.with_parsed_data do |data|
|
537
|
-
data.delete('__metadata')
|
538
|
-
# validate payload column names
|
539
|
-
if (invalid = invalid_hash_data?(data))
|
540
|
-
::OData::Request::ON_CGST_ERROR.call(req)
|
541
|
-
return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
|
542
|
-
end
|
188
|
+
def navigated_dataset
|
189
|
+
@child_dataset_method.call
|
190
|
+
end
|
543
191
|
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
else
|
549
|
-
# in-changeset requests get their own transaction
|
550
|
-
new_entity.save(transaction: !req.in_changeset)
|
551
|
-
end
|
552
|
-
req.register_content_id_ref(new_entity)
|
553
|
-
new_entity.copy_request_infos(req)
|
192
|
+
def each
|
193
|
+
y = @child_method.call
|
194
|
+
y.each { |enty| yield enty }
|
195
|
+
end
|
554
196
|
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
end
|
197
|
+
def to_a
|
198
|
+
y = @child_method.call
|
199
|
+
y.to_a
|
559
200
|
end
|
560
201
|
end
|
561
202
|
end
|