safrano 0.4.3 → 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.
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