Parser.java

/***************************************************************************
   Copyright 2015 Emily Estes

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
***************************************************************************/
package net.metanotion.sqltest;


import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.metanotion.sqltest.parser.ParseException;
import net.metanotion.sqltest.parser.TestLogParser;
import net.metanotion.sqltest.parser.syntaxtree.Apply;
import net.metanotion.sqltest.parser.syntaxtree.Arr;
import net.metanotion.sqltest.parser.syntaxtree.Capture;
import net.metanotion.sqltest.parser.syntaxtree.Executable;
import net.metanotion.sqltest.parser.syntaxtree.Expressions;
import net.metanotion.sqltest.parser.syntaxtree.Imports;
import net.metanotion.sqltest.parser.syntaxtree.Key;
import net.metanotion.sqltest.parser.syntaxtree.Literal;
import net.metanotion.sqltest.parser.syntaxtree.Node;
import net.metanotion.sqltest.parser.syntaxtree.NodeSequence;
import net.metanotion.sqltest.parser.syntaxtree.Obj;
import net.metanotion.sqltest.parser.syntaxtree.Params;
import net.metanotion.sqltest.parser.syntaxtree.SideEffect;
import net.metanotion.sqltest.parser.syntaxtree.SimpleValue;
import net.metanotion.sqltest.parser.syntaxtree.TestExpression;
import net.metanotion.sqltest.parser.syntaxtree.TestStatement;
import net.metanotion.sqltest.parser.syntaxtree.ValueExpr;
import net.metanotion.sqltest.parser.syntaxtree.ValueList;
import net.metanotion.sqltest.parser.syntaxtree.WithParam;
import net.metanotion.sqltest.parser.syntaxtree.WithParams;

final class Parser {
	private static final Logger logger = LoggerFactory.getLogger(Parser.class);

	private String parseKey(final Key k) {
		final String s = k.f0.choice.toString();
		switch(k.f0.which) {
			case 0: // ID
				return s;
			case 1: // String
				return s.substring(1, s.length() - 1).replaceAll("\\\\\"", "\"").replaceAll("\\\\", "\\");
			case 2: // Single Quote string
				return s.substring(1, s.length() - 1).replaceAll("\\\\'", "'").replaceAll("\\\\", "\\");
			default:
				throw new RuntimeException("Bad column name for row literal " + k.f0.choice.toString());
		}
	}

	private void parseWP(final Map<String,Value> struct, final WithParam wp) {
		final String key = parseKey(wp.f0);
		final Value val = parseSimpleValue(wp.f2);
		struct.put(key, val);
	}

	private Value parseObj(final Obj obj) {
		logger.debug("Parsing Row");
		final Map<String,Value> struct = new HashMap<>();
		if(obj.f1.present()) {
			final WithParams wParams = (WithParams) obj.f1.node;
			parseWP(struct, wParams.f0);
			for(final Node n: wParams.f1.nodes) {
				parseWP(struct, (WithParam) ((NodeSequence) n).nodes.get(1));
			}
		}
		return new RowValue(struct);
	}

	private Value parseArr(final Arr arr) {
		logger.debug("Parsing List");
		final ArrayList<Value> list = new ArrayList<>();
		if(arr.f1.present()) {
			final ValueList vList = (ValueList) arr.f1.node;
			list.add(parseValue(vList.f0));
			for(final Node n: vList.f1.nodes) {
				list.add(parseValue((ValueExpr) ((NodeSequence) n).nodes.get(1)));
			}
		}
		return new ListValue(list);
	}

	private Value parseValue(final ValueExpr val) {
		switch(val.f0.which) {
			case 0: // Literal
				return parseLiteral((Literal) val.f0.choice);
			case 1: // Obj/struct
				return parseObj((Obj) val.f0.choice);
			case 2: // Arr/list
				return parseArr((Arr) val.f0.choice);
			case 3: // environment look up.
				return new VariableValue(val.f0.choice.toString());
			default:
				throw new RuntimeException("Bad Value " + val.f0.choice.toString());
		}
	}

	private GenerateValue parseSQL(final String sql) { return new ApplySql(sql); }

	private Value parseLiteral(final Literal lit) {
		final String s = lit.f0.choice.toString();
		switch(lit.f0.which) {
			case 0: // Decimal literal
				return new NumberValue(new BigDecimal(s));
			case 1: // Float literal
				return new NumberValue(new BigDecimal(s));
			case 2: // String literal
				return new StringValue(s.substring(1, s.length() - 1).replaceAll("\\\\\"", "\"").replaceAll("\\\\", "\\"));
			case 3: // Single quote string literal.
				return new StringValue(s.substring(1, s.length() - 1).replaceAll("\\\\'", "'").replaceAll("\\\\", "\\"));
			case 4: // Boolean literal
				return new BooleanValue("true".equals(s) ? Boolean.TRUE : Boolean.FALSE);
			case 5: // Null literal.
				return NullValue.INSTANCE;
			case 6: // Error literal
				logger.debug("Error Literal");
				return ErrorValue.INSTANCE;
			case 7: // Any literal
				logger.debug("Any Literal");
				return AnyValue.INSTANCE;
			default:
				throw new RuntimeException("Invalid literal " + s);
		}
	}

	private Value parseSimpleValue(final SimpleValue sv) {
		if(sv.f0.which == 0) {
			return parseLiteral((Literal) sv.f0.choice);
		} else {
			return new VariableValue(sv.f0.choice.toString());
		}
	}

	private GenerateValue parseExecutable(final Map<String,String> imports, final Executable exec) {
		final String sql = exec.f0.choice.toString();
		switch(exec.f0.which) {
			case 0: // apply
				return parseMethod(imports, (Apply) exec.f0.choice);
			case 1: // string literal
				return parseSQL(sql.substring(1, sql.length() - 1).replaceAll("\\\\\"", "\"").replaceAll("\\\\", "\\"));
			case 2: // single quote string literal
				return parseSQL(sql.substring(1, sql.length() - 1).replaceAll("\\\\'", "'").replaceAll("\\\\", "\\"));
			default:
				throw new RuntimeException("Bad executable");
		}
	}

	private GenerateValue parseMethod(final Map<String, String> imports, final Apply apply) {
		String klazz = (apply.f0.present()) ? ((NodeSequence) apply.f0.node).nodes.get(0).toString() : "";
		final String impClass = imports.get(klazz);
		if(impClass != null) { klazz = impClass; }
		final String method = apply.f1.toString();
		final ArrayList<Value> params = new ArrayList<>();
		if(apply.f3.present()) {
			final Params p = (Params) apply.f3.node;
			params.add(parseSimpleValue(p.f0));
			for(final Node n: p.f1.nodes) {
				params.add(parseSimpleValue((SimpleValue) ((NodeSequence) n).nodes.get(1)));
			}
		}
		return new ApplyMethod(klazz, method, params);
	}

	private Test parseImport(final Map<String,String> imports, final Imports i) {
		String fullName = i.f1.toString();
		String rename = fullName;
		for(final Node n: i.f2.nodes) {
			final NodeSequence ns = (NodeSequence) n;
			rename = ns.nodes.get(1).toString();
			fullName += "." + rename;
		}
		if(i.f3.present()) {
			rename = ((NodeSequence) i.f3.node).nodes.get(1).toString();
		}
		logger.debug("Importing {} AS {}", fullName, rename);
		imports.put(rename, fullName);
		return null;
	}

	private Test parseTest(final Map<String,String> imports, final TestStatement stmt) {
		switch (stmt.f0.which) {
			case 0:
				final SideEffect se = (SideEffect) stmt.f0.choice;
				return new Exec(parseExecutable(imports, se.f1));
			case 1:
				final TestExpression te = (TestExpression) stmt.f0.choice;
				return new Check(te.f0.beginLine, parseExecutable(imports, te.f1), parseValue(te.f3));
			case 2:
				final Capture c = (Capture) stmt.f0.choice;
				return new Let(c.f1.toString(), parseExecutable(imports, c.f3));
			case 3:
				final Imports i = (Imports) stmt.f0.choice;
				return parseImport(imports, i);
			default:
				throw new RuntimeException("Invalid Statement type");
		}
	}

	public Iterable<Test> load(final Map<String,String> imports, final InputStream in) throws ParseException {
		final TestLogParser parser = new TestLogParser(in);
		final Expressions expressions = parser.Expressions();
		final ArrayList<Test> tests = new ArrayList<>();
		for(final Node n: expressions.f0.nodes) {
			logger.debug("Expression");
			final Test t = parseTest(imports, (TestStatement) n);
			if(t!=null) { tests.add(t); }
		}
		return tests;
	}
}