require 'pathname' module FixtureFox class Parser attr_reader :file attr_reader :ast attr_reader :anchor_files # Name of external anchor files from @anchors directive def initialize(file, lines, schema: nil) @file = file @lines = lines @schema = schema || "public" @anchor_files = [] end def call(ast = nil) @ast = ast || Ast.new(file) # Current schema. The schema is initialized with a synthetic Ident token # because the #analyzer needs a token to emit an error message if the # public schema doesn't exist schema = Ident.new(file, 1, peek.initial_indent, 1, @schema) # Parse get_line while line case line when DirectiveLine case line.directive.value when "schema" schema = line.argument get_line when "include" parse_included_file(line.argument.value) get_line when "anchors" # TODO: Remove. Replaced by a command line option @anchor_files << line.argument.value get_line end when Line if line.root_table? && line.empty table = AstTable.new(@ast, schema, line.ident) get_line elsif line.root_table? peek && peek.element? or line.error("Table definition expected") table = AstTable.new(@ast, schema, line.ident) get_line parse_elements(table) elsif line.root_record? peek && peek.indent > 0 or line.error("Record definition expected") # The tokenizer recognizes root records as fields (where the # table name is tokenized as a Value - ie. a text). The following # recreates the table name as a Token and also embeds the record # in an AstTable. This effectively transforms a root-level record # definition into a single-row table definition # table_ident = Ident.new( # file, line.value.lineno, line.initial_indent, line.value.pos, line.value.value) name = PgGraph.inflector.record_type2table(line.value.value) table_ident = Ident.new( file, line.value.lineno, line.initial_indent, line.value.pos, name) table = AstTable.new(@ast, schema, table_ident) record = AstRecordElement.new(table, AnchorToken.of_ident(line.ident)) get_line parse_members(record, line.indent) else line.error("Table or record definition expected") end else raise "Oops" end end @ast end private def line() @line end def get_line() @line = @lines.shift end def peek() @lines.first end def parse_included_file(path) include_path = Pathname.new(path) if include_path.absolute? include_file = include_path.to_s else including_dir = Pathname.new(file).expand_path.dirname include_file = Pathname.new(including_dir.to_s + "/" + include_path.to_s) .cleanpath .relative_path_from(Pathname.getwd).to_s end tokenizer = Tokenizer.new(include_file) Parser.new(tokenizer.file, tokenizer.call).call(@ast) end # Parse table elements. Current line should be the first element def parse_elements(table) element_indent = line.indent while line && line.indent == element_indent && line.element? if line.ident.nil? && line.reference AstReferenceElement.new(table, line.reference) get_line elsif line.ident.nil? && line.anchor && !(peek && peek.indent > element_indent) line.error("Empty record definition in table") else if !line.ident && line.anchor # - &label record = AstRecordElement.new(table, line.anchor) indent = line.anchor.pos - 1 get_line parse_members(record, indent) else # - key: value record = AstRecordElement.new(table) parse_members(record, line.ident.pos - 1, skip_first_check: true) end end end end # Parse record members. Current line should be the first member # # If :skip_first_check the first check of indent is skipped. It is used for # table rows where the first line in a record has a prefixed dash def parse_members(record, indent, skip_first_check: false) constrain indent, Integer while line break if !skip_first_check && line&.indent < indent # Record ends here if line is outdented skip_first_check = false if peek && peek.indent >= indent && peek.element? # This is a table if next line is dashed table = AstTableMember.new(record, line.ident) get_line parse_elements(table) elsif peek && peek.indent > indent # This is a record if next line is indented record_member = AstRecordMember.new(record, line.ident, line.anchor) get_line parse_members(record_member, line.indent) elsif line.anchor && !(peek && peek.indent > indent) # Empty record with a label line.error("Empty record definition") elsif line.anchor && line.dash && !(peek && peek.indent == indent) line.error("Empty record definition 2") elsif line.reference AstReferenceMember.new(record, line.ident, line.reference) get_line else AstFieldMember.new(record, line.ident, line.value || line.reference) get_line end end end end end