safrano 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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 +6 -2
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +136 -642
  16. data/lib/odata/collection_filter.rb +16 -40
  17. data/lib/odata/collection_media.rb +56 -37
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  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 +53 -117
  23. data/lib/odata/error.rb +142 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +4 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +33 -25
  28. data/lib/odata/filter/sequel.rb +97 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +10 -10
  31. data/lib/odata/filter/tree.rb +75 -41
  32. data/lib/odata/function_import.rb +166 -0
  33. data/lib/odata/model_ext.rb +618 -0
  34. data/lib/odata/navigation_attribute.rb +9 -24
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +15 -7
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +12 -94
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +25 -20
  46. data/lib/safrano/rack_app.rb +61 -62
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +95 -37
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +132 -94
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +24 -5
@@ -1,56 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'odata/error.rb'
2
4
 
3
5
  require_relative 'filter/parse.rb'
4
6
  require_relative 'filter/sequel.rb'
5
7
 
6
- # a few helper method
7
- class String
8
- MASK_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
9
- UNMASK_RGX = /'(%?)(\$\d+)(%?)'/.freeze
10
- def with_mask_quoted_substrings!
11
- cnt = 0
12
- repl = {}
13
- gsub!(MASK_RGX) do |_m|
14
- cnt += 1
15
- repl["$#{cnt}"] = Regexp.last_match(1)
16
- "'$#{cnt}'"
17
- end
18
- yield self
19
-
20
- gsub!(UNMASK_RGX) do |_m|
21
- k = Regexp.last_match(2).to_s
22
- "'#{Regexp.last_match(1)}#{repl[k]}#{Regexp.last_match(3)}'"
23
- end
24
- end
25
-
26
- def with_mask_quoted_substrings
27
- cnt = 0
28
- repl = {}
29
- tmpstr = gsub(MASK_RGX) do |_m|
30
- cnt += 1
31
- repl["$#{cnt}"] = Regexp.last_match(1)
32
- "'$#{cnt}'"
33
- end
34
- yield tmpstr
35
- end
36
- end
37
-
38
8
  # filter base class and subclass in our OData namespace
39
- module OData
9
+ module Safrano
40
10
  class FilterBase
41
11
  # re-useable empty filtering (idempotent)
42
- EmptyFilter = new
12
+ EmptyFilter = new.freeze
43
13
 
44
14
  def self.factory(filterstr)
45
15
  filterstr.nil? ? EmptyFilter : FilterByParse.new(filterstr)
46
16
  end
47
17
 
48
18
  def apply_to_dataset(dtcx)
49
- dtcx
19
+ Contract.valid(dtcx)
50
20
  end
51
21
 
52
22
  # finalize
53
- def finalize(jh) end
23
+ def finalize(_jh) Contract::OK end
54
24
 
55
25
  def empty?
56
26
  true
@@ -60,12 +30,14 @@ module OData
60
30
  false
61
31
  end
62
32
  end
33
+
63
34
  # should handle everything by parsing
64
35
  class FilterByParse < FilterBase
65
36
  attr_reader :filterstr
37
+
66
38
  def initialize(filterstr)
67
39
  @filterstr = filterstr.dup
68
- @ast = OData::Filter::Parser.new(@filterstr).build
40
+ @ast = Safrano::Filter::Parser.new(@filterstr).build
69
41
  end
70
42
 
71
43
  # this build's up the Sequel Filter Expression, and as a side effect,
@@ -73,16 +45,20 @@ module OData
73
45
  # the join-helper is shared by the order-by object and was potentially already
74
46
  # partly built on order-by object creation.
75
47
  def finalize(jh)
76
- @filtexpr = @ast.sequel_expr(jh)
48
+ @filtexpr = @ast.if_valid do |ast| ast.sequel_expr(jh) end
77
49
  end
78
50
 
79
51
  def apply_to_dataset(dtcx)
80
52
  # normally finalize is called before, and thus @filtexpr is set
81
- dtcx.where(@filtexpr)
53
+ @filtexpr.map_result! do |f| dtcx.where(f) end
82
54
  end
83
55
 
56
+ # Note: this is really only *parse* error, ie the error encounterd while
57
+ # trying to build the AST
58
+ # Later when evaluating the AST, there can be other errors, they shall
59
+ # be tracked with @error
84
60
  def parse_error?
85
- @ast.is_a? StandardError
61
+ @ast.error
86
62
  end
87
63
 
88
64
  def empty?
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
  require 'fileutils'
3
5
  require_relative './navigation_attribute.rb'
4
6
 
5
- module OData
7
+ module Safrano
6
8
  module Media
7
9
  # base class for Media Handler
8
10
  class Handler
@@ -12,15 +14,17 @@ module OData
12
14
  # similar to Rack::Static
13
15
  # with a flat directory structure
14
16
  class Static < Handler
15
- def initialize(root: nil)
17
+ def initialize(root: nil, mediaklass:)
16
18
  @root = File.absolute_path(root || Dir.pwd)
17
19
  @file_server = ::Rack::File.new(@root)
20
+ @media_class = mediaklass
21
+ @media_dir_name = mediaklass.to_s
22
+ register
18
23
  end
19
24
 
20
- # TODO: testcase and better abs_klass_dir design
21
- def register(klass)
22
- abs_klass_dir = File.absolute_path(klass.type_name, @root)
23
- FileUtils.makedirs abs_klass_dir unless Dir.exist?(abs_klass_dir)
25
+ def register
26
+ @abs_klass_dir = File.absolute_path(@media_dir_name, @root)
27
+ FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
24
28
  end
25
29
 
26
30
  # minimal working implementation...
@@ -36,17 +40,10 @@ module OData
36
40
  fsret
37
41
  end
38
42
 
39
- # TODO: [perf] this can be precalculated and cached on MediaModelKlass level
40
- # and passed as argument to save_file
41
- # eg. /@root/Photo
42
- def abs_klass_dir(entity)
43
- File.absolute_path(entity.klass_dir, @root)
44
- end
45
-
46
43
  # this is relative to @root
47
44
  # eg. Photo/1
48
45
  def media_path(entity)
49
- File.join(entity.klass_dir, media_directory(entity))
46
+ File.join(@media_dir_name, media_directory(entity))
50
47
  end
51
48
 
52
49
  # relative to @root
@@ -56,7 +53,7 @@ module OData
56
53
  # simple design: one file per directory, and the directory
57
54
  # contains the media entity-id --> implicit link between the media
58
55
  # entity
59
- File.join(media_path(entity), Dir.glob('*').first)
56
+ File.join(media_path(entity), Dir.glob('*').sort.first)
60
57
  end
61
58
  end
62
59
 
@@ -81,7 +78,7 @@ module OData
81
78
  end
82
79
 
83
80
  def odata_delete(entity:)
84
- Dir.chdir(abs_klass_dir(entity)) do
81
+ Dir.chdir(@abs_klass_dir) do
85
82
  in_media_directory(entity) do
86
83
  Dir.glob('*').each { |oldf| File.delete(oldf) }
87
84
  end
@@ -90,7 +87,7 @@ module OData
90
87
 
91
88
  # Here as well, MVP implementation
92
89
  def save_file(data:, filename:, entity:)
93
- Dir.chdir(abs_klass_dir(entity)) do
90
+ Dir.chdir(@abs_klass_dir) do
94
91
  in_media_directory(entity) do
95
92
  filename = '1'
96
93
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
@@ -102,19 +99,19 @@ module OData
102
99
  # after each upload, so that clients get informed about new versions
103
100
  # of the same media ressource
104
101
  def ressource_version(entity)
105
- Dir.chdir(abs_klass_dir(entity)) do
102
+ Dir.chdir(@abs_klass_dir) do
106
103
  in_media_directory(entity) do
107
- Dir.glob('*').last
104
+ Dir.glob('*').sort.last
108
105
  end
109
106
  end
110
107
  end
111
108
 
112
109
  # Here as well, MVP implementation
113
110
  def replace_file(data:, filename:, entity:)
114
- Dir.chdir(abs_klass_dir(entity)) do
111
+ Dir.chdir(@abs_klass_dir) do
115
112
  in_media_directory(entity) do
116
113
  version = nil
117
- Dir.glob('*').each do |oldf|
114
+ Dir.glob('*').sort.each do |oldf|
118
115
  version = oldf
119
116
  File.delete(oldf)
120
117
  end
@@ -156,9 +153,9 @@ module OData
156
153
  end
157
154
 
158
155
  def odata_delete(entity:)
159
- Dir.chdir(abs_klass_dir(entity)) do
156
+ Dir.chdir(@abs_klass_dir) do
160
157
  in_media_directory(entity) do
161
- Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
158
+ Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
162
159
  end
163
160
  end
164
161
  end
@@ -174,10 +171,10 @@ module OData
174
171
  # end
175
172
  # Here as well, MVP implementation
176
173
  def replace_file(data:, filename:, entity:)
177
- Dir.chdir(abs_klass_dir(entity)) do
174
+ Dir.chdir(@abs_klass_dir) do
178
175
  in_media_directory(entity) do
179
176
  version = nil
180
- Dir.glob('*').each do |oldf|
177
+ Dir.glob('*').sort.each do |oldf|
181
178
  version = oldf
182
179
  File.delete(oldf)
183
180
  end
@@ -197,14 +194,15 @@ module OData
197
194
  # API method for defining the media handler
198
195
  # eg.
199
196
  # publish_media_model photos do
200
- # use OData::Media::Static, :root => '/media_root'
197
+ # use Safrano::Media::Static, :root => '/media_root'
201
198
  # end
202
199
 
203
200
  def set_default_media_handler
204
- @media_handler = OData::Media::Static.new
201
+ @media_handler = Safrano::Media::Static.new(mediaklass: self)
205
202
  end
206
203
 
207
204
  def use(klass, args)
205
+ args[:mediaklass] = self
208
206
  @media_handler = klass.new(**args)
209
207
  end
210
208
 
@@ -214,11 +212,7 @@ module OData
214
212
  end
215
213
 
216
214
  def api_check_media_fields
217
- raise(OData::API::MediaModelError, self) unless db_schema.key?(:content_type)
218
-
219
- # unless self.db_schema.has_key?(:media_src)
220
- # raise OData::API::MediaModelError, self
221
- # end
215
+ raise(Safrano::API::MediaModelError, self) unless db_schema.key?(:content_type)
222
216
  end
223
217
 
224
218
  # END API methods
@@ -237,11 +231,11 @@ module OData
237
231
  # NOTE: we will implement this first in a MVP way. There will be plenty of
238
232
  # potential future enhancements (performance, scallability, flexibility... with added complexity)
239
233
  # 4. create relation to parent if needed
240
- def odata_create_entity_and_relation(req, assoc, parent)
234
+ def odata_create_entity_and_relation(req, assoc = nil, parent = nil)
241
235
  req.with_media_data do |data, mimetype, filename|
242
236
  ## future enhancement: validate allowed mimetypes ?
243
237
  # if (invalid = invalid_media_mimetype(mimetype))
244
- # ::OData::Request::ON_CGST_ERROR.call(req)
238
+ # ::Safrano::Request::ON_CGST_ERROR.call(req)
245
239
  # return [422, {}, ['Invalid mime type: ', invalid.to_s]]
246
240
  # end
247
241
 
@@ -267,9 +261,11 @@ module OData
267
261
  req.register_content_id_ref(new_entity)
268
262
  new_entity.copy_request_infos(req)
269
263
 
270
- media_handler.save_file(data: data, entity: new_entity, filename: filename)
271
-
272
- [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
264
+ media_handler.save_file(data: data,
265
+ entity: new_entity,
266
+ filename: filename)
267
+ # json is default content type so we dont need to specify it here again
268
+ [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
273
269
  else # TODO: other formats
274
270
  415
275
271
  end
@@ -277,3 +273,26 @@ module OData
277
273
  end
278
274
  end
279
275
  end
276
+
277
+ # deprecated
278
+ # REMOVE 0.6
279
+ module OData
280
+ module Media
281
+ class Static < ::Safrano::Media::Static
282
+ def initialize(root: nil, mediaklass:)
283
+ ::Safrano::Deprecation.deprecate('OData::Media::Static',
284
+ 'Use Safrano::Media::Static instead')
285
+
286
+ super
287
+ end
288
+ end
289
+ class StaticTree < ::Safrano::Media::StaticTree
290
+ def initialize(root: nil, mediaklass:)
291
+ ::Safrano::Deprecation.deprecate('OData::Media::StaticTree',
292
+ 'Use Safrano::Media::StaticTree instead')
293
+
294
+ super
295
+ end
296
+ end
297
+ end
298
+ end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'odata/error.rb'
2
4
 
3
5
  # all ordering related classes in our OData module
4
- module OData
6
+ module Safrano
5
7
  # base class for ordering
6
8
  class OrderBase
7
9
  # re-useable empty ordering (idempotent)
8
- EmptyOrder = new
10
+ EmptyOrder = new.freeze
9
11
 
10
12
  # input : the OData order string
11
13
  # returns a Order object that should have a apply_to(cx) method
@@ -28,6 +30,7 @@ module OData
28
30
 
29
31
  class Order < OrderBase
30
32
  attr_reader :oarg
33
+
31
34
  def initialize(ostr, jh)
32
35
  ostr.strip!
33
36
  @orderp = ostr
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class ODataCommonLogger < CommonLogger
3
5
  def call(env)
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module FunctionImport
5
+ class ResultDefinition
6
+ D = 'd'.freeze
7
+ DJ_OPEN = '{"d":'.freeze
8
+ DJ_CLOSE = '}'.freeze
9
+ METAK = '__metadata'.freeze
10
+ TYPEK = 'type'.freeze
11
+ VALUEK = 'value'.freeze
12
+ RESULTSK = 'results'.freeze
13
+ COLLECTION = 'Collection'.freeze
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'.freeze
101
+ TYPEK = 'type'.freeze
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