dataMetaDom 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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