fixture_fox 0.1.1

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +7 -0
  7. data/README.md +36 -0
  8. data/Rakefile +6 -0
  9. data/TODO +79 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/doc/diagram.drawio +1 -0
  13. data/examples/1-1-subrecord.fox +15 -0
  14. data/examples/1-1-subrecord.sql +22 -0
  15. data/examples/N-M.fox +25 -0
  16. data/examples/N-M.sql +34 -0
  17. data/examples/anchors.sql +13 -0
  18. data/examples/anchors.yml +4 -0
  19. data/examples/anchors1.fox +4 -0
  20. data/examples/anchors2.fox +6 -0
  21. data/examples/array.fox +84 -0
  22. data/examples/array.sql +53 -0
  23. data/examples/base.fox +81 -0
  24. data/examples/base.sql +52 -0
  25. data/examples/empty.fox +8 -0
  26. data/examples/empty.sql +33 -0
  27. data/examples/include/schema-included.fox +23 -0
  28. data/examples/inherit.fox +26 -0
  29. data/examples/inherit.sql +28 -0
  30. data/examples/kind.fox +17 -0
  31. data/examples/kind.sql +31 -0
  32. data/examples/link.fox +35 -0
  33. data/examples/link.sql +32 -0
  34. data/examples/root.fox +22 -0
  35. data/examples/schema-fragment-1.fox +9 -0
  36. data/examples/schema-fragment-2.fox +21 -0
  37. data/examples/schema-include.fox +10 -0
  38. data/examples/schema-indent.fox +29 -0
  39. data/examples/schema.fox +29 -0
  40. data/examples/schema.sql +33 -0
  41. data/examples/types.fox +8 -0
  42. data/examples/types.sql +17 -0
  43. data/examples/views.fox +15 -0
  44. data/examples/views.sql +38 -0
  45. data/exe/fox +178 -0
  46. data/fixture_fox.gemspec +37 -0
  47. data/lib/fixture_fox/analyzer.rb +371 -0
  48. data/lib/fixture_fox/anchor.rb +93 -0
  49. data/lib/fixture_fox/ast.rb +176 -0
  50. data/lib/fixture_fox/error.rb +23 -0
  51. data/lib/fixture_fox/hash_parser.rb +111 -0
  52. data/lib/fixture_fox/idr.rb +62 -0
  53. data/lib/fixture_fox/line.rb +217 -0
  54. data/lib/fixture_fox/parser.rb +153 -0
  55. data/lib/fixture_fox/token.rb +173 -0
  56. data/lib/fixture_fox/tokenizer.rb +78 -0
  57. data/lib/fixture_fox/version.rb +3 -0
  58. data/lib/fixture_fox.rb +216 -0
  59. metadata +227 -0
@@ -0,0 +1,371 @@
1
+ module FixtureFox
2
+ class Analyzer
3
+ attr_reader :ast
4
+ attr_reader :type
5
+ attr_reader :idr # Initialized by #call
6
+
7
+ # List of all known tables whether empty or not (PgGraph::Type::Table objects)
8
+ def tables() @tables.values end
9
+
10
+ # List of non-empty tables (PgGraph::Type::Table objects)
11
+ def data_tables() @data_tables.values end
12
+
13
+ attr_reader :records # [AstRecord]
14
+ attr_reader :record_refs # [AstRecordRef]
15
+ attr_reader :fields # [AstField]
16
+
17
+ # Map from qualified table name to maximum record ID
18
+ attr_reader :ids
19
+
20
+ # Like ids but includes only tables with records from the sources
21
+ # attr_reader :referenced_ids
22
+
23
+ # Anchors object. This includes both anchors defined in the sources and
24
+ # anchors supplied to #initialize
25
+ attr_reader :anchors
26
+
27
+ # Map from anchor name to anchors defined by the sources
28
+ attr_reader :defined_anchors
29
+
30
+ # Map from anchor name to anchors referenced by the sources. FIXME: Unused
31
+ attr_reader :referenced_anchors
32
+
33
+ # True iff types have been assigned
34
+ def assigned?() @assigned end
35
+
36
+ # True iff types have been checked
37
+ def checked?() @checked end
38
+
39
+ # True iff data has been generated
40
+ def generated?() !@idr.nil? end
41
+
42
+ def initialize(type, ast, ids: {}, anchors: Anchors.new(type))
43
+ constrain type, PgGraph::Type
44
+ constrain ast, Ast
45
+ constrain anchors, Anchors
46
+ @type = type
47
+ @ast = ast
48
+ @tables = {}
49
+ @data_tables = {}
50
+ @records = []
51
+ @record_refs = []
52
+ @fields = [] # List of all fields
53
+
54
+ @ids = Hash.new(0)
55
+ ids.each { |k,v| @ids[k] = v } # copy but preserve default value
56
+ @anchors = anchors
57
+ @defined_anchors = {}
58
+ @referenced_anchors = {}
59
+
60
+ @assigned = false
61
+ @checked = false
62
+ end
63
+
64
+ # TODO: Also generate data
65
+ def call
66
+ assign_types
67
+ check_types
68
+ self
69
+ end
70
+
71
+ # Assign types and collect fields, records, and anchors. Note that
72
+ # #assign_types and #check_types are separate methods to make it possible
73
+ # to analyze the AST just up to the point where external anchors are needed
74
+ # for further processing. This is used in the caching mechanism in Postspec
75
+ def assign_types
76
+ @assigned = true
77
+ assign_table_types
78
+ self
79
+ end
80
+
81
+ # Check types. Merges anchors: into self.anchors
82
+ def check_types(anchors: nil)
83
+ @checked = true
84
+ @anchors.merge!(anchors) if anchors
85
+ check_field_types
86
+ check_record_ref_types
87
+ self
88
+ end
89
+
90
+ # Generate IDR. Use self.ids if ids: is nil
91
+ def generate(ids: nil)
92
+ ids.each { |k,v| @ids[k] = v } if ids # copy while preserving default value in hash
93
+ generate_record_ids
94
+ generate_idr
95
+ @idr
96
+ end
97
+
98
+ private
99
+ def merge_anchor(record)
100
+ constrain record, AstRecord
101
+ name = record.anchor&.value
102
+ if !@anchors.key?(name)
103
+ @defined_anchors[name] = @anchors.create(name, record.type)
104
+ end
105
+ end
106
+
107
+ def assign_table_types
108
+ ast.tables.each { |ast_table|
109
+ @type.key?(ast_table.schema.value.downcase) or
110
+ ast_table.schema.error("Can't find schema '#{ast_table.schema}'")
111
+ type = ast_table.type = @type[ast_table.schema.value.downcase][ast_table.name.downcase] or
112
+ ast_table.ident.error(
113
+ "Can't find ast_table '#{ast_table.ident}' (maybe you forgot to declare a schema?)")
114
+ @tables[type.uid] = type
115
+ assign_record_types(ast_table)
116
+ }
117
+ end
118
+
119
+ def assign_record_types(ast_table)
120
+ constrain ast_table, AstTable, AstTableMember
121
+ ast_table.elements.each { |ast_record|
122
+ ast_record.type = ast_table.type.record_type
123
+ @data_tables[ast_record.type.table.uid] ||= ast_record.type.table
124
+ case ast_record
125
+ when AstRecordElement
126
+ records << ast_record
127
+ merge_anchor(ast_record) if ast_record.anchor
128
+ assign_field_types(ast_record)
129
+ when AstReferenceElement
130
+ record_refs << ast_record
131
+ else
132
+ raise "Oops"
133
+ end
134
+ }
135
+ end
136
+
137
+ def assign_field_types(ast_record)
138
+ ast_record.members.each { |field|
139
+ field.column = ast_record.type[field.ident.litt.downcase] or
140
+ field.ident.error("Can't find field '#{ast_record.type.table.name}.#{field.ident}'")
141
+ field.type = field.column.type
142
+ case field
143
+ when AstTableMember
144
+ assign_record_types(field)
145
+ when AstRecordMember
146
+ records << field
147
+ merge_anchor(field) if field.anchor
148
+ assign_field_types(field)
149
+ when AstFieldMember, AstReferenceMember
150
+ fields << field
151
+ else
152
+ raise "Oops"
153
+ end
154
+ }
155
+ end
156
+
157
+ def check_field_types
158
+ fields.each { |f|
159
+ case f
160
+ when AstFieldMember # FIXME: Doesn't handle multidimensional arrays
161
+ if f.type.array?
162
+ values = f.value.value
163
+ klass = f.type.element_type.ruby_class
164
+ elsif f.type.is_a?(PgGraph::Type::RecordType)
165
+ if f.column.is_a?(PgGraph::Type::KindRecordColumn)
166
+ values = [f.value.value]
167
+ klass = f.column.kind_column.type.ruby_class
168
+ else
169
+ f.value.error("Expected a record or a reference, got #{f.value.value.inspect}")
170
+ end
171
+ else
172
+ values = [f.value.value]
173
+ klass = f.type.ruby_class
174
+ end
175
+
176
+ values.each { |value|
177
+ if value.is_a?(klass) || !f.type.array? &&
178
+ klass == Time &&
179
+ value.is_a?(String) &&
180
+ value =~ /^\d\d\d\d-\d\d-\d\d(?: \d\d:\d\d(?::\d\d)?)?$/
181
+ ;
182
+ else
183
+ f.value.error("Data type mismatch - expected #{klass}, got #{value.class}")
184
+ end
185
+ }
186
+ when AstReferenceMember
187
+ check_ref(f)
188
+ else
189
+ raise "Oops"
190
+ end
191
+ }
192
+ end
193
+
194
+ def check_ref(ast_ref)
195
+ anchor = ast_ref.referenced_anchor = @anchors[ast_ref.reference.value.to_sym] or
196
+ ast_ref.reference.error("Can't find anchor for reference '#{ast_ref.reference.litt}'")
197
+ anchor_types =
198
+ [anchor.type] +
199
+ (anchor.type.table.subtable? ? [anchor.type.table.supertable.record_type] : [])
200
+ anchor_types.any? { |anchor_type| ast_ref.type == anchor_type } or
201
+ ast_ref.reference.error(
202
+ "Data type mismatch - expected a reference to a " \
203
+ "#{ast_ref.type.identifier} record, got #{anchor.type.identifier}")
204
+ end
205
+
206
+ def check_record_ref_types
207
+ record_refs.each { |record| check_ref(record) }
208
+ end
209
+
210
+ def check_record_kind_ref_types
211
+ end
212
+
213
+ def generate_record_ids
214
+ # puts "generate_record_ids"
215
+ # puts " ids: #{@ids}"
216
+ @supertable_records = [] # Array of pairs of supertable/id
217
+ records.each { |ast_record|
218
+ table = ast_record.type.table
219
+ table_uid = table.subtable? ? table.supertable.uid : table.uid
220
+
221
+ # If enclosing record/table is the supertable, then take the ID from there
222
+ if table.subtable?
223
+ parent_table = ast_record.parent.type.table
224
+ if table.supertable == parent_table # works for both Record and Table objects
225
+ id = @ids[table.uid] = @ids[parent_table.uid]
226
+
227
+ # Enclosing record/table is not the supertable so generate a new ID
228
+ # based on the current supertable ID and prepare a new supertable
229
+ # record
230
+ else
231
+ id = @ids[table.uid] = @ids[table.supertable.uid] += 1
232
+ @supertable_records << [table.supertable, id]
233
+ end
234
+ ast_record.id = id
235
+
236
+ # If record is a (labelled) root record then check if it has already been defined
237
+ # and take the ID from there
238
+ elsif name = ast_record.anchor&.value
239
+ @anchors.key?(name) or raise "Oops"
240
+ @referenced_anchors[name] ||= @anchors[name] if !@defined_anchors.key?(name)
241
+ ast_record.id = @anchors[name].id ||= @ids[table_uid] += 1
242
+
243
+ else
244
+ # Assign a new ID to the record
245
+ ast_record.id = @ids[table_uid] += 1
246
+ end
247
+
248
+ # Register anchor
249
+ if name = ast_record.anchor&.value
250
+ @anchors.key?(name) or raise "Oops"
251
+ @anchors[name].id = ast_record.id
252
+ # @referenced_anchors[name] ||= @anchors[name] if !@defined_anchors.key?(name)
253
+ # ast_record.id = @anchors[name].id ||= @ids[table_uid] += 1
254
+ end
255
+ }
256
+ end
257
+
258
+ def generate_idr
259
+ @idr = Idr.new
260
+
261
+ # Build all root tables. This makes sure empty tables are registered
262
+ ast.tables.each { |t| idr.put(t.schema, t.type.table.name) }
263
+
264
+ # Create implicit supertable records
265
+ @supertable_records.each { |table, id|
266
+ idr.put(table.schema.name, table.name, id)
267
+ }
268
+
269
+ # Create records by filling in the Idr field-by-field
270
+ records.each { |record|
271
+ record.members.each { |field|
272
+ table = nil # To bring table into scope. FIXME: Yt?
273
+ case field
274
+ when AstFieldMember
275
+ # puts "AstFieldMember: #{field}"
276
+ table = record.type.table
277
+ column = field.column.postgres_column
278
+ idr.put(table.schema.name, table.name, record.id, column, field.value.value)
279
+ when AstReferenceMember
280
+ # puts "AstReferenceMember: #{field}"
281
+ table = record.type.table
282
+ column = field.column.postgres_column
283
+ idr.put(table.schema.name, table.name, record.id, column, field.referenced_anchor.id)
284
+ when AstRecordMember
285
+ # puts "AstRecordMember: #{record.type}"
286
+ column = field.column
287
+
288
+ this_record_id = field.record.id
289
+ that_record_id = field.id
290
+
291
+ if column.reference? # The key is on this side
292
+ table = record.type.table
293
+ schema = table.schema
294
+ link_column = column.this_link_column
295
+ idr.put(schema.name, table.name, this_record_id, link_column, that_record_id)
296
+ else # The key is on the other side
297
+ table = column.type.table
298
+ schema = table.schema
299
+ link_column = column.that_link_column
300
+ idr.put(schema.name, table.name, that_record_id, link_column, this_record_id)
301
+ end
302
+ when AstTableMember
303
+ # puts "AstTableMember: #{field}"
304
+ if field.column.is_a?(PgGraph::Type::MmTableColumn)
305
+ table = mm_table = field.column.mm_table
306
+ this_mm_column = field.column.this_mm_column
307
+ that_mm_column = field.column.that_mm_column
308
+
309
+ field.elements.each { |r|
310
+ mm_id = @ids[mm_table.path] += 1
311
+ this_mm_column_id = record.id
312
+ that_mm_column_id = r.is_a?(AstReferenceElement) ? r.referenced_anchor.id : r.id
313
+ idr.put(mm_table.schema.name, mm_table.name, mm_id, this_mm_column, this_mm_column_id)
314
+ idr.put(mm_table.schema.name, mm_table.name, mm_id, that_mm_column, that_mm_column_id)
315
+ }
316
+ else
317
+ table = record_table = field.column.type.table
318
+ record_column = field.column
319
+ field.elements.each { |r|
320
+ record_id = r.is_a?(AstReferenceElement) ? r.referenced_anchor.id : r.id
321
+ idr.put(
322
+ record_table.schema.name, record_table.name,
323
+ record_id, record_column.that_link_column, record.id)
324
+ }
325
+ end
326
+ else
327
+ raise "Oops: Unhandled case - #{field.class}"
328
+ end
329
+ }
330
+ }
331
+
332
+ idr.materialized_views = @tables.values.map(&:depending_materialized_views).flatten.uniq
333
+ end
334
+
335
+ def dump
336
+ records.sort { |l,r| l.type.identifier <=> r.type.identifier }.each { |r|
337
+ puts r.type.identifier
338
+ indent {
339
+ puts "id: #{r.id}"
340
+ r.members.each { |f|
341
+ print "#{f.ident}: #{f.type.identifier}"
342
+ case f
343
+ when AstField
344
+ if f.value.is_a?(Value)
345
+ print " = #{f.value.to_s}"
346
+ elsif f.value.is_a?(Reference)
347
+ print " = #{f.type.table.identifier}[#{f.referenced_anchor.id}]"
348
+ else
349
+ raise "Oops"
350
+ end
351
+ puts " (#{f.type.class})"
352
+ when AstRecord
353
+ puts " = #{r.type.table.identifier}[#{f.id}] (#{f.type.class})"
354
+ when AstTable
355
+ print " = ["
356
+ print f.records.map { |r| "#{r.type.table.identifier}[#{r.id}]" }.join(", ")
357
+ puts "] (#{f.class}, #{f.type.class})"
358
+ else
359
+ puts
360
+ end
361
+ }
362
+ }
363
+ }
364
+ end
365
+
366
+ def dump_anchors
367
+ @anchor_uids.map { |k,v| puts "#{k}: #{v}" }
368
+ end
369
+ end
370
+ end
371
+
@@ -0,0 +1,93 @@
1
+
2
+ module FixtureFox
3
+ class Anchor
4
+ attr_reader :name
5
+ attr_accessor :type
6
+ attr_accessor :id
7
+
8
+ def initialize(name, type, id: nil)
9
+ constrain name, Symbol
10
+ constrain type, PgGraph::Type::RecordType
11
+ constrain id, NilClass, Integer
12
+ @name, @type, @id = name, type, id
13
+ end
14
+
15
+ def uid() @uid ||= "#{table_uid}[#{id}]" end
16
+ def table_uid() type.table.uid end
17
+ end
18
+
19
+ # TODO: Remove Anchors and replace with a simple hash
20
+ class Anchors
21
+ attr_reader :type
22
+
23
+ # Initialize an Anchors object
24
+ #
25
+ # +anchors+ is an array of hashes of name, table, and id values. It is the
26
+ # same as the output from #to_yaml
27
+ def initialize(type, anchors = [])
28
+ constrain type, PgGraph::Type
29
+ constrain anchors, [Hash], NilClass
30
+ @type = type
31
+ @anchors = {}
32
+ (anchors || []).each { |anchor| create(anchor[:name], anchor[:table], id: anchor[:id]) }
33
+
34
+ # @anchors = anchors.map { |name, record_uid|
35
+ # raise "This is actually used!"
36
+ # table_uid, id = PgGraph::Data::Record.split_uid(record_uid)
37
+ # Anchor.new(name, type.dot(table_uid).record_type, id)
38
+ # }.to_h
39
+ end
40
+
41
+ forward_to :@anchors, :size, :empty?, :key?, :keys, :values, :each, :map
42
+
43
+ def create(name, arg, id: nil)
44
+ constrain name, Symbol
45
+ constrain arg, PgGraph::Type::RecordType, String
46
+ constrain id, NilClass, Integer # id is initially nil for anchors declared in the source
47
+ !@anchors.key?(name) or raise Error, "Duplicate anchor: #{name.inspect}"
48
+ type =
49
+ case arg
50
+ when PgGraph::Type::RecordType; arg
51
+ when String; @type.dot(arg) or raise Error, "Illegal path: #{path.inspect}"
52
+ end
53
+ @anchors[name] = Anchor.new(name, type.record_type, id: id)
54
+ end
55
+
56
+ def [](name)
57
+ constrain name, Symbol
58
+ @anchors[name]
59
+ end
60
+
61
+ def merge!(anchors)
62
+ constrain anchors, Anchors
63
+ type == anchors.type or raise Error, "anchor type mismatch"
64
+ @anchors.merge!(anchors.instance_variable_get(:@anchors))
65
+ self
66
+ end
67
+
68
+ def to_yaml
69
+ @anchors.values.map { |anchor| { name: anchor.name, table: anchor.table_uid, id: anchor.id } }
70
+ end
71
+
72
+ def save(filename)
73
+ IO.write(filename, YAML.dump(to_yaml))
74
+ end
75
+
76
+ def self.load(type, filename)
77
+ anchors = Anchors.new(type)
78
+ YAML.load(IO.read(filename)).each { |entry|
79
+ anchors.create(entry[:name], entry[:table], id: entry[:id])
80
+ }
81
+ anchors
82
+ end
83
+
84
+ def dump
85
+ puts "Anchors"
86
+ indent {
87
+ puts "type: #{type.uid}"
88
+ puts "anchors: #{@anchors.inspect}"
89
+ }
90
+ end
91
+ end
92
+ end
93
+
@@ -0,0 +1,176 @@
1
+ require "indented_io"
2
+
3
+ module FixtureFox
4
+ class AstNode
5
+ # Parent node. nil for the top-level Ast node
6
+ attr_reader :parent
7
+
8
+ # List of elements or members. Only non-empty for table and records nodes.
9
+ # Assigned by the parser
10
+ attr_reader :children
11
+
12
+ # Type of this node or of the node referenced by this node. nil for the
13
+ # top-level Ast node. Assigned by the analyzer
14
+ attr_accessor :type
15
+
16
+ def initialize(parent)
17
+ @parent = parent
18
+ @children = []
19
+ @parent.children << self if @parent
20
+ end
21
+
22
+ def dump
23
+ puts self.to_s
24
+ indent { children.each(&:dump) }
25
+ end
26
+
27
+ def inspect
28
+ "#<#{self.class}:#{object_id} @type=#{type&.uid.inspect}>"
29
+ # raise
30
+ end
31
+ end
32
+
33
+ class Ast < AstNode
34
+ alias_method :tables, :children
35
+
36
+ attr_reader :file
37
+
38
+ def initialize(file = nil) # file = nil is used in tests
39
+ super(nil)
40
+ @file = file
41
+ end
42
+
43
+ def to_s() "Ast" end
44
+ end
45
+
46
+ class AstTable < AstNode # Root table
47
+ alias_method :ast, :parent
48
+ alias_method :elements, :children # Initialized by the parser
49
+
50
+ attr_reader :ident # Ident token
51
+
52
+ attr_reader :schema # Ident token
53
+ attr_reader :name # String
54
+
55
+ attr_reader :dependent_materialized_views # Initialized by the analyzer
56
+
57
+ def initialize(ast, schema, ident)
58
+ super(ast)
59
+ @ident = ident
60
+ if @ident.value =~ /^(\w+)\.(\w+)$/
61
+ @schema = Ident.new(ident.file, ident.lineno, ident.initial_indent, ident.pos, $1)
62
+ @name = $2
63
+ else
64
+ @schema = schema
65
+ @name = ident.value
66
+ end
67
+ end
68
+
69
+ def to_s() "#{schema.value}.#{name}: AstTable" end
70
+ end
71
+
72
+ class AstElement < AstNode
73
+ alias_method :table, :parent
74
+ end
75
+
76
+ # Make AstRecordElement and AstRecordMember act as they're instances of AstRecord
77
+ module AstRecord; end
78
+
79
+ class AstRecordElement < AstElement # Table record element
80
+ include AstRecord
81
+
82
+ alias_method :members, :children
83
+
84
+ attr_reader :anchor # Anchor token. Can be nil
85
+ attr_accessor :id # Assigned by the analyzer
86
+
87
+ def initialize(table, anchor = nil)
88
+ super(table)
89
+ @anchor = anchor
90
+ end
91
+
92
+ def to_s() "AstRecordElement" + (anchor ? " #{anchor.litt}" : "") end
93
+ end
94
+
95
+ class AstReferenceElement < AstElement # Table reference element
96
+ attr_reader :reference
97
+ # attr_accessor :referenced_node # Initialized by the analyzer
98
+ attr_accessor :referenced_anchor # Tuple of [table_uid, id]
99
+
100
+ def initialize(table, reference)
101
+ super(table)
102
+ @reference = reference
103
+ end
104
+
105
+ def to_s() "AstReferenceElement #{reference.litt}" end
106
+ end
107
+
108
+ # A member of a record
109
+ class AstMember < AstNode
110
+ alias_method :record, :parent
111
+
112
+ attr_reader :ident # Ident token
113
+ attr_accessor :column # Initialized by the analyzer
114
+
115
+ def initialize(record, ident)
116
+ super(record)
117
+ @ident = ident
118
+ end
119
+ end
120
+
121
+ class AstTableMember < AstMember # Also root table
122
+ alias_method :elements, :children # Initialized by the parser
123
+
124
+ def to_s() "#{ident}: AstTableMember" end
125
+ end
126
+
127
+ class AstRecordMember < AstMember
128
+ include AstRecord
129
+
130
+ alias_method :members, :children
131
+
132
+ attr_reader :anchor # Can be nil
133
+ attr_accessor :id # Assigned by the analyzer
134
+
135
+ def initialize(record, ident, anchor = nil)
136
+ super(record, ident)
137
+ @anchor = anchor
138
+ end
139
+
140
+ def to_s() "#{ident}: AstRecordMember" + (anchor ? " #{anchor.litt}" : "") end
141
+ end
142
+
143
+ class AstReferenceMember < AstMember
144
+ attr_reader :reference # Reference token
145
+ # attr_accessor :referenced_node # Initialized by the analyzer
146
+ attr_accessor :referenced_anchor # Tuple of [table_uid, id]
147
+
148
+ def initialize(record, ident, reference)
149
+ super(record, ident)
150
+ @reference = reference
151
+ end
152
+
153
+ def to_s() "#{ident}: AstReferenceMember #{reference.litt}" end
154
+ end
155
+
156
+ class AstFieldMember < AstMember
157
+ attr_reader :value
158
+
159
+ def initialize(record, ident, value)
160
+ super(record, ident)
161
+ @value = value
162
+ end
163
+
164
+ def to_s() "#{ident}: #{value}" end
165
+ end
166
+ end
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+
@@ -0,0 +1,23 @@
1
+ module FixtureFox
2
+ class Error < StandardError
3
+ def error_message() message end
4
+ end
5
+
6
+ class ParseError < Error
7
+ attr_reader :file
8
+ attr_reader :lineno
9
+ attr_reader :pos
10
+
11
+ def initialize(file, lineno, pos, msg)
12
+ super(msg)
13
+ @file = file
14
+ @lineno = lineno
15
+ @pos = pos
16
+ end
17
+
18
+ def error_message()
19
+ [file, lineno, pos].compact.join(":") + " " + message
20
+ end
21
+ end
22
+ end
23
+