fixture_fox 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/README.md +36 -0
- data/Rakefile +6 -0
- data/TODO +79 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/diagram.drawio +1 -0
- data/examples/1-1-subrecord.fox +15 -0
- data/examples/1-1-subrecord.sql +22 -0
- data/examples/N-M.fox +25 -0
- data/examples/N-M.sql +34 -0
- data/examples/anchors.sql +13 -0
- data/examples/anchors.yml +4 -0
- data/examples/anchors1.fox +4 -0
- data/examples/anchors2.fox +6 -0
- data/examples/array.fox +84 -0
- data/examples/array.sql +53 -0
- data/examples/base.fox +81 -0
- data/examples/base.sql +52 -0
- data/examples/empty.fox +8 -0
- data/examples/empty.sql +33 -0
- data/examples/include/schema-included.fox +23 -0
- data/examples/inherit.fox +26 -0
- data/examples/inherit.sql +28 -0
- data/examples/kind.fox +17 -0
- data/examples/kind.sql +31 -0
- data/examples/link.fox +35 -0
- data/examples/link.sql +32 -0
- data/examples/root.fox +22 -0
- data/examples/schema-fragment-1.fox +9 -0
- data/examples/schema-fragment-2.fox +21 -0
- data/examples/schema-include.fox +10 -0
- data/examples/schema-indent.fox +29 -0
- data/examples/schema.fox +29 -0
- data/examples/schema.sql +33 -0
- data/examples/types.fox +8 -0
- data/examples/types.sql +17 -0
- data/examples/views.fox +15 -0
- data/examples/views.sql +38 -0
- data/exe/fox +178 -0
- data/fixture_fox.gemspec +37 -0
- data/lib/fixture_fox/analyzer.rb +371 -0
- data/lib/fixture_fox/anchor.rb +93 -0
- data/lib/fixture_fox/ast.rb +176 -0
- data/lib/fixture_fox/error.rb +23 -0
- data/lib/fixture_fox/hash_parser.rb +111 -0
- data/lib/fixture_fox/idr.rb +62 -0
- data/lib/fixture_fox/line.rb +217 -0
- data/lib/fixture_fox/parser.rb +153 -0
- data/lib/fixture_fox/token.rb +173 -0
- data/lib/fixture_fox/tokenizer.rb +78 -0
- data/lib/fixture_fox/version.rb +3 -0
- data/lib/fixture_fox.rb +216 -0
- metadata +227 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
class HashParser
|
2
|
+
def self.parse(s)
|
3
|
+
self.new(s).send(:parse)
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
def initialize(s)
|
8
|
+
@s, @i = s, 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse
|
12
|
+
@s[@i] == "{" or raise "Missing initial '{' in hash literal: #{@s.inspect}"
|
13
|
+
@i += 1
|
14
|
+
pairs = []
|
15
|
+
while @i < @s.size && @s[@i] !~ /[,\}]/
|
16
|
+
pairs << extract_key_value
|
17
|
+
extract_element_separator
|
18
|
+
end
|
19
|
+
@s[@i] == "}" or raise "Missing terminating '}' in hash literal: #{@s.inspect}"
|
20
|
+
Hash[*pairs.flatten]
|
21
|
+
end
|
22
|
+
|
23
|
+
def extract_element_separator
|
24
|
+
while @i < @s.size && @s[@i] =~ /\s/
|
25
|
+
@i += 1
|
26
|
+
end
|
27
|
+
if @i < @s.size && @s[@i] == ","
|
28
|
+
@i += 1
|
29
|
+
while @i < @s.size && @s[@i] =~ /\s/
|
30
|
+
@i += 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def extract_key_value
|
36
|
+
key = extract_key
|
37
|
+
extract_separator
|
38
|
+
value = extract_value
|
39
|
+
[key.to_sym, value]
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_key
|
43
|
+
key = ""
|
44
|
+
while @i < @s.size && @s[@i] =~ /\w/
|
45
|
+
key += @s[@i]
|
46
|
+
@i += 1
|
47
|
+
end
|
48
|
+
key
|
49
|
+
end
|
50
|
+
|
51
|
+
def extract_separator
|
52
|
+
while @i < @s.size && @s[@i] =~ /[\s:]/
|
53
|
+
@i += 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def extract_value
|
58
|
+
if @s[@i] =~ /['"]/
|
59
|
+
extract_quoted_value
|
60
|
+
else
|
61
|
+
extract_unquoted_value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def extract_quoted_value
|
66
|
+
quot = @s[@i]
|
67
|
+
@i += 1
|
68
|
+
|
69
|
+
litt = ""
|
70
|
+
while @i < @s.size && @s[@i] != quot
|
71
|
+
if @s[@i] == "\\"
|
72
|
+
@i += 1
|
73
|
+
@i < @s.size or raise "Syntax error in hash literal: #{@s.inspect}"
|
74
|
+
case @s[@i]
|
75
|
+
when "\\"; litt += "\\"
|
76
|
+
when quot; litt += quot
|
77
|
+
else
|
78
|
+
litt += "\\" + @s[@i]
|
79
|
+
end
|
80
|
+
else
|
81
|
+
litt += @s[@i]
|
82
|
+
end
|
83
|
+
@i += 1
|
84
|
+
end
|
85
|
+
|
86
|
+
@i < @s.size && @s[@i] == quot or raise "Unterminated hash literal: #{@s.inspect}"
|
87
|
+
@i += 1
|
88
|
+
return litt
|
89
|
+
end
|
90
|
+
|
91
|
+
def extract_unquoted_value
|
92
|
+
j = @i + 1
|
93
|
+
while j < @s.size && @s[j] !~ /[,\}]/
|
94
|
+
j += 1
|
95
|
+
end
|
96
|
+
litt = @s[@i...j].strip
|
97
|
+
@i = j
|
98
|
+
value =
|
99
|
+
case litt
|
100
|
+
when "nil"; nil
|
101
|
+
when "true"; true
|
102
|
+
when "false"; false
|
103
|
+
when /^\d+$/; litt.to_i
|
104
|
+
else
|
105
|
+
litt.strip
|
106
|
+
end
|
107
|
+
value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
module FixtureFox
|
5
|
+
class Idr
|
6
|
+
# Qualified names of controlled tables
|
7
|
+
def tables() @tables_hash.keys end
|
8
|
+
|
9
|
+
# List of materialized views that depends on the tables. Assigned by the analyzer
|
10
|
+
# FIXME: Is this in use?
|
11
|
+
attr_accessor :materialized_views
|
12
|
+
|
13
|
+
# Data as a hash from schema to table to id to record to field to value.
|
14
|
+
# Ie. { "schema" => { "table" => { 1 => { id: 1, name: "Alice" } } } }
|
15
|
+
attr_reader :data
|
16
|
+
|
17
|
+
# Map from qualified table name to last used ID. FIXME: Unused?
|
18
|
+
attr_reader :ids
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@tables_hash = {}
|
22
|
+
@data = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Number of records
|
26
|
+
def count()
|
27
|
+
count = 0
|
28
|
+
@data.each { |_, tables| tables.each { |_, records| count += records.size } }
|
29
|
+
count
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add a field to the data representation. If field and value are nil, an
|
33
|
+
# empty record will be created. If id is also nil, an empty table is
|
34
|
+
# created
|
35
|
+
#
|
36
|
+
def put(schema, table, id = nil, field = nil, value = nil)
|
37
|
+
# puts "Idr.put(#{schema}, #{table}, #{id.inspect}, #{field.inspect}, #{value.inspect})"
|
38
|
+
!id.nil? || field.nil? or raise ArgumentError
|
39
|
+
raise if table.to_s =~ /[A-Z]/
|
40
|
+
uid = "#{schema}.#{table}"
|
41
|
+
@tables_hash[uid] = true
|
42
|
+
return if id.nil?
|
43
|
+
tuple = ((@data[schema] ||= {})[table] ||= {})[id] ||= {id: id}
|
44
|
+
tuple[field.to_sym] = value if field
|
45
|
+
end
|
46
|
+
|
47
|
+
alias_method :to_h, :data
|
48
|
+
|
49
|
+
def dump
|
50
|
+
data.sort_by(&:first).each { |schema, tables|
|
51
|
+
puts schema
|
52
|
+
tables.each { |table, records|
|
53
|
+
puts " #{table}"
|
54
|
+
records.each { |id, fields|
|
55
|
+
puts " #{fields.inspect}"
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,217 @@
|
|
1
|
+
module FixtureFox
|
2
|
+
class TokenizedLine
|
3
|
+
attr_reader :file
|
4
|
+
attr_reader :line
|
5
|
+
attr_reader :lineno
|
6
|
+
attr_reader :initial_indent
|
7
|
+
attr_reader :indent
|
8
|
+
|
9
|
+
def initialize(file, lineno, initial_indent, indent, line)
|
10
|
+
@file, @line, @lineno, @initial_indent, @indent = file, line.dup, lineno, initial_indent, indent
|
11
|
+
@pos = 1 + @indent # Used to keep track of position for the make_* methods
|
12
|
+
end
|
13
|
+
|
14
|
+
def error(msg)
|
15
|
+
raise ParseError.new(@file, @lineno, @pos + initial_indent, msg)
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s(long: false)
|
19
|
+
if long
|
20
|
+
"#{file}:#{lineno} "
|
21
|
+
else
|
22
|
+
"line #{lineno} "
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
def make_token(litt)
|
28
|
+
Token.new(file, lineno, initial_indent, @pos, litt)
|
29
|
+
end
|
30
|
+
|
31
|
+
def make_ident(litt)
|
32
|
+
Ident.new(file, lineno, initial_indent, @pos, litt)
|
33
|
+
end
|
34
|
+
|
35
|
+
def make_empty(litt)
|
36
|
+
Empty.new(file, lineno, initial_indent, @pos, litt)
|
37
|
+
end
|
38
|
+
|
39
|
+
def make_value(litt)
|
40
|
+
Value.new(file, lineno, initial_indent, @pos, litt)
|
41
|
+
end
|
42
|
+
|
43
|
+
def make_directive(litt)
|
44
|
+
Directive.new(file, lineno, initial_indent, @pos, litt)
|
45
|
+
end
|
46
|
+
|
47
|
+
def make_anchor(litt)
|
48
|
+
AnchorToken.new(@file, @lineno, initial_indent, @pos, litt)
|
49
|
+
end
|
50
|
+
|
51
|
+
def make_reference(litt)
|
52
|
+
Reference.new(@file, @lineno, initial_indent, @pos, litt)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class DirectiveLine < TokenizedLine
|
57
|
+
attr_reader :directive # Directive token
|
58
|
+
attr_reader :argument # Value token
|
59
|
+
|
60
|
+
def initialize(file, lineno, initial_indent, indent, line)
|
61
|
+
super
|
62
|
+
case line
|
63
|
+
when /^(@schema)(\s+)(\w+)\s*(?:#.*)?$/
|
64
|
+
@directive = make_directive($1)
|
65
|
+
@pos += $1.size + $2.size
|
66
|
+
@argument = make_value($3)
|
67
|
+
when /^(@include)(\s+)(\S+)\s*(?:#.*)?$/
|
68
|
+
@directive = make_directive($1)
|
69
|
+
@pos += $1.size + $2.size
|
70
|
+
@argument = make_value($3)
|
71
|
+
when /^(@anchors)(\s+)(\S+)\s*(?:#.*)?$/
|
72
|
+
@directive = make_directive($1)
|
73
|
+
@pos += $1.size + $2.size
|
74
|
+
@argument = make_value($3)
|
75
|
+
else
|
76
|
+
error("Illegal directive")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_s(long: false)
|
81
|
+
super +
|
82
|
+
if long
|
83
|
+
"directive:#{directive.pos} #{directive.litt}, argument:#{argument.pos} #{argument.litt}"
|
84
|
+
else
|
85
|
+
"#{directive.litt} #{argument.litt}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Line < TokenizedLine
|
91
|
+
attr_reader :dash
|
92
|
+
attr_reader :ident
|
93
|
+
attr_reader :empty # Empty token
|
94
|
+
attr_reader :value
|
95
|
+
attr_reader :reference
|
96
|
+
attr_reader :anchor
|
97
|
+
|
98
|
+
attr_reader :directive # SchemaDirective token
|
99
|
+
|
100
|
+
def initialize(file, lineno, initial_indent, indent, line)
|
101
|
+
super
|
102
|
+
|
103
|
+
# Remove comments not preceded by a quote (' or ")
|
104
|
+
if line =~ /^([^'"#]*?)\s*#.*/
|
105
|
+
line = $1
|
106
|
+
end
|
107
|
+
|
108
|
+
# Parse root element
|
109
|
+
if indent == 0 && line[0] != "-"
|
110
|
+
# Table
|
111
|
+
case line
|
112
|
+
when /^(\w+(?:\.\w+)?)$/
|
113
|
+
@ident = make_ident($1)
|
114
|
+
return
|
115
|
+
when /^(\w+(?:\.\w+)?)(\s*:\s*)(\[\])$/
|
116
|
+
@ident = make_ident($1)
|
117
|
+
@pos += $1.size + $2.size
|
118
|
+
@empty = make_empty($3)
|
119
|
+
return
|
120
|
+
end
|
121
|
+
|
122
|
+
# Check for missing value in root record
|
123
|
+
if line =~ /^(\w+\s*:)$/
|
124
|
+
error "Table name expected"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Parse dash
|
129
|
+
case line
|
130
|
+
when /^-$/
|
131
|
+
error "Entry content expected"
|
132
|
+
when /^(-\s+)(&\w+.*)/
|
133
|
+
@dash = make_token("-")
|
134
|
+
@pos += $1.size
|
135
|
+
@anchor = make_anchor($2.sub(/#.*/, ""))
|
136
|
+
return
|
137
|
+
when /^(-\s+)(.*)/
|
138
|
+
@dash = make_token("-")
|
139
|
+
@pos += $1.size
|
140
|
+
line = $2
|
141
|
+
when /^-./
|
142
|
+
@pos += 1
|
143
|
+
error "Illegal character after '-'"
|
144
|
+
end
|
145
|
+
|
146
|
+
# Expect key/value pair
|
147
|
+
case line
|
148
|
+
when /^(\w+)(\s*):\s*$/ # Record
|
149
|
+
@ident = make_ident($1)
|
150
|
+
@pos += $1.size + $2.size + 1
|
151
|
+
when /^(\w+)(\s*:\s*)(&\w+)(\s*)(\S.*)?$/ # Anchor
|
152
|
+
@ident = make_ident($1)
|
153
|
+
@pos += $1.size + $2.size
|
154
|
+
@anchor = make_anchor($3)
|
155
|
+
@pos += $3.size + $4.size
|
156
|
+
$5.nil? or error "Illegal character after anchor"
|
157
|
+
when /^(\w+)(\s*:\s*)(\*.+)(\s*)(\S.*)?/ # Reference
|
158
|
+
@ident = make_ident($1)
|
159
|
+
@pos += $1.size + $2.size
|
160
|
+
@reference = make_reference($3)
|
161
|
+
@pos += $3.size + $4.size
|
162
|
+
$5.nil? or error "Illegal character after reference"
|
163
|
+
when /^(\w+)(\s*:\s*)(["']?.+)/ # Simple value
|
164
|
+
@ident = make_ident($1)
|
165
|
+
@pos += $1.size + $2.size
|
166
|
+
@value = make_value($3)
|
167
|
+
when /^(\w+)(\s*:\s*)(\[.*)/ # Array value
|
168
|
+
@ident = make_ident($1)
|
169
|
+
@pos += $1.size + $2.size
|
170
|
+
litt =~ /.*\]\s*$/ or error "Missing ']'"
|
171
|
+
@value = make_value($3)
|
172
|
+
when /^(\w+)(\s*:\s*)(\{.*)/ # Hash value
|
173
|
+
@ident = make_ident($1)
|
174
|
+
@pos += $1.size + $2.size
|
175
|
+
litt =~ /.*\]\s*$/ or error "Missing '}'"
|
176
|
+
@value = make_value($3)
|
177
|
+
when /^(\*.+)(\s*)(\S.*)?$/
|
178
|
+
@reference = make_reference($1)
|
179
|
+
@pos += $1.size + $2.size
|
180
|
+
$3.nil? or error "Illegal character after reference"
|
181
|
+
when /^\[.*\]\s*$/
|
182
|
+
else
|
183
|
+
@pos += line.size
|
184
|
+
error "Missing value"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# True if the line is the first line of a root table
|
189
|
+
def root_table?() indent == 0 && dash.nil? && value.nil? end
|
190
|
+
|
191
|
+
# True if the line is the first line of a root record
|
192
|
+
def root_record?() indent == 0 && dash.nil? && !value.nil? || false end
|
193
|
+
|
194
|
+
# True if the line is the first line of a table element
|
195
|
+
def element?() !dash.nil? end
|
196
|
+
|
197
|
+
def to_s(long: false)
|
198
|
+
super +
|
199
|
+
if long
|
200
|
+
[ dash && "dash:#{dash.pos}",
|
201
|
+
ident && "ident:#{ident.pos} #{ident.value}",
|
202
|
+
value && "value:#{value.pos} #{value.value}",
|
203
|
+
reference && "reference:#{reference.pos} #{reference.value}",
|
204
|
+
anchor && "anchor:##{anchor.pos} #{anchor.value}"
|
205
|
+
]
|
206
|
+
else
|
207
|
+
[ dash && "dash",
|
208
|
+
ident && "ident: #{ident.to_s}",
|
209
|
+
value && "value: #{value.to_s}",
|
210
|
+
reference && "reference: #{reference.to_s}",
|
211
|
+
anchor && "anchor: #{anchor.to_s}"
|
212
|
+
]
|
213
|
+
end.compact.join(", ")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
@@ -0,0 +1,153 @@
|
|
1
|
+
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module FixtureFox
|
5
|
+
class Parser
|
6
|
+
attr_reader :file
|
7
|
+
attr_reader :ast
|
8
|
+
attr_reader :anchor_files # Name of external anchor files from @anchors directive
|
9
|
+
|
10
|
+
def initialize(file, lines, schema: nil)
|
11
|
+
@file = file
|
12
|
+
@lines = lines
|
13
|
+
@schema = schema || "public"
|
14
|
+
@anchor_files = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(ast = nil)
|
18
|
+
@ast = ast || Ast.new(file)
|
19
|
+
|
20
|
+
# Current schema. The schema is initialized with a synthetic Ident token
|
21
|
+
# because the #analyzer needs a token to emit an error message if the
|
22
|
+
# public schema doesn't exist
|
23
|
+
schema = Ident.new(file, 1, peek.initial_indent, 1, @schema)
|
24
|
+
|
25
|
+
# Parse
|
26
|
+
get_line
|
27
|
+
while line
|
28
|
+
case line
|
29
|
+
when DirectiveLine
|
30
|
+
case line.directive.value
|
31
|
+
when "schema"
|
32
|
+
schema = line.argument
|
33
|
+
get_line
|
34
|
+
when "include"
|
35
|
+
parse_included_file(line.argument.value)
|
36
|
+
get_line
|
37
|
+
when "anchors" # TODO: Remove. Replaced by a command line option
|
38
|
+
@anchor_files << line.argument.value
|
39
|
+
get_line
|
40
|
+
end
|
41
|
+
|
42
|
+
when Line
|
43
|
+
if line.root_table? && line.empty
|
44
|
+
table = AstTable.new(@ast, schema, line.ident)
|
45
|
+
get_line
|
46
|
+
elsif line.root_table?
|
47
|
+
peek && peek.element? or line.error("Table definition expected")
|
48
|
+
table = AstTable.new(@ast, schema, line.ident)
|
49
|
+
get_line
|
50
|
+
parse_elements(table)
|
51
|
+
elsif line.root_record?
|
52
|
+
peek && peek.indent > 0 or line.error("Record definition expected")
|
53
|
+
# The tokenizer recognizes root records as fields (where the
|
54
|
+
# table name is tokenized as a Value - ie. a text). The following
|
55
|
+
# recreates the table name as a Token and also embeds the record
|
56
|
+
# in an AstTable. This effectively transforms a root-level record
|
57
|
+
# definition into a single-row table definition
|
58
|
+
# table_ident = Ident.new(
|
59
|
+
# file, line.value.lineno, line.initial_indent, line.value.pos, line.value.value)
|
60
|
+
name = PgGraph.inflector.record_type2table(line.value.value)
|
61
|
+
table_ident = Ident.new(
|
62
|
+
file, line.value.lineno, line.initial_indent, line.value.pos, name)
|
63
|
+
table = AstTable.new(@ast, schema, table_ident)
|
64
|
+
record = AstRecordElement.new(table, AnchorToken.of_ident(line.ident))
|
65
|
+
get_line
|
66
|
+
parse_members(record, line.indent)
|
67
|
+
else
|
68
|
+
line.error("Table or record definition expected")
|
69
|
+
end
|
70
|
+
else
|
71
|
+
raise "Oops"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@ast
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def line() @line end
|
80
|
+
def get_line() @line = @lines.shift end
|
81
|
+
def peek() @lines.first end
|
82
|
+
|
83
|
+
def parse_included_file(path)
|
84
|
+
include_path = Pathname.new(path)
|
85
|
+
if include_path.absolute?
|
86
|
+
include_file = include_path.to_s
|
87
|
+
else
|
88
|
+
including_dir = Pathname.new(file).expand_path.dirname
|
89
|
+
include_file =
|
90
|
+
Pathname.new(including_dir.to_s + "/" + include_path.to_s)
|
91
|
+
.cleanpath
|
92
|
+
.relative_path_from(Pathname.getwd).to_s
|
93
|
+
end
|
94
|
+
tokenizer = Tokenizer.new(include_file)
|
95
|
+
Parser.new(tokenizer.file, tokenizer.call).call(@ast)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Parse table elements. Current line should be the first element
|
99
|
+
def parse_elements(table)
|
100
|
+
element_indent = line.indent
|
101
|
+
while line && line.indent == element_indent && line.element?
|
102
|
+
if line.ident.nil? && line.reference
|
103
|
+
AstReferenceElement.new(table, line.reference)
|
104
|
+
get_line
|
105
|
+
elsif line.ident.nil? && line.anchor && !(peek && peek.indent > element_indent)
|
106
|
+
line.error("Empty record definition in table")
|
107
|
+
else
|
108
|
+
if !line.ident && line.anchor # - &label
|
109
|
+
record = AstRecordElement.new(table, line.anchor)
|
110
|
+
indent = line.anchor.pos - 1
|
111
|
+
get_line
|
112
|
+
parse_members(record, indent)
|
113
|
+
else # - key: value
|
114
|
+
record = AstRecordElement.new(table)
|
115
|
+
parse_members(record, line.ident.pos - 1, skip_first_check: true)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Parse record members. Current line should be the first member
|
122
|
+
#
|
123
|
+
# If :skip_first_check the first check of indent is skipped. It is used for
|
124
|
+
# table rows where the first line in a record has a prefixed dash
|
125
|
+
def parse_members(record, indent, skip_first_check: false)
|
126
|
+
constrain indent, Integer
|
127
|
+
while line
|
128
|
+
break if !skip_first_check && line&.indent < indent # Record ends here if line is outdented
|
129
|
+
skip_first_check = false
|
130
|
+
if peek && peek.indent >= indent && peek.element? # This is a table if next line is dashed
|
131
|
+
table = AstTableMember.new(record, line.ident)
|
132
|
+
get_line
|
133
|
+
parse_elements(table)
|
134
|
+
elsif peek && peek.indent > indent # This is a record if next line is indented
|
135
|
+
record_member = AstRecordMember.new(record, line.ident, line.anchor)
|
136
|
+
get_line
|
137
|
+
parse_members(record_member, line.indent)
|
138
|
+
elsif line.anchor && !(peek && peek.indent > indent) # Empty record with a label
|
139
|
+
line.error("Empty record definition")
|
140
|
+
elsif line.anchor && line.dash && !(peek && peek.indent == indent)
|
141
|
+
line.error("Empty record definition 2")
|
142
|
+
elsif line.reference
|
143
|
+
AstReferenceMember.new(record, line.ident, line.reference)
|
144
|
+
get_line
|
145
|
+
else
|
146
|
+
AstFieldMember.new(record, line.ident, line.value || line.reference)
|
147
|
+
get_line
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module FixtureFox
|
2
|
+
class Token
|
3
|
+
attr_reader :file
|
4
|
+
attr_reader :lineno
|
5
|
+
attr_reader :initial_indent
|
6
|
+
attr_reader :pos
|
7
|
+
attr_reader :litt
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
def initialize(file, lineno, initial_indent, pos, litt)
|
11
|
+
@file, @lineno, @initial_indent, @pos, @litt = file, lineno, initial_indent, pos, litt
|
12
|
+
@value = @litt
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(s) @litt == s end
|
16
|
+
def to_s() @litt end
|
17
|
+
|
18
|
+
def error(msg)
|
19
|
+
raise ParseError.new(file, lineno, initial_indent + pos, msg)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class AnchorToken < Token
|
24
|
+
def initialize(file, lineno, initial_indent, pos, litt)
|
25
|
+
super
|
26
|
+
@value = litt[1..-1].to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
# Convert an Ident token to a AnchorToken token
|
30
|
+
def self.of_ident(ident)
|
31
|
+
AnchorToken.new(ident.file, ident.lineno, ident.initial_indent, ident.pos, "&#{ident.litt}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Empty < Token
|
36
|
+
end
|
37
|
+
|
38
|
+
class Directive < Token
|
39
|
+
def initialize(file, lineno, initial_indent, pos, litt)
|
40
|
+
super
|
41
|
+
@value = litt[1..-1]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Ident < Token
|
46
|
+
end
|
47
|
+
|
48
|
+
class Reference < Token
|
49
|
+
# attr_reader :reference
|
50
|
+
def initialize(file, lineno, initial_indent, pos, litt)
|
51
|
+
super
|
52
|
+
@value = litt[1..-1]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Value < Token
|
57
|
+
attr_reader :klass # NilClass, TrueClass, Integer, Float, String, Array
|
58
|
+
|
59
|
+
def initialize(file, lineno, initial_indent, pos, litt)
|
60
|
+
super
|
61
|
+
|
62
|
+
i = 1
|
63
|
+
case litt
|
64
|
+
when /^(nil)/, /^(null)/
|
65
|
+
i += $1.size
|
66
|
+
@klass = NilClass
|
67
|
+
@value = nil
|
68
|
+
|
69
|
+
when /^(true)/, /^(false)/
|
70
|
+
i += $1.size
|
71
|
+
@klass = TrueClass
|
72
|
+
@value = ($1 == "true")
|
73
|
+
|
74
|
+
when /^([-+]?\d+\.\d+)/
|
75
|
+
i += $1.size
|
76
|
+
@klass = Float
|
77
|
+
@value = $1.to_f
|
78
|
+
|
79
|
+
when /^([-+]?\d+)/
|
80
|
+
i += $1.size
|
81
|
+
@klass = Integer
|
82
|
+
@value = $1.to_i
|
83
|
+
|
84
|
+
when /^"/
|
85
|
+
@value = ""
|
86
|
+
@klass = String
|
87
|
+
loop do
|
88
|
+
i < litt.size or self.error("Unterminated string")
|
89
|
+
if litt[i] == "\\"
|
90
|
+
if litt[i+1] == "\\"
|
91
|
+
@value += "\\"
|
92
|
+
i += 2
|
93
|
+
elsif litt[i+1] == '"'
|
94
|
+
@value += '"'
|
95
|
+
i += 2
|
96
|
+
elsif litt[i+1] == 'n'
|
97
|
+
@value += "\n"
|
98
|
+
i += 2
|
99
|
+
elsif litt[i+1] == 't'
|
100
|
+
@value += "\t"
|
101
|
+
i += 2
|
102
|
+
else
|
103
|
+
@value += "\\"
|
104
|
+
i += 1
|
105
|
+
end
|
106
|
+
elsif litt[i] == '"'
|
107
|
+
i += 1
|
108
|
+
break
|
109
|
+
else
|
110
|
+
@value += litt[i]
|
111
|
+
i += 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
when /^'/
|
116
|
+
@value = ""
|
117
|
+
@klass = String
|
118
|
+
loop do
|
119
|
+
i < litt.size or self.error("Unterminated string")
|
120
|
+
if litt[i] == "'"
|
121
|
+
if litt[i+1] == "'"
|
122
|
+
@value += "'"
|
123
|
+
i += 2
|
124
|
+
else
|
125
|
+
i += 1
|
126
|
+
break
|
127
|
+
end
|
128
|
+
else
|
129
|
+
@value += litt[i]
|
130
|
+
i += 1
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
when /^(\[\s*)(.*)\]$/
|
135
|
+
i += $1.size
|
136
|
+
elements = $2
|
137
|
+
matches = elements.split(/(\s*(?<!(?<!\\{2})\\),\s*)/)
|
138
|
+
|
139
|
+
@value = []
|
140
|
+
while element = matches.shift
|
141
|
+
unescaped_element = element.sub(/\\/, "")
|
142
|
+
@value << Value.new(file, lineno, initial_indent, i, unescaped_element).value
|
143
|
+
i += element.size + (matches.shift&.size || 0)
|
144
|
+
end
|
145
|
+
@klass = Array
|
146
|
+
|
147
|
+
when /^\{/
|
148
|
+
i += litt.size
|
149
|
+
@value = HashParser.parse(litt)
|
150
|
+
@klass = Hash
|
151
|
+
|
152
|
+
else
|
153
|
+
i += litt.size
|
154
|
+
@klass = String
|
155
|
+
@value = litt.dup
|
156
|
+
end
|
157
|
+
|
158
|
+
(litt[i..-1] || "") =~ /^\s*(?:#.*)?$/ or self.error("Illegal characters after literal")
|
159
|
+
end
|
160
|
+
|
161
|
+
def to_s()
|
162
|
+
if klass == NilClass
|
163
|
+
"null"
|
164
|
+
# elsif klass == String
|
165
|
+
# "'#{value.gsub("'", "''")}'"
|
166
|
+
else
|
167
|
+
value.to_s
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|