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.
- 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
|
+
|