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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +15 -10
  14. data/lib/odata/batch.rb +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +155 -99
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +183 -216
  23. data/lib/odata/error.rb +195 -31
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +113 -63
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +274 -219
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -11
@@ -1,19 +1,36 @@
1
- require 'odata/error.rb'
1
+ # frozen_string_literal: true
2
2
 
3
- # Ordering with ruby expression
4
- module OrderWithRuby
5
- # this module requires the @fn attribute to exist where it is used
6
- def fn=(fnam)
7
- @fn = fnam
8
- @fn_tab = fnam.split('/').map(&:to_sym)
9
- end
10
- end
3
+ require 'odata/error.rb'
11
4
 
12
5
  # all ordering related classes in our OData module
13
- module OData
6
+ module Safrano
14
7
  # base class for ordering
15
- class Order
8
+ class OrderBase
9
+ # re-useable empty ordering (idempotent)
10
+ EmptyOrder = new.freeze
11
+
12
+ # input : the OData order string
13
+ # returns a Order object that should have a apply_to(cx) method
14
+ def self.factory(orderstr, jh)
15
+ orderstr.nil? ? EmptyOrder : MultiOrder.new(orderstr, jh)
16
+ end
17
+
18
+ def empty?
19
+ true
20
+ end
21
+
22
+ def parse_error?
23
+ false
24
+ end
25
+
26
+ def apply_to_dataset(dtcx)
27
+ dtcx
28
+ end
29
+ end
30
+
31
+ class Order < OrderBase
16
32
  attr_reader :oarg
33
+
17
34
  def initialize(ostr, jh)
18
35
  ostr.strip!
19
36
  @orderp = ostr
@@ -21,23 +38,14 @@ module OData
21
38
  build_oarg if @orderp
22
39
  end
23
40
 
24
- class << self
25
- attr_reader :regexp
26
- end
27
-
28
- # input : the filter string
29
- # returns a filter object that should have a apply_to(cx) method
30
- def self.new_by_parse(orderstr, jh)
31
- Order.new_full_match_complexpr(orderstr, jh)
32
- end
33
-
34
- # handle with Sequel
35
- def self.new_full_match_complexpr(orderstr, jh)
36
- ComplexOrder.new(orderstr, jh)
41
+ def empty?
42
+ false
37
43
  end
38
44
 
39
45
  def apply_to_dataset(dtcx)
40
- dtcx
46
+ # Warning, we need order_append, simply order(oarg) overwrites
47
+ # previous one !
48
+ dtcx.order_append(@oarg)
41
49
  end
42
50
 
43
51
  def build_oarg
@@ -60,26 +68,31 @@ module OData
60
68
  end
61
69
 
62
70
  # complex ordering logic
63
- class ComplexOrder < Order
71
+ class MultiOrder < Order
64
72
  def initialize(orderstr, jh)
65
73
  super
66
74
  @olist = []
67
75
  @jh = jh
68
- return unless orderstr
69
-
70
- @olist = orderstr.split(',').map do |ostr|
71
- oo = Order.new(ostr, @jh)
72
- oo.oarg
73
- end
76
+ @orderstr = orderstr.dup
77
+ @olist = orderstr.split(',').map { |ostr| Order.new(ostr, @jh) }
74
78
  end
75
79
 
76
80
  def apply_to_dataset(dtcx)
77
- @olist.each { |oarg|
78
- # Warning, we need order_append, simply order(oarg) overwrites
79
- # previous one !
80
- dtcx = dtcx.order_append(oarg)
81
- }
81
+ @olist.each { |osingl| dtcx = osingl.apply_to_dataset(dtcx) }
82
82
  dtcx
83
83
  end
84
+
85
+ def parse_error?
86
+ @orderstr.split(',').each do |pord|
87
+ pord.strip!
88
+ qualfn, dir = pord.split(/\s/)
89
+ qualfn.strip!
90
+ dir.strip! if dir
91
+ return true unless @jh.start_model.attrib_path_valid? qualfn
92
+ return true unless [nil, 'asc', 'desc'].include? dir
93
+ end
94
+
95
+ false
96
+ end
84
97
  end
85
98
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class ODataCommonLogger < CommonLogger
3
5
  def call(env)
@@ -5,41 +7,41 @@ module Rack
5
7
  super
6
8
  end
7
9
 
8
- # Handle https://github.com/rack/rack/pull/1526
10
+ # Handle https://github.com/rack/rack/pull/1526
9
11
  # new in Rack 2.2.2 : Format has now 11 placeholders instead of 10
10
-
11
- MSG_FUNC = if (FORMAT.count('%') == 10)
12
- lambda {|env,length,status,began_at|
13
- FORMAT % [
14
- env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
15
- env["REMOTE_USER"] || "-",
16
- Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
17
- env[REQUEST_METHOD],
18
- env[SCRIPT_NAME] + env[PATH_INFO],
19
- env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
20
- env[SERVER_PROTOCOL],
21
- status.to_s[0..3],
22
- length,
23
- Utils.clock_time - began_at
24
- ]
25
- }
26
- elsif (FORMAT.count('%') == 11)
27
- lambda {|env,length,status,began_at|
28
- FORMAT % [
29
- env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
30
- env["REMOTE_USER"] || "-",
31
- Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
32
- env[REQUEST_METHOD],
33
- env[SCRIPT_NAME],
34
- env[PATH_INFO],
35
- env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
36
- env[SERVER_PROTOCOL],
37
- status.to_s[0..3],
38
- length,
39
- Utils.clock_time - began_at
40
- ]
41
- }
42
- end
12
+
13
+ MSG_FUNC = if FORMAT.count('%') == 10
14
+ lambda { |env, length, status, began_at|
15
+ FORMAT % [
16
+ env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
17
+ env['REMOTE_USER'] || '-',
18
+ Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
19
+ env[REQUEST_METHOD],
20
+ env[SCRIPT_NAME] + env[PATH_INFO],
21
+ env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
22
+ env[SERVER_PROTOCOL],
23
+ status.to_s[0..3],
24
+ length,
25
+ Utils.clock_time - began_at
26
+ ]
27
+ }
28
+ elsif FORMAT.count('%') == 11
29
+ lambda { |env, length, status, began_at|
30
+ FORMAT % [
31
+ env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
32
+ env['REMOTE_USER'] || '-',
33
+ Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
34
+ env[REQUEST_METHOD],
35
+ env[SCRIPT_NAME],
36
+ env[PATH_INFO],
37
+ env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
38
+ env[SERVER_PROTOCOL],
39
+ status.to_s[0..3],
40
+ length,
41
+ Utils.clock_time - began_at
42
+ ]
43
+ }
44
+ end
43
45
 
44
46
  def batch_log(env, status, header, began_at)
45
47
  length = extract_content_length(header)
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module FunctionImport
5
+ class ResultDefinition
6
+ D = 'd'
7
+ DJ_OPEN = '{"d":'
8
+ DJ_CLOSE = '}'
9
+ METAK = '__metadata'
10
+ TYPEK = 'type'
11
+ VALUEK = 'value'
12
+ RESULTSK = 'results'
13
+ COLLECTION = 'Collection'
14
+
15
+ def initialize(klassmod)
16
+ @klassmod = klassmod
17
+ end
18
+
19
+ def to_odata_json(result, _req)
20
+ "#{DJ_OPEN}#{result.odata_h.to_json}#{DJ_CLOSE}"
21
+ end
22
+
23
+ def type_metadata
24
+ @klassmod.type_name
25
+ end
26
+ end
27
+ class ResultAsComplexType < ResultDefinition
28
+ end
29
+ class ResultAsComplexTypeColl < ResultDefinition
30
+ def type_metadata
31
+ "Collection(#{@klassmod.type_name})"
32
+ end
33
+
34
+ def to_odata_json(coll, _req)
35
+ "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
36
+ end
37
+ end
38
+ class ResultAsEntity < ResultDefinition
39
+ def to_odata_json(result_entity, req)
40
+ result_entity.instance_exec do
41
+ copy_request_infos(req)
42
+ to_odata_json(request: req)
43
+ end
44
+ end
45
+ end
46
+ class ResultAsEntityColl < ResultDefinition
47
+ def type_metadata
48
+ "Collection(#{@klassmod.type_name})"
49
+ end
50
+
51
+ def to_odata_json(result_dataset, req)
52
+ coll = Safrano::OData::Collection.new(@klassmod)
53
+ coll.instance_exec do
54
+ @params = req.params
55
+ initialize_dataset(result_dataset)
56
+ end
57
+ coll.to_odata_json(request: req)
58
+ end
59
+ end
60
+ class ResultAsPrimitiveType < ResultDefinition
61
+ def to_odata_json(result, _req)
62
+ { D => { METAK => { TYPEK => type_metadata },
63
+ VALUEK => @klassmod.odata_value(result) } }.to_json
64
+ end
65
+ end
66
+ class ResultAsPrimitiveTypeColl < ResultDefinition
67
+ def type_metadata
68
+ "Collection(#{@klassmod.type_name})"
69
+ end
70
+
71
+ def to_odata_json(result, _req)
72
+ { D => { METAK => { TYPEK => type_metadata },
73
+ RESULTSK => @klassmod.odata_collection(result) } }.to_json
74
+ end
75
+ end
76
+ end
77
+
78
+ # a generic Struct like ruby's standard Struct, but implemented with a
79
+ # @values Hash, similar to Sequel models and
80
+ # with added OData functionality
81
+ class ComplexType
82
+ attr_reader :values
83
+
84
+ @namespace = nil
85
+ def self.namespace
86
+ @namespace
87
+ end
88
+
89
+ def self.props
90
+ @props
91
+ end
92
+
93
+ def self.type_name
94
+ "#{@namespace}.#{self.to_s}"
95
+ end
96
+
97
+ def initialize
98
+ @values = {}
99
+ end
100
+ METAK = '__metadata'
101
+ TYPEK = 'type'
102
+
103
+ def odata_h
104
+ ret = { METAK => { TYPEK => self.class.type_name } }
105
+ @values.each { |k, v|
106
+ ret[k] = if v.respond_to? :odata_h
107
+ v.odata_h
108
+ else
109
+ v
110
+ end
111
+ }
112
+ ret
113
+ end
114
+
115
+ def self.return_as_collection_descriptor
116
+ FunctionImport::ResultAsComplexTypeColl.new(self)
117
+ end
118
+
119
+ def self.return_as_instance_descriptor
120
+ FunctionImport::ResultAsComplexType.new(self)
121
+ end
122
+
123
+ # add metadata xml to the passed REXML schema object
124
+ def self.add_metadata_rexml(schema)
125
+ ctty = schema.add_element('ComplexType', 'Name' => to_s)
126
+
127
+ # with their properties
128
+ @props.each do |prop, rbtype|
129
+ attrs = { 'Name' => prop.to_s,
130
+ 'Type' => rbtype.type_name }
131
+ ctty.add_element('Property', attrs)
132
+ end
133
+ ctty
134
+ end
135
+ end
136
+
137
+ def Safrano.ComplexType(**props)
138
+ Class.new(Safrano::ComplexType) do
139
+ @props = props
140
+ props.each { |a, klassmod|
141
+ asym = a.to_sym
142
+ define_method(asym) do @values[asym] end
143
+ define_method("#{a}=") do |val| @values[asym] = val end
144
+ }
145
+ define_method :initialize do |*p, **kwvals|
146
+ super()
147
+ p.zip(props.keys).each { |val, a| @values[a] = val } if p
148
+ kwvals.each { |a, val| @values[a] = val if props.key?(a) } if kwvals
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Safrano
6
+ # Type mapping DB --> Edm
7
+ # TypeMap = {"INTEGER" => "Edm.Int32" , "TEXT" => "Edm.String",
8
+ # "STRING" => "Edm.String"}
9
+ # Todo: complete mapping... this is just for the most common ones
10
+
11
+ # TODO: use Sequel GENERIC_TYPES: -->
12
+ # Constants
13
+ # GENERIC_TYPES = %w'String Integer Float Numeric BigDecimal Date DateTime
14
+ # Time File TrueClass FalseClass'.freeze
15
+ # Classes specifying generic types that Sequel will convert to
16
+ # database-specific types.
17
+ DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
18
+
19
+ # used in $metadata
20
+ # cf. Sequel Database column_schema_default_to_ruby_value
21
+ # schema_column_type
22
+ # https://www.odata.org/documentation/odata-version-2-0/overview/
23
+ def self.default_edm_type(ruby_type:)
24
+ case ruby_type
25
+ when :integer
26
+ 'Edm.Int32'
27
+ when :string
28
+ 'Edm.String'
29
+ when :date, :datetime,
30
+ 'Edm.DateTime'
31
+ when :time
32
+ 'Edm.Time'
33
+ when :boolean
34
+ 'Edm.Boolean'
35
+ when :float
36
+ 'Edm.Double'
37
+ when :decimal
38
+ 'Edm.Decimal'
39
+ when :blob
40
+ 'Edm.Binary'
41
+ end
42
+ end
43
+
44
+ # use Edm twice so that we can do include Safrano::Edm and then
45
+ # have Edm::Int32 etc... availabe
46
+ # and we can have Edm::String different from ::String
47
+ module Edm
48
+ module Edm
49
+ module OutputClassMethods
50
+ def type_name
51
+ "Edm.#{name.split('::').last}"
52
+ end
53
+
54
+ def odata_collection(array)
55
+ array
56
+ end
57
+
58
+ def odata_value(instance)
59
+ instance
60
+ end
61
+ end
62
+
63
+ class Null < NilClass
64
+ extend OutputClassMethods
65
+ # nil --> null convertion is done by to_json
66
+ def self.odata_value(instance)
67
+ nil
68
+ end
69
+
70
+ def self.convert_from_urlparam(v)
71
+ return Contract::NOK unless (v == 'null')
72
+
73
+ Contract.valid(nil)
74
+ end
75
+ end
76
+
77
+ # Binary is a String with the BINARY encoding
78
+ class Binary < String
79
+ extend OutputClassMethods
80
+
81
+ def self.convert_from_urlparam(v)
82
+ Contract.valid(v.dup.force_encoding('BINARY'))
83
+ end
84
+ end
85
+
86
+ # an object alwys evaluates to
87
+ # true ([true, anything not false & not nil objs])
88
+ # or false([nil, false])
89
+ class Boolean < Object
90
+ extend OutputClassMethods
91
+ def Boolean.odata_value(instance)
92
+ instance ? true : false
93
+ end
94
+
95
+ def self.odata_collection(array)
96
+ array.map { |v| odata_value(v) }
97
+ end
98
+
99
+ def self.convert_from_urlparam(v)
100
+ return Contract::NOK unless ['true', 'false'].include?(v)
101
+
102
+ Contract.valid(v == 'true')
103
+ end
104
+ end
105
+
106
+ # Bytes are usualy represented as Intger in ruby,
107
+ # eg.String.bytes --> Array of ints
108
+ class Byte < Integer
109
+ extend OutputClassMethods
110
+
111
+ def self.convert_from_urlparam(v)
112
+ return Contract::NOK unless ((bytev = v.to_i) < 256)
113
+
114
+ Contract.valid(bytev)
115
+ end
116
+ end
117
+
118
+ class DateTime < ::DateTime
119
+ extend OutputClassMethods
120
+ def DateTime.odata_value(instance)
121
+ instance.to_datetime
122
+ end
123
+
124
+ def self.odata_collection(array)
125
+ array.map { |v| odata_value(v) }
126
+ end
127
+
128
+ def self.convert_from_urlparam(v)
129
+ begin
130
+ Contract.valid(DateTime.parse(v))
131
+ rescue
132
+ return convertion_error(v)
133
+ end
134
+ end
135
+ end
136
+
137
+ class String < ::String
138
+ extend OutputClassMethods
139
+
140
+ def self.convert_from_urlparam(v)
141
+ Contract.valid(v)
142
+ end
143
+ end
144
+
145
+ class Int32 < Integer
146
+ extend OutputClassMethods
147
+
148
+ def self.convert_from_urlparam(v)
149
+ return Contract::NOK unless (ret = number_or_nil(v))
150
+
151
+ Contract.valid(ret)
152
+ end
153
+ end
154
+
155
+ class Int64 < Integer
156
+ extend OutputClassMethods
157
+
158
+ def self.convert_from_urlparam(v)
159
+ return Contract::NOK unless (ret = number_or_nil(v))
160
+
161
+ Contract.valid(ret)
162
+ end
163
+ end
164
+
165
+ class Double < Float
166
+ extend OutputClassMethods
167
+
168
+ def self.convert_from_urlparam(v)
169
+ begin
170
+ Contract.valid(v.to_f)
171
+ rescue
172
+ return Contract::NOK
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ # include Safrano
181
+
182
+ # x = Edm::String.new('xxx')
183
+
184
+ # pp x