safrano 0.4.2 → 0.5.0

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 +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