dataMetaDom 1.0.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.
@@ -0,0 +1,168 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ require 'set'
4
+ require 'fileutils'
5
+ require 'dataMetaDom/help'
6
+ require 'dataMetaDom/field'
7
+ require 'dataMetaDom/pojo' # we borrow some useful methods from the POJO Lexer
8
+ require 'erb'
9
+ require 'ostruct'
10
+
11
+ module DataMetaDom
12
+ =begin rdoc
13
+ Definition for generating Python basic classes that do not relate to serialization layer.
14
+
15
+ For command line details either check the new method's source or the README.rdoc file, the usage section.
16
+ =end
17
+ module PythonLexer
18
+ include DataMetaDom
19
+
20
+ # Standard header for every __init__.py file
21
+ INIT_PY_HEADER = %q<
22
+ # see https://docs.python.org/3/library/pkgutil.html
23
+ # without this, Python will have trouble finding packages that share some common tree off the root
24
+ from pkgutil import extend_path
25
+ __path__ = extend_path(__path__, __name__)
26
+
27
+ >
28
+
29
+ # The top of each model file.
30
+ MODEL_HEADER = %q|# This file is generated by DataMeta DOM. Do not edit manually!
31
+ # package %s
32
+
33
+ import re
34
+
35
+ from ebay_datameta_core.base import Verifiable, DateTime, Migrator, SemVer
36
+ from ebay_datameta_core.canned_re import CannedRe
37
+
38
+ # noinspection PyCompatibility
39
+ from enum import Enum
40
+
41
+ |
42
+
43
+ # Augment the RegExRoster class with Python specifics
44
+ class PyRegExRoster < RegExRoster
45
+ # converts the registry to the Python variable -- compiled Re
46
+ def to_patterns
47
+ i_to_r.keys.map { |ix|
48
+ r = i_to_r[ix]
49
+ rx = r.r.to_s
50
+ %<#{RegExRoster.ixToVarName(ix)} = re.compile(#{rx.inspect}) # #{r.vars.to_a.sort.join(', ')}>
51
+ }.join("\n#{INDENT}")
52
+ end
53
+
54
+ # converts the registry to the verification code for the verify() method
55
+ def to_verifications(baseName)
56
+ result = (canned.keys.map { |r|
57
+ r = canned[r]
58
+ vs = r.vars.to_a.sort
59
+ vs.map { |v|
60
+ rx = r.r.to_s
61
+ %<\n#{INDENT*2}if(#{r.req? ? '' : "self.__#{v} is not None and "}CannedRe.CANNED_RES[#{rx.inspect}].match(self.__#{v}) is None):
62
+ #{INDENT*3}raise AttributeError("Property \\"#{v}\\" == {{%s}} didn't match canned expression \\"#{rx}\\"" % self.__#{v} )>
63
+ }
64
+ }).flatten
65
+ (result << i_to_r.keys.map { |ix|
66
+ r = i_to_r[ix]
67
+ vs = r.vars.to_a.sort
68
+ rv = "#{baseName}.#{RegExRoster.ixToVarName(ix)}"
69
+ vs.map { |v|
70
+ %<\n#{INDENT*2}if(#{r.req? ? '' : "self.__#{v} is not None and "}#{rv}.match(self.__#{v}) is None):
71
+ #{INDENT*3}raise AttributeError("Property \\"#{v}\\" == {{%s}} didn't match custom expression {{%s}}" %(self.__#{v}, #{rv}))>
72
+ }
73
+ }).flatten
74
+ result.join("\n")
75
+ end
76
+ end
77
+
78
+ =begin rdoc
79
+ Generates Python sources for the model, the "plain" Python part, without fancy dependencies.
80
+ * Parameters
81
+ * +parser+ - instance of Model
82
+ * +outRoot+ - output directory
83
+ =end
84
+ def genPy(model, outRoot)
85
+ firstRecord = model.records.values.first
86
+ @pyPackage, baseName, packagePath = DataMetaDom::PojoLexer::assertNamespace(firstRecord.name)
87
+
88
+ # Next: replace dots with underscores.
89
+ # The path also adjusted accordingly.
90
+ #
91
+ # Rationale for this, quoting PEP 8:
92
+ #
93
+ # Package and Module Names
94
+ #
95
+ # Modules should have short, all-lowercase names. Underscores can be used in the module name if it improves
96
+ # readability. Python packages should also have short, all-lowercase names, although the use of underscores
97
+ # is discouraged.
98
+ #
99
+ # Short and all-lowercase names, and improving readability if you have complex system and need long package names,
100
+ # is "discouraged". Can't do this here, our system is more complicated for strictly religous, "pythonic" Python.
101
+ # A tool must be enabling, and in this case, this irrational ruling gets in the way.
102
+ # And dots are a no-no, Python can't find packages with complicated package structures and imports.
103
+ #
104
+ # Hence, we opt for long package names with underscores for distinctiveness and readability:
105
+ @pyPackage = @pyPackage.gsub('.', '_')
106
+ packagePath = packagePath.gsub('/', '_')
107
+ destDir = File.join(outRoot, packagePath)
108
+ FileUtils.mkdir_p destDir
109
+ # build the package path and create __init__.py files in each package's dir if it's not there.
110
+ @destDir = packagePath.split(File::SEPARATOR).reduce(outRoot){ |s,v|
111
+ r = s.empty? ? v : File.join(s, v) # next package dir
112
+ FileUtils.mkdir_p r # create if not there
113
+ # create the __init__.py with proper content if it's not there
114
+ df = File.join(r, '__init__.py'); IO.write(df, INIT_PY_HEADER, mode: 'wb') unless r == outRoot || File.file?(df)
115
+ r # pass r for the next reduce iteration
116
+ }
117
+ vars = OpenStruct.new # for template's local variables. ERB does not make them visible to the binding
118
+ modelFile = File.join(@destDir, 'model.py')
119
+ [modelFile].each{|f| FileUtils.rm f if File.file?(f)}
120
+ IO.write(modelFile, MODEL_HEADER % @pyPackage, mode: 'wb')
121
+ (model.records.values + model.enums.values).each { |srcE| # it is important that the records render first
122
+ _, baseName, _ = DataMetaDom::PojoLexer::assertNamespace(srcE.name)
123
+
124
+ pyClassName = baseName
125
+
126
+ case
127
+ when srcE.kind_of?(DataMetaDom::Record)
128
+ fields = srcE.fields
129
+ rxRoster = PyRegExRoster.new
130
+ eqHashFields = srcE.identity ? srcE.identity.args : fields.keys.sort
131
+ reqFields = fields.values.select{|f| f.isRequired }.map{|f| f.name}
132
+ verCalls = reqFields.map{|r| %<if(self.__#{r} is None): missingFields.append("#{r}");>}.join("\n#{INDENT * 2}")
133
+ fieldVerifications = ''
134
+ fields.each_key { |k|
135
+ f = fields[k]
136
+ dt = f.dataType
137
+ rxRoster.register(f) if f.regex
138
+ if f.trgType # Maps: if either the key or the value is verifiable, do it
139
+ mainVf = model.records[dt.type] # main data type is verifiable
140
+ trgVf = model.records[f.trgType.type] # target type is verifiable
141
+ if mainVf || trgVf
142
+ fieldVerifications << "\n#{INDENT*2}#{!f.isRequired ? "if(self.__#{f.name} is not None):\n#{INDENT*3}" : '' }for k, v in self.__#{f.name}.iteritems():#{mainVf ? 'k.verify();' : ''} #{trgVf ? 'v.verify()' :''}\n"
143
+ end
144
+ end
145
+
146
+ if model.records[dt.type] && !f.trgType # maps handled separately
147
+ fieldVerifications << "\n#{INDENT*2}#{!f.isRequired ? "if(self.__#{f.name} is not None): " : '' }#{f.aggr ? "[v___#{f.name}.verify() for v___#{f.name} in self.__#{f.name} ]" : "self.__#{f.name}.verify()"}"
148
+ # the Verifiable::verify method reference works just fine, tested it: Java correctly calls the method on the object
149
+ end
150
+ }
151
+ IO::write(File.join(@destDir, 'model.py'),
152
+ ERB.new(IO.read(File.join(File.dirname(__FILE__), '../../tmpl/python/entity.erb')),
153
+ $SAFE, '%<>').result(binding), mode: 'ab')
154
+ when srcE.kind_of?(DataMetaDom::Mappings)
155
+ # FIXME -- implement!!!
156
+ when srcE.kind_of?(DataMetaDom::Enum)
157
+ IO.write(modelFile, %|#{baseName} = Enum("#{baseName}", "#{srcE.rawVals.join(' ')}")\n\n|, mode: 'ab')
158
+ # # handled below, bundled in one huge file
159
+ when srcE.kind_of?(DataMetaDom::BitSet)
160
+ # FIXME -- implement!!!
161
+ else
162
+ raise "Unsupported Entity: #{srcE.inspect}"
163
+ end
164
+ }
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,271 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ %w(fileutils set).each { |r| require r }
4
+
5
+ module DataMetaDom
6
+
7
+ =begin rdoc
8
+ Record Attribute such as unique fields set, identity information, indexes, references etc
9
+ the common structure is like this:
10
+ keyword (hint1, hint2, hint3...) arg1, arg2
11
+
12
+ For command line details either check the new method's source or the README.rdoc file, the usage section.
13
+ =end
14
+ class RecAttr
15
+
16
+ =begin rdoc
17
+ The keyword for the attribute.
18
+ =end
19
+ attr_reader :keyword
20
+
21
+ =begin rdoc
22
+ A Set of hints, empty set if there are no hints on this attribute.
23
+ =end
24
+ attr_reader :hints
25
+
26
+ =begin rdoc
27
+ Arguments on this attribute if any, an array in the order as listed in the DataMeta DOM source. Order is important,
28
+ for example, for an identity.
29
+ =end
30
+ attr_reader :args
31
+
32
+ =begin rdoc
33
+ Unique key for the given attribute to distinguish between those and use in a map. Rebuilt by getKey method
34
+ defined on the subclasses.
35
+ =end
36
+ attr_reader :key
37
+
38
+ =begin rdoc
39
+ Determines if this attribute has the given hint.
40
+ =end
41
+ def hasHint?(hint)
42
+ @hints.member?(hint)
43
+ end
44
+
45
+ =begin rdoc
46
+ Creates an instance for the given keyword.
47
+ =end
48
+ def initialize(keyword)
49
+ @keyword = keyword.to_sym
50
+ raise "Unsupported keyword #@keyword" unless REC_ATTR_KEYWORDS.member?(@keyword)
51
+ @args = []; @hints = Set.new
52
+ end
53
+
54
+ =begin rdoc
55
+ Adds the given argument, updates the key
56
+ =end
57
+ def addArg(val)
58
+ @args << val.to_sym
59
+ updateKey
60
+ self
61
+ end
62
+
63
+ =begin rdoc
64
+ Adds the given hint.
65
+ =end
66
+ def addHint(val); @hints << val end
67
+
68
+ =begin rdoc
69
+ Adds an array of arguments.
70
+ =end
71
+ def addArgs(vals); vals.each { |v| addArg v }; self end
72
+
73
+ =begin rdoc
74
+ Adds a collection of hints.
75
+ =end
76
+ def addHints(vals); vals.each { |h| addHint h }; self end
77
+
78
+ =begin rdoc
79
+ Updates the key, returns self for call chaining
80
+ =end
81
+ def updateKey; @key = getKey; self end
82
+
83
+ =begin rdoc
84
+ Returns the count of arguments.
85
+ =end
86
+ def length; @args.length end
87
+
88
+ =begin rdoc
89
+ Returns the arguments in the given position, zero-based.
90
+ =end
91
+ def [](index); @args[index] end
92
+
93
+
94
+ =begin rdoc
95
+ Joins the arguments with the given delimiter.
96
+ =end
97
+ def join(delimiter); @args.join(delimiter) end
98
+
99
+ =begin rdoc
100
+ Parses this instance from the given source.
101
+ * Parameter:
102
+ * +source+ - an instance of SourceFile
103
+ =end
104
+ def parse(source)
105
+ @sourceRef = source.snapshot
106
+ line = source.line
107
+ recAttrMatch = line.scan(/^\s*(\w*)\s*(\([^\)]+\))?\s+(.+)$/)
108
+ raise "Invalid record attribute spec '#{line}'" unless recAttrMatch
109
+ keyw, hintList, argList = recAttrMatch[0]
110
+ raise "Wrong keyword '#{keyw}', '#@keyword' expected instead" unless keyw && keyw.to_sym == @keyword
111
+ @args = argList.split(/[\(\)\s\,]+/).map { |a| a.to_sym }
112
+ if hintList
113
+ @hints = Set.new hintList.split(/[\(\)\s\,]+/).select { |h| !h.strip.empty? }.map { |h| h.strip.to_sym }
114
+ else
115
+ @hints = Set.new
116
+ end
117
+ end
118
+
119
+ # textual representation of this instance
120
+ def to_s; "#@keyword:#@key; #@sourceRef" end
121
+
122
+ private :initialize
123
+ end
124
+
125
+ =begin rdoc
126
+ The record attribute with the unordered set of arguments.
127
+ See the RecAttrList for the ordered list implementation.
128
+ =end
129
+ class RecAttrSet < RecAttr
130
+
131
+ # Unordered unique set of the arguments
132
+ attr_reader :argSet
133
+
134
+ # Creates an instance with the given keyword
135
+ def initialize(keyword); super(keyword); @argSet = Set.new end
136
+
137
+ # Engages the super's parse method via the alias
138
+ alias :recAttrParse :parse
139
+
140
+ # Determines if the instance has the given argument
141
+ def hasArg?(arg); argSet.member?(arg) end
142
+
143
+ # Builds textual for the set of the arguments, for diagnostics.
144
+ def argSetTextual; @argSet.map { |a| a.to_s }.sort.join(':') end
145
+
146
+ # Builds the unique key for the set of arguments on the instance
147
+ def getKey; argSetTextual.to_sym end
148
+
149
+ # Adds the given argument to the instance
150
+ def addArg(val)
151
+ k = val.to_sym
152
+ raise "Duplicate arg #{k} in the set of #{argSetTextual}" if @argSet.member?(k)
153
+ @argSet << k
154
+ #RecAttr.instance_method(:addArg).bind(self).call k - fortunately, overkill in this case, can do with just:
155
+ super k
156
+ end
157
+
158
+ =begin rdoc
159
+ Parses the instance from the given source.
160
+ * Parameters
161
+ * +src+ - an instance of SourceFile
162
+ =end
163
+ def parse(src)
164
+ recAttrParse(src)
165
+ # look if there are any duplicates, if there are it's an error:
166
+ counterHash = Hash.new(0)
167
+ args.each { |a| k=a.to_sym; counterHash[k] += 1 }
168
+ dupes = []; counterHash.each { |k, v| dupes << k if v > 1 }
169
+ raise "Duplicate arguments for #{self} - [#{dupes.join(',')}]" unless dupes.empty?
170
+ @argSet = Set.new(args)
171
+ updateKey
172
+ self
173
+ end
174
+ end
175
+
176
+ =begin rdoc
177
+ The record attribute with the ordered list of arguments.
178
+ See RecAttrSet for the unordered set implementation.
179
+ =end
180
+ class RecAttrList < RecAttr
181
+
182
+ # Engages the super's parse method via the alias
183
+ alias :recAttrParse :parse
184
+
185
+ # Creates an instance with the given keyword
186
+ def initialize(keyword); super(keyword) end
187
+
188
+ # Builds the unique key for the list of arguments on the instance
189
+ def getKey; @args.map { |a| a.to_s }.join(':').to_sym end
190
+
191
+ =begin rdoc
192
+ Parses the instance from the given source preserving the order of arguments, returns self for call chaining.
193
+ * Parameters
194
+ * +src+ - an instance of SourceFile
195
+ =end
196
+ def parse(src)
197
+ recAttrParse(src)
198
+ updateKey
199
+ self
200
+ end
201
+ end
202
+
203
+ =begin rdoc
204
+ Record attrubute "<tt>unique</tt>"
205
+ =end
206
+ class RecUnique < RecAttrSet
207
+
208
+ # Creates an instance with the keyword "<tt>unique</tt>"
209
+ def initialize
210
+ #noinspection RubyArgCount
211
+ super(UNIQUE)
212
+ end
213
+
214
+ =begin rdoc
215
+ Attempts to consume the "<tt>unique</tt>" attribute for the given Record from the given source.
216
+ * Parameters
217
+ * +source+ - an instance of SourceFile
218
+ =end
219
+ def self.consumed?(source, record)
220
+ source.line =~ /^#{UNIQUE}\W.+$/ ? record.addUnique(RecUnique.new.parse(source)) : nil
221
+ end
222
+ end
223
+
224
+ =begin rdoc
225
+ Record attrubute "<tt>identity</tt>"
226
+ =end
227
+ class RecIdentity < RecAttrSet
228
+
229
+ # Creates an instance with the keyword "<tt>identity</tt>"
230
+ def initialize
231
+ #noinspection RubyArgCount
232
+ super(IDENTITY)
233
+ end
234
+
235
+ =begin rdoc
236
+ Attempts to consume the "<tt>identity</tt>" attribute for the given Record from the given source.
237
+ * Parameters
238
+ * +source+ - an instance of SourceFile
239
+ =end
240
+ def self.consumed?(source, record)
241
+ source.line =~ /^#{IDENTITY}\W+.+$/ ? record.identity = RecIdentity.new.parse(source) : nil
242
+ end
243
+ end
244
+
245
+ =begin rdoc
246
+ Record attrubute "<tt>index</tt>"
247
+ =end
248
+ class RecIndex < RecAttrList
249
+
250
+ # Creates an instance with the keyword "<tt>index</tt>"
251
+ def initialize
252
+ #noinspection RubyArgCount
253
+ super(INDEX)
254
+ end
255
+
256
+ =begin rdoc
257
+ Attempts to consume the "<tt>index</tt>" attribute for the given Record from the given source.
258
+ * Parameters
259
+ * +source+ - an instance of SourceFile
260
+ =end
261
+ def self.consumed?(source, record)
262
+ source.line =~ /^#{INDEX}\W+.+$/ ? record.addIndex(RecIndex.new.parse(source)) : nil
263
+ end
264
+ end
265
+
266
+ =begin rdoc
267
+ An array of record level parse token classes, namely RecIdentity, RecIndex, RecUnique
268
+ =end
269
+ RECORD_LEVEL_TOKENS=[RecIdentity, RecIndex, RecUnique]
270
+
271
+ end