safrano 0.0.1 → 0.0.2
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/odata/batch.rb +190 -0
- data/lib/odata/collection.rb +408 -0
- data/lib/odata/collection_filter.rb +447 -0
- data/lib/odata/collection_order.rb +91 -0
- data/lib/odata/entity.rb +267 -0
- data/lib/odata/error.rb +86 -0
- data/lib/odata/relations.rb +79 -0
- data/lib/odata/walker.rb +100 -0
- metadata +24 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d62d07c9e0df09819503c296c06aad10d160915649158c3e8aff15368880e2a4
|
4
|
+
data.tar.gz: 9666f85a2826483709ed963fe6a48fef768b5fe78c3adef504d2b8c412f86703
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8f61683c7bbdb3f8f45aa70e760d60b02e7acfca8ee6027d869f33f5fa16b475d85a04b549f132506e7653b064658947f8b6e8cdc2bdc1dea8fb992699e0155
|
7
|
+
data.tar.gz: 682bc1b1bba6e68c3f2855f6242aa3119883bbf1baacab74e90ee4671d5e827508eaad1d55eda9f596bcedfdeff5953a5c0e09599c094bd53b1d5e6dcfa2ffa9
|
data/lib/odata/batch.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rack_app.rb'
|
4
|
+
require 'safrano_core.rb'
|
5
|
+
|
6
|
+
module OData
|
7
|
+
module Batch
|
8
|
+
# Mayonaise
|
9
|
+
class MyOApp < OData::ServerApp
|
10
|
+
attr_writer :full_req
|
11
|
+
|
12
|
+
def batch_call(env)
|
13
|
+
@request = OData::Request.new(env)
|
14
|
+
@response = OData::Response.new
|
15
|
+
|
16
|
+
dispatch
|
17
|
+
|
18
|
+
@response.finish
|
19
|
+
end
|
20
|
+
|
21
|
+
def batch_body
|
22
|
+
b = ''
|
23
|
+
@response.headers.each { |k, v| b << k.to_s << ':' << v.to_s << "\r\n" }
|
24
|
+
b << "\r\n"
|
25
|
+
b << @response.body[0].to_s
|
26
|
+
b << "\r\n"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Moutarde
|
31
|
+
class RequestPartBuilder
|
32
|
+
attr_accessor :body_str
|
33
|
+
attr_accessor :odata_headers
|
34
|
+
attr_accessor :headers
|
35
|
+
attr_accessor :http_method
|
36
|
+
attr_accessor :uri
|
37
|
+
attr_accessor :env
|
38
|
+
def initialize
|
39
|
+
@headers = {}
|
40
|
+
@odata_headers = {}
|
41
|
+
@body_str = ''
|
42
|
+
end
|
43
|
+
|
44
|
+
# shamelessely copied from Rack::TEST:Session
|
45
|
+
def headers_for_env
|
46
|
+
converted_headers = {}
|
47
|
+
|
48
|
+
@headers.each do |name, value|
|
49
|
+
env_key = name.upcase.tr('-', '_')
|
50
|
+
env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
|
51
|
+
converted_headers[env_key] = value
|
52
|
+
end
|
53
|
+
|
54
|
+
converted_headers
|
55
|
+
end
|
56
|
+
|
57
|
+
def batch_env
|
58
|
+
@env = ::Rack::MockRequest.env_for(@uri,
|
59
|
+
method: @http_method,
|
60
|
+
input: @body_str)
|
61
|
+
@env.merge! headers_for_env
|
62
|
+
@env
|
63
|
+
end
|
64
|
+
end
|
65
|
+
# Huile d'olive extra
|
66
|
+
class HandlerBase
|
67
|
+
TREND = Safrano::Transition.new('', trans: 'transition_end')
|
68
|
+
def allowed_transitions
|
69
|
+
@allowed_transitions = [TREND]
|
70
|
+
end
|
71
|
+
|
72
|
+
def transition_end(_match_result)
|
73
|
+
[nil, :end]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
# jaune d'oeuf
|
77
|
+
class DisabledHandler < HandlerBase
|
78
|
+
def odata_post(_req)
|
79
|
+
[404, {}, '$batch is not enabled ']
|
80
|
+
end
|
81
|
+
|
82
|
+
def odata_get(_req)
|
83
|
+
[404, {}, '$batch is not enabled ']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
# battre le tout
|
87
|
+
class EnabledHandler < HandlerBase
|
88
|
+
HTTP_VERB_URI_REGEXP = /^(POST|GET|PUT|PATCH)\s+(\S*)\s?(\S*)$/.freeze
|
89
|
+
HEADER_REGEXP = %r{^([a-zA-Z\-]+):\s*([0-9a-zA-Z\-\/]+;?\S*)\s*$}.freeze
|
90
|
+
attr_accessor :boundary
|
91
|
+
attr_accessor :mmboundary
|
92
|
+
attr_accessor :body_str
|
93
|
+
attr_accessor :parts
|
94
|
+
attr_accessor :request
|
95
|
+
|
96
|
+
def initialize
|
97
|
+
@parts = []
|
98
|
+
end
|
99
|
+
|
100
|
+
def odata_post(req)
|
101
|
+
odata_batch(req)
|
102
|
+
end
|
103
|
+
|
104
|
+
def odata_get(_req)
|
105
|
+
[405, {}, 'You cant GET $batch, POST it ']
|
106
|
+
end
|
107
|
+
|
108
|
+
def odata_batch(req)
|
109
|
+
@request = req
|
110
|
+
if @request.media_type == 'multipart/mixed'
|
111
|
+
read
|
112
|
+
parse
|
113
|
+
[202,
|
114
|
+
{ 'Content-Type' => "multipart/mixed;boundary=#{@boundary}" },
|
115
|
+
process_parts]
|
116
|
+
else
|
117
|
+
[415, {}, 'Unsupported Media Type']
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def read
|
122
|
+
@body_str = @request.body.read
|
123
|
+
@boundary = @request.media_type_params['boundary']
|
124
|
+
@mmboundary = '--' << @boundary
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse
|
128
|
+
body_str.split(@mmboundary).each do |p|
|
129
|
+
@parts << parse_part(p) unless (p == '--\n') || p == '--' || p.empty?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Ultrasimplified parser...
|
134
|
+
def parse_part(inp)
|
135
|
+
state = :start
|
136
|
+
part = RequestPartBuilder.new
|
137
|
+
inp.each_line do |l|
|
138
|
+
next if l.strip.empty?
|
139
|
+
|
140
|
+
case state
|
141
|
+
when :start, :ohd
|
142
|
+
md = l.strip.match(HEADER_REGEXP)
|
143
|
+
|
144
|
+
if md
|
145
|
+
state = :ohd
|
146
|
+
part.odata_headers[md[1]] = md[2]
|
147
|
+
else
|
148
|
+
md = l.strip.match(HTTP_VERB_URI_REGEXP)
|
149
|
+
if md
|
150
|
+
state = :hd
|
151
|
+
part.http_method = md[1]
|
152
|
+
part.uri = md[2]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
when :hd
|
156
|
+
md = l.strip.match(HEADER_REGEXP)
|
157
|
+
if md
|
158
|
+
state = :hd
|
159
|
+
part.headers[md[1]] = md[2]
|
160
|
+
else
|
161
|
+
part.body_str << l
|
162
|
+
state = :bd
|
163
|
+
end
|
164
|
+
|
165
|
+
when :bd
|
166
|
+
part.body_str << l
|
167
|
+
end
|
168
|
+
end
|
169
|
+
part
|
170
|
+
end
|
171
|
+
|
172
|
+
def process_parts
|
173
|
+
full_batch_body = ''
|
174
|
+
@parts.each do |p|
|
175
|
+
next unless p.uri
|
176
|
+
|
177
|
+
batcha = MyOApp.new
|
178
|
+
|
179
|
+
batcha.full_req = @request
|
180
|
+
|
181
|
+
batcha.batch_call(p.batch_env)
|
182
|
+
|
183
|
+
full_batch_body << @mmboundary << "\r\n"
|
184
|
+
full_batch_body << batcha.batch_body
|
185
|
+
end
|
186
|
+
full_batch_body << @mmboundary << "--\r\n"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,408 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Design: Collections are nothing more as Sequel based model classes that have
|
3
|
+
# somehow the character of an array (Enumerable)
|
4
|
+
# Thus Below we have called that "EntityClass". It's meant as "Collection"
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
require 'pp'
|
8
|
+
require 'pry'
|
9
|
+
require 'rexml/document'
|
10
|
+
require 'safrano_core.rb'
|
11
|
+
require 'odata/error.rb'
|
12
|
+
require 'odata/collection_filter.rb'
|
13
|
+
require 'odata/collection_order.rb'
|
14
|
+
|
15
|
+
# small helper method
|
16
|
+
# http://stackoverflow.com/
|
17
|
+
# questions/24980295/strictly-convert-string-to-integer-or-nil
|
18
|
+
def number_or_nil(str)
|
19
|
+
num = str.to_i
|
20
|
+
num if num.to_s == str
|
21
|
+
end
|
22
|
+
|
23
|
+
# another helper
|
24
|
+
# thanks https://stackoverflow.com/questions/1448670/ruby-stringto-class
|
25
|
+
class String
|
26
|
+
def constantize
|
27
|
+
names = split('::')
|
28
|
+
names.shift if names.empty? || names.first.empty?
|
29
|
+
|
30
|
+
const = Object
|
31
|
+
names.each do |name|
|
32
|
+
const = if const.const_defined?(name)
|
33
|
+
const.const_get(name)
|
34
|
+
else
|
35
|
+
const.const_missing(name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
const
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module OData
|
43
|
+
# class methods. They Make heavy use of Sequel::Model functionality
|
44
|
+
# we will add this to our Model classes with "extend" --> self is the Class
|
45
|
+
module EntityClassBase
|
46
|
+
attr_reader :nav_collection_url_regexp
|
47
|
+
attr_reader :nav_entity_url_regexp
|
48
|
+
attr_reader :entity_id_url_regexp
|
49
|
+
attr_reader :attrib_paths_url_regexp
|
50
|
+
attr_reader :nav_collection_attribs
|
51
|
+
attr_reader :nav_entity_attribs
|
52
|
+
attr_reader :data_fields
|
53
|
+
|
54
|
+
attr_accessor :namespace
|
55
|
+
|
56
|
+
# dataset
|
57
|
+
attr_accessor :cx
|
58
|
+
|
59
|
+
# array of the objects --> dataset.to_a
|
60
|
+
attr_accessor :ax
|
61
|
+
|
62
|
+
# url params
|
63
|
+
attr_reader :params
|
64
|
+
|
65
|
+
# initialising block of code to be executed at end of
|
66
|
+
# ServerApp.publish_service after all model classes have been registered
|
67
|
+
# (without the associations/relationships)
|
68
|
+
# typically the block should contain the publication of the associations
|
69
|
+
attr_accessor :deferred_iblock
|
70
|
+
|
71
|
+
# convention: entityType is the Ruby Model class --> name is just to_s
|
72
|
+
def type_name
|
73
|
+
to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
# convention: default for entity_set_name is the model table name
|
77
|
+
def entity_set_name
|
78
|
+
@entity_set_name = (@entity_set_name || table_name.to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
def execute_deferred_iblock
|
82
|
+
instance_eval { @deferred_iblock.call } if @deferred_iblock
|
83
|
+
end
|
84
|
+
|
85
|
+
# Factory json-> Model Object instance
|
86
|
+
def new_from_hson_h(hash)
|
87
|
+
enty = new
|
88
|
+
hash.delete('__metadata')
|
89
|
+
# DONE: move this somewhere else where it's evaluated only once at setup
|
90
|
+
#data_fields = db_schema.map do |col, cattr|
|
91
|
+
# cattr[:primary_key] ? nil : col
|
92
|
+
#end.select { |col| col }
|
93
|
+
enty.set_fields(hash, @data_fields, missing: :skip)
|
94
|
+
enty.save
|
95
|
+
enty
|
96
|
+
end
|
97
|
+
|
98
|
+
def odata_get_apply_filter_w_sequel
|
99
|
+
return unless @params['$filter']
|
100
|
+
|
101
|
+
# Sequel requires a dataset to build some sql expressions, so we
|
102
|
+
# need to pass one todo: use a dummy one ?
|
103
|
+
# @fi = Filter.new_by_parse(@params['$filter'], @cx)
|
104
|
+
return if @fi.parse_error?
|
105
|
+
|
106
|
+
@cx = @fi.apply_to_dataset(@cx)
|
107
|
+
@right_assocs.merge @fi.assocs
|
108
|
+
end
|
109
|
+
|
110
|
+
def odata_get_apply_order_w_sequel
|
111
|
+
return unless @params['$orderby']
|
112
|
+
|
113
|
+
fo = Order.new_by_parse(@params['$orderby'], @cx)
|
114
|
+
@cx = fo.apply_to_dataset(@cx)
|
115
|
+
@left_assocs.merge fo.assocs
|
116
|
+
end
|
117
|
+
|
118
|
+
def odata_get_do_assoc_joins_w_sequel
|
119
|
+
return if @right_assocs.empty? && @left_assocs.empty?
|
120
|
+
|
121
|
+
# Preparation: ensure that the left and right assocs sets are disjoint
|
122
|
+
# by keeping the duplicates in the left assoc set and removing
|
123
|
+
# them from the right assocs set. We can use Set difference...
|
124
|
+
@right_assocs -= @left_assocs
|
125
|
+
|
126
|
+
# this is for the filtering. Normally we can use inner join
|
127
|
+
@right_assocs.each { |aj| @cx = @cx.association_join(aj) }
|
128
|
+
# this is for the ordering.
|
129
|
+
# we need left join otherwise we could miss records sometimes
|
130
|
+
@left_assocs.each { |aj| @cx = @cx.association_left_join(aj) }
|
131
|
+
@cx = @cx.select_all(entity_set_name.to_sym)
|
132
|
+
end
|
133
|
+
|
134
|
+
def odata_get_apply_params_w_sequel
|
135
|
+
@left_assocs = Set.new
|
136
|
+
@right_assocs = Set.new
|
137
|
+
odata_get_apply_filter_w_sequel
|
138
|
+
odata_get_apply_order_w_sequel
|
139
|
+
odata_get_do_assoc_joins_w_sequel
|
140
|
+
|
141
|
+
@cx = @cx.offset(@params['$skip']) if @params['$skip']
|
142
|
+
@cx = @cx.limit(@params['$top']) if @params['$top']
|
143
|
+
@cx
|
144
|
+
end
|
145
|
+
|
146
|
+
def navigated_coll
|
147
|
+
false
|
148
|
+
end
|
149
|
+
|
150
|
+
def odata_get_apply_params
|
151
|
+
odata_get_apply_params_w_sequel
|
152
|
+
end
|
153
|
+
|
154
|
+
# url params validation methods.
|
155
|
+
# nil is the expected return for no errors
|
156
|
+
# an error class is returned in case of errors
|
157
|
+
# this way we can combine multiple validation calls with logical ||
|
158
|
+
def check_u_p_top
|
159
|
+
return unless @params['$top']
|
160
|
+
|
161
|
+
itop = number_or_nil(@params['$top'])
|
162
|
+
return BadRequestError if itop.nil? || itop.zero?
|
163
|
+
end
|
164
|
+
|
165
|
+
def check_u_p_skip
|
166
|
+
return unless @params['$skip']
|
167
|
+
|
168
|
+
iskip = number_or_nil(@params['$skip'])
|
169
|
+
return BadRequestError if iskip.nil? || (iskip < 0)
|
170
|
+
end
|
171
|
+
|
172
|
+
def check_u_p_filter
|
173
|
+
return unless @params['$filter']
|
174
|
+
|
175
|
+
# Sequel requires a dataset to build some sql expressions, so we
|
176
|
+
# need to pass one todo: use a dummy one ?
|
177
|
+
@fi = Filter.new_by_parse(@params['$filter'], @cx)
|
178
|
+
return BadRequestFilterParseError if @fi.parse_error?
|
179
|
+
|
180
|
+
# nil is the expected return for no errors
|
181
|
+
nil
|
182
|
+
end
|
183
|
+
|
184
|
+
def check_u_p_orderby
|
185
|
+
# TODO: this should be moved into OData::Order somehow, at least partly
|
186
|
+
return unless (pordlist = @params['$orderby'].dup)
|
187
|
+
|
188
|
+
pordlist.split(',').each do |pord|
|
189
|
+
pord.strip!
|
190
|
+
qualfn, dir = pord.split(/\s/)
|
191
|
+
qualfn.strip!
|
192
|
+
dir.strip! if dir
|
193
|
+
return BadRequestError unless @attribute_path_list.include? qualfn
|
194
|
+
return BadRequestError unless [nil, 'asc', 'desc'].include? dir
|
195
|
+
end
|
196
|
+
# nil is the expected return for no errors
|
197
|
+
nil
|
198
|
+
end
|
199
|
+
|
200
|
+
def build_attribute_path_list
|
201
|
+
@attribute_path_list = attribute_path_list
|
202
|
+
@attrib_paths_url_regexp = @attribute_path_list.join('|')
|
203
|
+
end
|
204
|
+
|
205
|
+
def attribute_path_list(nodes = Set.new)
|
206
|
+
# break circles
|
207
|
+
return [] if nodes.include?(entity_set_name)
|
208
|
+
|
209
|
+
ret = @columns.map(&:to_s)
|
210
|
+
nodes.add entity_set_name
|
211
|
+
if @nav_entity_attribs
|
212
|
+
@nav_entity_attribs.each do |a, k|
|
213
|
+
ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
|
214
|
+
end
|
215
|
+
end
|
216
|
+
if @nav_collection_attribs
|
217
|
+
@nav_collection_attribs.each do |a, k|
|
218
|
+
ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
|
219
|
+
end
|
220
|
+
end
|
221
|
+
ret
|
222
|
+
end
|
223
|
+
|
224
|
+
def check_url_params
|
225
|
+
return nil unless @params
|
226
|
+
|
227
|
+
check_u_p_top || check_u_p_skip || check_u_p_orderby || check_u_p_filter
|
228
|
+
end
|
229
|
+
|
230
|
+
def initialize_dataset
|
231
|
+
# initialize dataset
|
232
|
+
@cx = self
|
233
|
+
@ax = nil
|
234
|
+
@cx = navigated_dataset if @cx.navigated_coll
|
235
|
+
end
|
236
|
+
|
237
|
+
# on model class level we return the collection
|
238
|
+
def odata_get(req)
|
239
|
+
@params = req.params
|
240
|
+
@uribase = req.uribase
|
241
|
+
initialize_dataset
|
242
|
+
|
243
|
+
if (perr = check_url_params)
|
244
|
+
perr.odata_get(req)
|
245
|
+
else
|
246
|
+
odata_get_apply_params
|
247
|
+
if req.walker.do_count
|
248
|
+
[200, { 'Content-Type' => 'text/plain;charset=utf-8' },
|
249
|
+
@cx.count.to_s]
|
250
|
+
elsif req.accept?('application/json')
|
251
|
+
[200, { 'Content-Type' => 'application/json;charset=utf-8' },
|
252
|
+
to_odata_json(service: req.service)]
|
253
|
+
else # TODO: other formats
|
254
|
+
406
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def to_odata_json(service:)
|
260
|
+
expand = @params['$expand']
|
261
|
+
{ 'd' => service.get_coll_odata_h(array: get_a,
|
262
|
+
expand: expand,
|
263
|
+
uribase: @uribase) }.to_json
|
264
|
+
end
|
265
|
+
|
266
|
+
def get_a
|
267
|
+
@ax.nil? ? @cx.to_a : @ax
|
268
|
+
end
|
269
|
+
|
270
|
+
def odata_post(req)
|
271
|
+
# TODO: check Request body format...
|
272
|
+
# TODO: this is for v2 only...
|
273
|
+
data = JSON.parse(req.body.read)['d']
|
274
|
+
if req.accept?('application/json')
|
275
|
+
[201, { 'Content-Type' => 'application/json;charset=utf-8' },
|
276
|
+
new_from_hson_h(data).to_odata_post_json(service: req.service)]
|
277
|
+
else # TODO: other formats
|
278
|
+
415
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# this functionally similar to the Sequel Rels (many_to_one etc)
|
283
|
+
# We need to base this on the Sequel rels, or extend them
|
284
|
+
def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
|
285
|
+
@nav_collection_attribs = (@nav_collection_attribs || {})
|
286
|
+
# DONE: Error handling. This requires that associations
|
287
|
+
# have been properly defined with Sequel before
|
288
|
+
assoc = all_association_reflections.find do |a|
|
289
|
+
a[:name] == assoc_symb && a[:model] == self
|
290
|
+
end
|
291
|
+
unless assoc
|
292
|
+
raise OData::API::ModelAssociationNameError.new(self, assoc_symb)
|
293
|
+
end
|
294
|
+
|
295
|
+
attr_class = assoc[:class_name].constantize
|
296
|
+
lattr_name_str = (attr_name_str || assoc_symb.to_s)
|
297
|
+
@nav_collection_attribs[lattr_name_str] = attr_class
|
298
|
+
@nav_collection_url_regexp = @nav_collection_attribs.keys.join('|')
|
299
|
+
end
|
300
|
+
|
301
|
+
def add_nav_prop_single(assoc_symb, attr_name_str = nil)
|
302
|
+
@nav_entity_attribs = (@nav_entity_attribs || {})
|
303
|
+
# DONE: Error handling. This requires that associations
|
304
|
+
# have been properly defined with Sequel before
|
305
|
+
assoc = all_association_reflections.find do |a|
|
306
|
+
a[:name] == assoc_symb && a[:model] == self
|
307
|
+
end
|
308
|
+
unless assoc
|
309
|
+
raise OData::API::ModelAssociationNameError.new(self, assoc_symb)
|
310
|
+
end
|
311
|
+
|
312
|
+
attr_class = assoc[:class_name].constantize
|
313
|
+
lattr_name_str = (attr_name_str || assoc_symb.to_s)
|
314
|
+
@nav_entity_attribs[lattr_name_str] = attr_class
|
315
|
+
@nav_entity_url_regexp = @nav_entity_attribs.keys.join('|')
|
316
|
+
end
|
317
|
+
|
318
|
+
# old names...
|
319
|
+
# alias_method :add_nav_prop_collection, :addNavCollectionAttrib
|
320
|
+
# alias_method :add_nav_prop_single, :addNavEntityAttrib
|
321
|
+
|
322
|
+
def prepare_pk
|
323
|
+
if primary_key.is_a? Array
|
324
|
+
@pk_names = []
|
325
|
+
primary_key.each { |pk| @pk_names << pk.to_s }
|
326
|
+
# TODO: better handle quotes based on type
|
327
|
+
# (stringlike--> quote, int-like --> no quotes)
|
328
|
+
|
329
|
+
iuk = @pk_names.map { |pk| "#{pk}='?(\\w+)'?" }
|
330
|
+
@iuk_rgx = /\A#{iuk.join(',\s*')}\z/
|
331
|
+
|
332
|
+
iuk = @pk_names.map { |pk| "#{pk}='?\\w+'?" }
|
333
|
+
@entity_id_url_regexp = /\A\(\s*(#{iuk.join(',\s*')})\s*\)(.*)/
|
334
|
+
else
|
335
|
+
@pk_names = [primary_key.to_s]
|
336
|
+
@entity_id_url_regexp = /\A\('?([\w\s]+)'?\)(.*)/
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def prepare_fields
|
341
|
+
@data_fields = db_schema.map do |col, cattr|
|
342
|
+
cattr[:primary_key] ? nil : col
|
343
|
+
end.select { |col| col }
|
344
|
+
end
|
345
|
+
|
346
|
+
# A regexp matching all allowed attributes of the Entity
|
347
|
+
# (eg ID|name|size etc... )
|
348
|
+
def attribute_url_regexp
|
349
|
+
# db_schema.map { |sch| sch[0] }.join('|')
|
350
|
+
# @columns is from Sequel Model
|
351
|
+
@columns.join('|')
|
352
|
+
end
|
353
|
+
|
354
|
+
# methods related to transitions to next state (cf. walker)
|
355
|
+
module Transitions
|
356
|
+
def transition_end(_match_result)
|
357
|
+
[nil, :end]
|
358
|
+
end
|
359
|
+
|
360
|
+
def transition_count(_match_result)
|
361
|
+
[self, :end_with_count]
|
362
|
+
end
|
363
|
+
|
364
|
+
def transition_id(match_result)
|
365
|
+
# binding.pry
|
366
|
+
if (id = match_result[1])
|
367
|
+
# puts "in transition_id, found #{y}"
|
368
|
+
if (y = find(id))
|
369
|
+
[y, :run]
|
370
|
+
else
|
371
|
+
[nil, :error, ErrorNotFound]
|
372
|
+
end
|
373
|
+
else
|
374
|
+
[nil, :error, ServerTransitionError]
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def allowed_transitions
|
379
|
+
[Safrano::TransitionEnd,
|
380
|
+
Safrano::TransitionCount,
|
381
|
+
Safrano::Transition.new(entity_id_url_regexp,
|
382
|
+
trans: 'transition_id')]
|
383
|
+
end
|
384
|
+
end
|
385
|
+
include Transitions
|
386
|
+
end
|
387
|
+
# special handling for composite key
|
388
|
+
module EntityClassMultiPK
|
389
|
+
include EntityClassBase
|
390
|
+
# id is for composite key, something like fx='aas',fy_w='0001'
|
391
|
+
def find(mid)
|
392
|
+
# Note: @iuk_rgx is (needs to be) built on start with
|
393
|
+
# collklass.prepare_pk
|
394
|
+
md = @iuk_rgx.match(mid).to_a
|
395
|
+
md.shift # remove first element which is the whole match
|
396
|
+
self[*md] # Sequel Model rulez
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# special handling for single key
|
401
|
+
module EntityClassSinglePK
|
402
|
+
include EntityClassBase
|
403
|
+
# id is really just the value of single pk
|
404
|
+
def find(id)
|
405
|
+
self[id]
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|