/** * erlyberly, erlang trace debugger * Copyright (C) 2016 Andy Till * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package erlyberly.format; import com.ericsson.otp.erlang.OtpErlangBinary; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangPid; import com.ericsson.otp.erlang.OtpErlangTuple; import com.ericsson.otp.erlang.OtpErlangAtom; import com.ericsson.otp.erlang.OtpErlangList; import com.ericsson.otp.erlang.OtpErlangString; import com.ericsson.otp.erlang.OtpErlangMap; import erlyberly.node.OtpUtil; import java.util.ArrayList; import java.util.Map; import java.util.Iterator; import java.nio.charset.StandardCharsets; public class ElixirFormatter implements TermFormatter { private static final String STRUCT_KEY = "__struct__"; private String stripElixirPrefix(String str) { if (str.startsWith("\'Elixir.")) { return stripQutes(str).substring(7); } if (str.startsWith("Elixir.")) { return str.substring(7); } return str; } private String stripQutes(String str) { return str.substring(1, str.length() - 1); } @Override public StringBuilder appendToString(OtpErlangObject obj, StringBuilder sb) { if(obj instanceof OtpErlangBinary) { OtpErlangBinary bin = (OtpErlangBinary) obj; if (Formatting.isDisplayableString(bin)) { sb.append("\""); sb.append(new String(bin.binaryValue(), StandardCharsets.UTF_8)); sb.append("\""); } else { sb.append("<<"); Formatting.binaryToString(bin, ", ", sb); sb.append(">>"); } } else if(obj instanceof OtpErlangPid) { sb.append(pidToString((OtpErlangPid) obj)); } else if(OtpUtil.isErlyberlyRecord(obj)) { OtpErlangTuple record = (OtpErlangTuple) obj; OtpErlangAtom recordName = (OtpErlangAtom) record.elementAt(1); OtpErlangList fields = (OtpErlangList) record.elementAt(2); sb.append("{").append(recordName).append(", "); for(int i=0; i < fields.arity(); i++) { if(i != 0) { sb.append(", "); } appendToString(fields.elementAt(i), sb); } sb.append("}"); } else if(OtpUtil.isErlyberlyRecordField(obj)) { OtpErlangObject fieldObj = ((OtpErlangTuple)obj).elementAt(2); appendToString(fieldObj, sb); } else if(obj instanceof OtpErlangTuple || obj instanceof OtpErlangList) { String brackets = bracketsForTerm(obj); OtpErlangObject[] elements = OtpUtil.elementsForTerm(obj); sb.append(brackets.charAt(0)); for(int i=0; i < elements.length; i++) { if(i != 0) { sb.append(", "); } appendToString(elements[i], sb); } if(obj instanceof OtpErlangList && !((OtpErlangList)obj).isProper()) { sb.append(cons()); appendToString(((OtpErlangList)obj).getLastTail(), sb); } sb.append(brackets.charAt(1)); } else if(obj instanceof OtpErlangString) { Formatting.appendString((OtpErlangString) obj, this, "\'", sb); } else if(obj instanceof OtpErlangAtom){ String str = obj.toString(); if (str.startsWith("\'") && str.endsWith("\'")) { String innerStr = str.substring(1, str.length() - 1); if(innerStr.startsWith("Elixir.")) { // Treat modules differently str = innerStr.substring(7); } else { // Convert Erlang style escaped atoms to Elixir style str = ":\"" + innerStr +"\""; } } else { str = ":" + str; } sb.append(str); } else if(obj instanceof OtpErlangMap) { OtpErlangMap map = (OtpErlangMap) obj; sb.append(mapLeft(obj)); Iterator<Map.Entry<OtpErlangObject,OtpErlangObject>> elemIt = map.entrySet().iterator(); while(elemIt.hasNext()) { Map.Entry<OtpErlangObject,OtpErlangObject> elem = elemIt.next(); if (elem.getKey() instanceof OtpErlangAtom) { if (isHiddenField(elem.getKey())) { continue; } sb.append(elem.getKey().toString()); sb.append(": "); } else { appendToString(elem.getKey(), sb); sb.append(" => "); } appendToString(elem.getValue(), sb); if (elemIt.hasNext()) { sb.append(", "); } } sb.append("}"); } else { sb.append(obj.toString()); } return sb; } @Override public String mapKeyToString(OtpErlangObject obj) { if (obj instanceof OtpErlangAtom) { return obj.toString() + ": "; } else { return appendToString(obj, new StringBuilder()).toString() + " => "; } } private static String pidToString(OtpErlangPid pid) { return pid.toString(); } public String bracketsForTerm(OtpErlangObject obj) { assert obj != null; if(obj instanceof OtpErlangTuple) return "{}"; else if(obj instanceof OtpErlangList) return "[]"; else throw new RuntimeException("No brackets for type " + obj.getClass()); } /** * Convert an MFA tuple to a string, where the MFA must have the type: * * {Module::atom(), Function::atom(), Args::[any()]}. */ @Override public String modFuncArgsToString(OtpErlangTuple mfa) { StringBuilder sb = new StringBuilder(); sb.append(moduleNameToString((OtpErlangAtom) mfa.elementAt(0))) .append(".") .append(funToStringNoQuotes((OtpErlangAtom) mfa.elementAt(1))) .append("("); OtpErlangList args = (OtpErlangList) mfa.elementAt(2); ArrayList<String> stringArgs = new ArrayList<>(); for (OtpErlangObject arg : args) { stringArgs.add(toString(arg)); } sb.append(String.join(", ", stringArgs)); sb.append(")"); return sb.toString(); } private String moduleNameToString(OtpErlangAtom mod) { String str = mod.atomValue(); return moduleNameToString(str); } @Override public String moduleNameToString(String moduleName) { if(moduleName.startsWith("Elixir.")) return moduleName.substring(7); else return ":" + moduleName; } private String funToStringNoQuotes(OtpErlangAtom atom) { return atom.atomValue(); } private String atomToStringNoQuotes(OtpErlangAtom atom) { return ":" + atom.atomValue(); } @Override public String modFuncArityToString(OtpErlangTuple mfa) { StringBuilder sb = new StringBuilder(); OtpErlangList argsList = OtpUtil.toErlangList(mfa.elementAt(2)); sb.append(moduleNameToString((OtpErlangAtom) mfa.elementAt(0))) .append(".") .append(funToStringNoQuotes((OtpErlangAtom) mfa.elementAt(1))) .append("/").append(argsList.arity()); return sb.toString(); } @Override public String modFuncArityToString(String mod, String func, int arity) { return moduleNameToString(mod) + "." + func + "/" + arity; } @Override public String exceptionToString(OtpErlangAtom errorClass, OtpErlangObject errorReason) { return errorClass + ":" + toString(errorReason); } @Override public String emptyTupleString() { return "{ }"; } @Override public String tupleLeftParen() { return "{"; } @Override public String tupleRightParen() { return "}"; } @Override public String emptyListString() { return "[ ]"; } @Override public String listLeftParen() { return "["; } @Override public String listRightParen() { return "]"; } @Override public String mapLeft(OtpErlangObject obj) { OtpErlangMap map = (OtpErlangMap) obj; OtpErlangAtom structNameKey = new OtpErlangAtom(STRUCT_KEY); if (map.get(structNameKey) != null) { String structName = stripElixirPrefix(map.get(structNameKey).toString()); return "%" + structName + "{"; } return "%{"; } @Override public Boolean isHiddenField(OtpErlangObject key) { OtpErlangAtom structNameKey = new OtpErlangAtom(STRUCT_KEY); return (key instanceof OtpErlangAtom) && key.equals(structNameKey); } @Override public String mapRight() { return "}"; } @Override public String cons() { return "|"; } }