fixture_fox 0.1.1

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