safrano 0.4.2 → 0.5.0

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 +9 -7
  15. data/lib/odata/collection.rb +140 -591
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  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 +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  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 +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  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 +106 -52
  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 +13 -26
  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 +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  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 +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -11
@@ -1,56 +1,26 @@
1
- require 'odata/error.rb'
1
+ # frozen_string_literal: true
2
2
 
3
- require_relative 'filter/parse.rb'
4
- require_relative 'filter/sequel.rb'
3
+ require 'odata/error'
5
4
 
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
5
+ require_relative 'filter/parse'
6
+ require_relative 'filter/sequel'
37
7
 
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 { |ast| ast.sequel_expr(jh) }
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! { |f| dtcx.where(f) }
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,26 +1,42 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
  require 'fileutils'
3
- require_relative './navigation_attribute.rb'
5
+ require_relative './navigation_attribute'
4
6
 
5
- module OData
7
+ module Safrano
6
8
  module Media
7
9
  # base class for Media Handler
8
10
  class Handler
11
+ def check_before_create(data:,
12
+ entity:,
13
+ filename:)
14
+ Contract::OK
15
+ end
9
16
  end
10
17
 
11
18
  # Simple static File/Directory based media store handler
12
19
  # similar to Rack::Static
13
20
  # with a flat directory structure
14
21
  class Static < Handler
15
- def initialize(root: nil)
22
+ def initialize(root: nil, mediaklass:)
16
23
  @root = File.absolute_path(root || Dir.pwd)
17
24
  @file_server = ::Rack::File.new(@root)
25
+ @media_class = mediaklass
26
+ @media_dir_name = mediaklass.to_s
27
+ register
28
+ end
29
+
30
+ def register
31
+ @abs_klass_dir = File.absolute_path(@media_dir_name, @root)
32
+ end
33
+
34
+ def create_abs_class_dir
35
+ FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
18
36
  end
19
37
 
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)
38
+ def finalize
39
+ create_abs_class_dir
24
40
  end
25
41
 
26
42
  # minimal working implementation...
@@ -36,27 +52,20 @@ module OData
36
52
  fsret
37
53
  end
38
54
 
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
55
  # this is relative to @root
47
56
  # eg. Photo/1
48
57
  def media_path(entity)
49
- File.join(entity.klass_dir, media_directory(entity))
58
+ File.join(@media_dir_name, media_directory(entity))
50
59
  end
51
60
 
52
61
  # relative to @root
53
- # eg Photo/1/pommes-topaz.jpg
62
+ # eg Photo/1/1
54
63
  def filename(entity)
55
64
  Dir.chdir(abs_path(entity)) do
56
65
  # simple design: one file per directory, and the directory
57
66
  # contains the media entity-id --> implicit link between the media
58
67
  # entity
59
- File.join(media_path(entity), Dir.glob('*').first)
68
+ File.join(media_path(entity), Dir.glob('*').max)
60
69
  end
61
70
  end
62
71
 
@@ -65,6 +74,11 @@ module OData
65
74
  File.absolute_path(media_path(entity), @root)
66
75
  end
67
76
 
77
+ # absolute filename
78
+ def abs_filename(entity)
79
+ File.absolute_path(filename(entity), @root)
80
+ end
81
+
68
82
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
69
83
  # simplest implementation is media_directory = entity.media_path_id
70
84
  # --> we get a 1 level depth flat directory structure
@@ -81,7 +95,7 @@ module OData
81
95
  end
82
96
 
83
97
  def odata_delete(entity:)
84
- Dir.chdir(abs_klass_dir(entity)) do
98
+ Dir.chdir(@abs_klass_dir) do
85
99
  in_media_directory(entity) do
86
100
  Dir.glob('*').each { |oldf| File.delete(oldf) }
87
101
  end
@@ -90,7 +104,7 @@ module OData
90
104
 
91
105
  # Here as well, MVP implementation
92
106
  def save_file(data:, filename:, entity:)
93
- Dir.chdir(abs_klass_dir(entity)) do
107
+ Dir.chdir(@abs_klass_dir) do
94
108
  in_media_directory(entity) do
95
109
  filename = '1'
96
110
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
@@ -102,19 +116,19 @@ module OData
102
116
  # after each upload, so that clients get informed about new versions
103
117
  # of the same media ressource
104
118
  def ressource_version(entity)
105
- Dir.chdir(abs_klass_dir(entity)) do
119
+ Dir.chdir(@abs_klass_dir) do
106
120
  in_media_directory(entity) do
107
- Dir.glob('*').last
121
+ Dir.glob('*').max
108
122
  end
109
123
  end
110
124
  end
111
125
 
112
126
  # Here as well, MVP implementation
113
127
  def replace_file(data:, filename:, entity:)
114
- Dir.chdir(abs_klass_dir(entity)) do
128
+ Dir.chdir(@abs_klass_dir) do
115
129
  in_media_directory(entity) do
116
130
  version = nil
117
- Dir.glob('*').each do |oldf|
131
+ Dir.glob('*').sort.each do |oldf|
118
132
  version = oldf
119
133
  File.delete(oldf)
120
134
  end
@@ -138,15 +152,14 @@ module OData
138
152
 
139
153
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
140
154
  # tree-structure
141
- # media_path_ids = 1 --> 1
142
- # media_path_ids = 15 --> 1/5
143
- # media_path_ids = 555 --> 5/5/5
144
- # media_path_ids = 5,5,5 --> 5/00/5/00/5
145
- # media_path_ids = 5,00,5 --> 5/00/0/0/00/5
146
- # media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
155
+ # media_path_ids = 1 --> 1/v
156
+ # media_path_ids = 15 --> 1/5/v
157
+ # media_path_ids = 555 --> 5/5/5/v
158
+ # media_path_ids = 5,5,5 --> 5/00/5/00/5/v
159
+ # media_path_ids = 5,00,5 --> 5/00/0/0/00/5/v
160
+ # media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5/v
147
161
  def media_directory(entity)
148
162
  StaticTree.path_builder(entity.media_path_ids)
149
- # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
150
163
  end
151
164
 
152
165
  def in_media_directory(entity)
@@ -156,9 +169,9 @@ module OData
156
169
  end
157
170
 
158
171
  def odata_delete(entity:)
159
- Dir.chdir(abs_klass_dir(entity)) do
172
+ Dir.chdir(@abs_klass_dir) do
160
173
  in_media_directory(entity) do
161
- Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
174
+ Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
162
175
  end
163
176
  end
164
177
  end
@@ -174,10 +187,10 @@ module OData
174
187
  # end
175
188
  # Here as well, MVP implementation
176
189
  def replace_file(data:, filename:, entity:)
177
- Dir.chdir(abs_klass_dir(entity)) do
190
+ Dir.chdir(@abs_klass_dir) do
178
191
  in_media_directory(entity) do
179
192
  version = nil
180
- Dir.glob('*').each do |oldf|
193
+ Dir.glob('*').sort.each do |oldf|
181
194
  version = oldf
182
195
  File.delete(oldf)
183
196
  end
@@ -197,15 +210,21 @@ module OData
197
210
  # API method for defining the media handler
198
211
  # eg.
199
212
  # publish_media_model photos do
200
- # use OData::Media::Static, :root => '/media_root'
213
+ # use Safrano::Media::Static, :root => '/media_root'
201
214
  # end
202
215
 
203
216
  def set_default_media_handler
204
- @media_handler = OData::Media::Static.new
217
+ @media_handler = Safrano::Media::Static.new(mediaklass: self)
218
+ end
219
+
220
+ def finalize_media
221
+ @media_handler.finalize
205
222
  end
206
223
 
207
224
  def use(klass, args)
225
+ args[:mediaklass] = self
208
226
  @media_handler = klass.new(**args)
227
+ @media_handler.create_abs_class_dir
209
228
  end
210
229
 
211
230
  # API method for setting the model field mapped to SLUG on upload
@@ -214,11 +233,7 @@ module OData
214
233
  end
215
234
 
216
235
  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
236
+ raise(Safrano::API::MediaModelError, self) unless db_schema.key?(:content_type)
222
237
  end
223
238
 
224
239
  # END API methods
@@ -237,11 +252,11 @@ module OData
237
252
  # NOTE: we will implement this first in a MVP way. There will be plenty of
238
253
  # potential future enhancements (performance, scallability, flexibility... with added complexity)
239
254
  # 4. create relation to parent if needed
240
- def odata_create_entity_and_relation(req, assoc, parent)
255
+ def odata_create_entity_and_relation(req, assoc = nil, parent = nil)
241
256
  req.with_media_data do |data, mimetype, filename|
242
257
  ## future enhancement: validate allowed mimetypes ?
243
258
  # if (invalid = invalid_media_mimetype(mimetype))
244
- # ::OData::Request::ON_CGST_ERROR.call(req)
259
+ # ::Safrano::Request::ON_CGST_ERROR.call(req)
245
260
  # return [422, {}, ['Invalid mime type: ', invalid.to_s]]
246
261
  # end
247
262
 
@@ -256,20 +271,39 @@ module OData
256
271
  missing: :skip)
257
272
  end
258
273
 
259
- # to_one rels are create with FK data set on the parent entity
260
- if parent
261
- odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
262
- else
263
- # in-changeset requests get their own transaction
264
- new_entity.save(transaction: !req.in_changeset)
265
- end
274
+ # call before_create_entity media hook
275
+ new_entity.before_create_media_entity(data: data, mimetype: mimetype) if new_entity.respond_to? :before_create_media_entity
276
+
277
+ media_handler.check_before_create(data: data,
278
+ entity: new_entity,
279
+ filename: filename).if_valid { |_ret|
280
+ # to_one rels are create with FK data set on the parent entity
281
+ if parent
282
+ odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
283
+ else
284
+ # in-changeset requests get their own transaction
285
+ new_entity.save(transaction: !req.in_changeset)
286
+ end
287
+
288
+ req.register_content_id_ref(new_entity)
289
+ new_entity.copy_request_infos(req)
290
+
291
+ # call before_create_media hook
292
+ new_entity.before_create_media if new_entity.respond_to? :before_create_media
293
+
294
+ media_handler.save_file(data: data,
295
+ entity: new_entity,
296
+ filename: filename)
266
297
 
267
- req.register_content_id_ref(new_entity)
268
- new_entity.copy_request_infos(req)
298
+ # call after_create_media hook
299
+ new_entity.after_create_media if new_entity.respond_to? :after_create_media
269
300
 
270
- media_handler.save_file(data: data, entity: new_entity, filename: filename)
301
+ # json is default content type so we dont need to specify it here again
302
+ # Contract.valid([201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)])
303
+ # TODO quirks array mode !
304
+ Contract.valid([201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)])
305
+ }.tap_error { |e| return e.odata_get(req) }.result
271
306
 
272
- [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
273
307
  else # TODO: other formats
274
308
  415
275
309
  end
@@ -277,3 +311,26 @@ module OData
277
311
  end
278
312
  end
279
313
  end
314
+
315
+ # deprecated
316
+ # REMOVE 0.6
317
+ module OData
318
+ module Media
319
+ class Static < ::Safrano::Media::Static
320
+ def initialize(root: nil, mediaklass:)
321
+ ::Safrano::Deprecation.deprecate('OData::Media::Static',
322
+ 'Use Safrano::Media::Static instead')
323
+
324
+ super
325
+ end
326
+ end
327
+ class StaticTree < ::Safrano::Media::StaticTree
328
+ def initialize(root: nil, mediaklass:)
329
+ ::Safrano::Deprecation.deprecate('OData::Media::StaticTree',
330
+ 'Use Safrano::Media::StaticTree instead')
331
+
332
+ super
333
+ end
334
+ end
335
+ end
336
+ 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'
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