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.
- checksums.yaml +4 -4
- data/lib/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +6 -2
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +136 -642
- data/lib/odata/collection_filter.rb +16 -40
- data/lib/odata/collection_media.rb +56 -37
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +53 -117
- data/lib/odata/error.rb +142 -37
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +4 -1
- data/lib/odata/filter/error.rb +43 -27
- data/lib/odata/filter/parse.rb +33 -25
- data/lib/odata/filter/sequel.rb +97 -56
- data/lib/odata/filter/sequel_function_adapter.rb +50 -49
- data/lib/odata/filter/token.rb +10 -10
- data/lib/odata/filter/tree.rb +75 -41
- data/lib/odata/function_import.rb +166 -0
- data/lib/odata/model_ext.rb +618 -0
- data/lib/odata/navigation_attribute.rb +9 -24
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +17 -5
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +100 -24
- data/lib/odata/walker.rb +15 -7
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +12 -94
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +25 -20
- data/lib/safrano/rack_app.rb +61 -62
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
- data/lib/safrano/request.rb +95 -37
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +132 -94
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- 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
|
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(
|
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 =
|
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(
|
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.
|
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
|
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
|
-
|
21
|
-
|
22
|
-
abs_klass_dir
|
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(
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
197
|
+
# use Safrano::Media::Static, :root => '/media_root'
|
201
198
|
# end
|
202
199
|
|
203
200
|
def set_default_media_handler
|
204
|
-
@media_handler =
|
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(
|
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
|
-
# ::
|
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,
|
271
|
-
|
272
|
-
|
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
|
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
|
data/lib/odata/common_logger.rb
CHANGED
@@ -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
|