safrano 0.0.4 → 0.0.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/odata/attribute.rb +58 -0
- data/lib/odata/collection.rb +2 -2
- data/lib/odata/collection_filter.rb +87 -9
- data/lib/odata/entity.rb +2 -5
- data/lib/odata/walker.rb +8 -1
- data/lib/safrano.rb +1 -0
- data/lib/safrano_core.rb +2 -26
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a69035d5b1015e36842490919163234c4ae55591657fb7d3b4b730b871c0035b
|
4
|
+
data.tar.gz: 5c0024539a67f37cbf397975315066b1df803dd826b242b873147bbd1da60062
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7538b83433db7381e77a34415e9cd4887cd2db20bfe8ba05db66e6d3703a0358e9f3024f137bddfca3b9b490caf3afbc2262b6b26514a36efeae792c8983a670
|
7
|
+
data.tar.gz: b12e002bdbdeb826678d9cd1f8c1c9dce8bd11791adf8205464d12b115551bebbc59ddf4635d4406e93c4f5cee955f5356fea3d373857bac662ddbe5dd098ef7
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative '../safrano_core.rb'
|
3
|
+
require_relative './entity.rb'
|
4
|
+
|
5
|
+
module OData
|
6
|
+
# Represents a named and valued attribute of an Entity
|
7
|
+
class Attribute
|
8
|
+
attr_reader :name
|
9
|
+
attr_reader :entity
|
10
|
+
def initialize(entity, name)
|
11
|
+
@entity = entity
|
12
|
+
@name = name
|
13
|
+
end
|
14
|
+
|
15
|
+
def value
|
16
|
+
@entity.values[@name.to_sym]
|
17
|
+
end
|
18
|
+
|
19
|
+
def odata_get(req)
|
20
|
+
if req.walker.raw_value
|
21
|
+
[200, { 'Content-Type' => 'text/plain;charset=utf-8' },
|
22
|
+
value.to_s]
|
23
|
+
elsif req.accept?('application/json')
|
24
|
+
[200, { 'Content-Type' => 'application/json;charset=utf-8' },
|
25
|
+
to_odata_json(service: req.service)]
|
26
|
+
else # TODO: other formats
|
27
|
+
406
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# output as OData json (v2)
|
32
|
+
def to_odata_json(*)
|
33
|
+
{ 'd' => { @name => value } }.to_json
|
34
|
+
end
|
35
|
+
|
36
|
+
# for testing purpose (assert_equal ...)
|
37
|
+
def ==(other)
|
38
|
+
(@entity == other.entity) && (@name == other.name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# methods related to transitions to next state (cf. walker)
|
42
|
+
module Transitions
|
43
|
+
def transition_end(_match_result)
|
44
|
+
[nil, :end]
|
45
|
+
end
|
46
|
+
|
47
|
+
def transition_value(_match_result)
|
48
|
+
[self, :end_with_value]
|
49
|
+
end
|
50
|
+
|
51
|
+
def allowed_transitions
|
52
|
+
[Safrano::TransitionEnd,
|
53
|
+
Safrano::TransitionValue]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
include Transitions
|
57
|
+
end
|
58
|
+
end
|
data/lib/odata/collection.rb
CHANGED
@@ -87,9 +87,9 @@ module OData
|
|
87
87
|
enty = new
|
88
88
|
hash.delete('__metadata')
|
89
89
|
# DONE: move this somewhere else where it's evaluated only once at setup
|
90
|
-
#data_fields = db_schema.map do |col, cattr|
|
90
|
+
# data_fields = db_schema.map do |col, cattr|
|
91
91
|
# cattr[:primary_key] ? nil : col
|
92
|
-
#end.select { |col| col }
|
92
|
+
# end.select { |col| col }
|
93
93
|
enty.set_fields(hash, @data_fields, missing: :skip)
|
94
94
|
enty.save
|
95
95
|
enty
|
@@ -88,8 +88,8 @@ module OData
|
|
88
88
|
class Filter
|
89
89
|
attr_reader :assoc
|
90
90
|
attr_reader :assocs
|
91
|
-
|
92
|
-
|
91
|
+
QUO = /'((?:[^']|(?:\'{2}))*)'/.freeze
|
92
|
+
NOTQ = /((?:[^'\s\)])*)/.freeze
|
93
93
|
|
94
94
|
def initialize(matx)
|
95
95
|
@matx = matx
|
@@ -139,11 +139,10 @@ module OData
|
|
139
139
|
class EqualFilter < Filter
|
140
140
|
include FilterWithRuby
|
141
141
|
include FilterWithAssoc
|
142
|
-
|
143
|
-
RIGHT_RGX = %q{'[^']+'|[^'\)\s]+}.freeze
|
142
|
+
EQL = '[eE][qQ]|[LlgGNn][eETt]'.freeze
|
144
143
|
|
145
144
|
def self.qualified_regexp(paths_rx)
|
146
|
-
/\s*(#{paths_rx})\s+(#{
|
145
|
+
/\s*(#{paths_rx})\s+(#{EQL})\s+(?:#{QUO}|#{NOTQ})\s*/
|
147
146
|
end
|
148
147
|
|
149
148
|
def initialize(matx)
|
@@ -208,6 +207,82 @@ module OData
|
|
208
207
|
end
|
209
208
|
end
|
210
209
|
|
210
|
+
# equality expressions with functions ... length(name) eq 2
|
211
|
+
class FuncEqualFilter < EqualFilter
|
212
|
+
FUNC = 'length|trim|tolower|toupper'.freeze
|
213
|
+
|
214
|
+
attr_reader :funcname
|
215
|
+
|
216
|
+
def self.qualified_regexp(pathsrx)
|
217
|
+
/\s*(#{FUNC})\((#{pathsrx})\)\s+(#{EQL})\s+(?:#{QUO}|#{NOTQ})\s*/
|
218
|
+
end
|
219
|
+
|
220
|
+
def initialize(matx)
|
221
|
+
super(matx)
|
222
|
+
@funcname = matx[1]
|
223
|
+
self.fn = matx[2]
|
224
|
+
get_assoc if @fn
|
225
|
+
|
226
|
+
@val = matx[4] ? matx[4].gsub("''", "'") : matx[5]
|
227
|
+
|
228
|
+
@op = matx[3].downcase
|
229
|
+
end
|
230
|
+
|
231
|
+
# ugly hack... but working
|
232
|
+
def gen_sql(dtcx)
|
233
|
+
# handle ambiguous column names (joins) by qualifiying the names
|
234
|
+
# seqfn = @assoc ? Sequel[@assoc.to_sym][@fn.to_sym] : @fn.to_sym
|
235
|
+
# handle ambiguous column names (joins) by qualifiying the names
|
236
|
+
# for the main table as well
|
237
|
+
# TODO: find a better way to differentiate between the two types
|
238
|
+
# of dtcx : Model or Dataset
|
239
|
+
|
240
|
+
# DONE: same handling for order and substring and everywhere where
|
241
|
+
# qualified fieldnames could be needed (uses attrib path regexp)
|
242
|
+
x = apply_to_dataset(dtcx)
|
243
|
+
# ugly ugly hack :-(
|
244
|
+
y = x.unordered.sql.sub(x.unfiltered.unordered.sql + ' WHERE', '')
|
245
|
+
y
|
246
|
+
end
|
247
|
+
|
248
|
+
def apply_to_dataset(dtcx)
|
249
|
+
seqfn = get_qualified_fn(dtcx)
|
250
|
+
# using @val in Procs below does not work reliably. Using a local var
|
251
|
+
# seems to work better
|
252
|
+
argval = @val
|
253
|
+
seqfunc = case @funcname
|
254
|
+
when 'length'
|
255
|
+
argval = Sequel.lit(@val)
|
256
|
+
Sequel.char_length(seqfn)
|
257
|
+
when 'trim'
|
258
|
+
Sequel.trim(seqfn)
|
259
|
+
when 'toupper'
|
260
|
+
Sequel.function(:upper, seqfn)
|
261
|
+
when 'tolower'
|
262
|
+
Sequel.function(:lower, seqfn)
|
263
|
+
else
|
264
|
+
raise OData::FilterParseError
|
265
|
+
end
|
266
|
+
|
267
|
+
case @op
|
268
|
+
when 'eq'
|
269
|
+
dtcx.where(seqfunc => argval)
|
270
|
+
when 'ne'
|
271
|
+
dtcx.exclude(seqfunc => argval)
|
272
|
+
when 'le'
|
273
|
+
dtcx.where { seqfunc <= argval }
|
274
|
+
when 'ge'
|
275
|
+
dtcx.where { seqfunc >= argval }
|
276
|
+
when 'lt'
|
277
|
+
dtcx.where { seqfunc < argval }
|
278
|
+
when 'gt'
|
279
|
+
dtcx.where { seqfunc > argval }
|
280
|
+
else
|
281
|
+
raise OData::FilterParseError
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
211
286
|
# keyword based
|
212
287
|
class FilterWithKeyword < Filter
|
213
288
|
attr_reader :keyword
|
@@ -221,8 +296,9 @@ module OData
|
|
221
296
|
def self.qualified_regexp(paths_rx)
|
222
297
|
# Note: QUOTED_RGX captures the string content without
|
223
298
|
# start and end quote (no need to un-quote)
|
224
|
-
# NOTQ_RGX captures an not-quoted value argument
|
225
|
-
|
299
|
+
# NOTQ_RGX captures an not-quoted value argument
|
300
|
+
# (like for numbers)
|
301
|
+
/(substringof)\((?:#{QUO}|#{NOTQ}),\s*(#{paths_rx})\)/
|
226
302
|
end
|
227
303
|
|
228
304
|
def initialize(matx)
|
@@ -237,7 +313,7 @@ module OData
|
|
237
313
|
|
238
314
|
def whcl(dtcx)
|
239
315
|
# handle ambiguous column names (joins) by qualifiying the names
|
240
|
-
#
|
316
|
+
# seqfn = @assoc ? Sequel[@assoc.to_sym][@fn.to_sym] : @fn.to_sym
|
241
317
|
seqfn = get_qualified_fn(dtcx)
|
242
318
|
Sequel.like(seqfn, "%#{@val}%")
|
243
319
|
end
|
@@ -289,7 +365,7 @@ module OData
|
|
289
365
|
# example : startswith(year, 190')
|
290
366
|
|
291
367
|
def self.qualified_regexp(paths)
|
292
|
-
/(endswith|startswith)\((#{paths}),\s*(?:#{
|
368
|
+
/(endswith|startswith)\((#{paths}),\s*(?:#{QUO}|#{NOTQ})\s*\)/
|
293
369
|
end
|
294
370
|
|
295
371
|
def initialize(matx)
|
@@ -358,6 +434,7 @@ module OData
|
|
358
434
|
class ComplexFilter < Filter
|
359
435
|
# list of active Subfilter classes. The ordering is important
|
360
436
|
SUBFILTERS = [EqualFilter,
|
437
|
+
FuncEqualFilter,
|
361
438
|
SubstringOfFilterSig1,
|
362
439
|
StartOrEndsWithFilter].freeze
|
363
440
|
# for counting number of AND OR's
|
@@ -375,6 +452,7 @@ module OData
|
|
375
452
|
else
|
376
453
|
dtset.model.attrib_paths_url_regexp.dup
|
377
454
|
end
|
455
|
+
|
378
456
|
@active_subfs = []
|
379
457
|
@osql = filterstr.dup
|
380
458
|
@dt = dtset
|
data/lib/odata/entity.rb
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# !/usr/bin/env ruby
|
3
|
-
|
4
1
|
require 'json'
|
5
2
|
require 'pp'
|
6
3
|
require 'rexml/document'
|
@@ -47,7 +44,8 @@ module OData
|
|
47
44
|
|
48
45
|
def transition_attribute(match_result)
|
49
46
|
attrib = match_result[1]
|
50
|
-
[values[attrib.to_sym], :run]
|
47
|
+
# [values[attrib.to_sym], :run]
|
48
|
+
[OData::Attribute.new(self, attrib), :run]
|
51
49
|
end
|
52
50
|
|
53
51
|
def transition_nav_collection(match_result)
|
@@ -156,7 +154,6 @@ module OData
|
|
156
154
|
end
|
157
155
|
|
158
156
|
def odata_patch(req)
|
159
|
-
|
160
157
|
data = JSON.parse(req.body.read)
|
161
158
|
@uribase = req.uribase
|
162
159
|
|
data/lib/odata/walker.rb
CHANGED
@@ -22,6 +22,9 @@ module OData
|
|
22
22
|
# is $count requested?
|
23
23
|
attr_accessor :do_count
|
24
24
|
|
25
|
+
# is $value requested?
|
26
|
+
attr_reader :raw_value
|
27
|
+
|
25
28
|
def initialize(service, path)
|
26
29
|
path = URI.decode_www_form_component(path)
|
27
30
|
@context = service
|
@@ -78,11 +81,15 @@ module OData
|
|
78
81
|
@contexts << @context
|
79
82
|
@path_remain = @tr_next.path_remain
|
80
83
|
@path_done << @tr_next.path_done
|
81
|
-
# little hack
|
84
|
+
# little hack's
|
82
85
|
if @status == :end_with_count
|
83
86
|
@do_count = true
|
84
87
|
@status == :end
|
85
88
|
end
|
89
|
+
if @status == :end_with_value
|
90
|
+
@raw_value = true
|
91
|
+
@status == :end
|
92
|
+
end
|
86
93
|
else
|
87
94
|
@context = nil
|
88
95
|
@status = :error
|
data/lib/safrano.rb
CHANGED
data/lib/safrano_core.rb
CHANGED
@@ -83,31 +83,7 @@ module Safrano
|
|
83
83
|
trans: 'transition_batch')
|
84
84
|
TransitionCount = Transition.new('(\A\/\$count)(.*)\z',
|
85
85
|
trans: 'transition_count')
|
86
|
+
TransitionValue = Transition.new('(\A\/\$value)(.*)\z',
|
87
|
+
trans: 'transition_value')
|
86
88
|
attr_accessor :allowed_transitions
|
87
89
|
end
|
88
|
-
|
89
|
-
# for $count and number type attributes
|
90
|
-
# class Fixnum deprecated as of ruby 2.4+
|
91
|
-
class Integer
|
92
|
-
def odata_get(_req)
|
93
|
-
[200, { 'Content-Type' => 'text/plain;charset=utf-8' }, to_s]
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# for string type attributes
|
98
|
-
class String
|
99
|
-
def odata_get(_req)
|
100
|
-
[200, { 'Content-Type' => 'text/plain;charset=utf-8' }, to_s]
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# Final attribute value....
|
105
|
-
class Object
|
106
|
-
def allowed_transitions
|
107
|
-
[Safrano::TransitionEnd]
|
108
|
-
end
|
109
|
-
|
110
|
-
def transition_end(_match_result)
|
111
|
-
[nil, :end]
|
112
|
-
end
|
113
|
-
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safrano
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- D.M.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-04-
|
11
|
+
date: 2019-04-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -73,6 +73,7 @@ executables: []
|
|
73
73
|
extensions: []
|
74
74
|
extra_rdoc_files: []
|
75
75
|
files:
|
76
|
+
- lib/odata/attribute.rb
|
76
77
|
- lib/odata/batch.rb
|
77
78
|
- lib/odata/collection.rb
|
78
79
|
- lib/odata/collection_filter.rb
|
@@ -110,5 +111,5 @@ requirements: []
|
|
110
111
|
rubygems_version: 3.0.3
|
111
112
|
signing_key:
|
112
113
|
specification_version: 4
|
113
|
-
summary: Safrano
|
114
|
+
summary: Safrano is a ruby (Sequel + Rack) OData provider
|
114
115
|
test_files: []
|