001package gudusoft.gsqlparser.ir.semantic.diff; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.List; 006import java.util.Map; 007 008/** 009 * Hand-rolled deterministic JSON exporter for {@link DivergenceReport}, in 010 * the same style as 011 * {@code gudusoft.gsqlparser.ir.semantic.export.SemanticIRJsonExporter}. 012 * 013 * <p>Edges are sorted via {@link CanonicalLineageEdge#ORDER}; aggregate 014 * map keys are sorted lexicographically; divergences are sorted via 015 * {@link Divergence#ORDER}. 016 */ 017public final class DivergenceJsonExporter { 018 019 private static final String INDENT = " "; 020 021 private DivergenceJsonExporter() {} 022 023 public static String toJson(DivergenceReport report) { 024 if (report == null) { 025 throw new IllegalArgumentException("report must not be null"); 026 } 027 StringBuilder sb = new StringBuilder(); 028 sb.append("{\n"); 029 writeKey(sb, 1, "sql"); 030 writeString(sb, report.getSqlName()); 031 sb.append(",\n"); 032 writeKey(sb, 1, "irProjection"); 033 writeProjection(sb, 1, report.getIrModel()); 034 sb.append(",\n"); 035 writeKey(sb, 1, "dlineageProjection"); 036 writeProjection(sb, 1, report.getDlineageModel()); 037 sb.append(",\n"); 038 writeKey(sb, 1, "divergences"); 039 writeDivergences(sb, 1, report.getDivergences()); 040 sb.append(",\n"); 041 writeKey(sb, 1, "summary"); 042 writeSummary(sb, 1, report.getSummary()); 043 sb.append("\n}\n"); 044 return sb.toString(); 045 } 046 047 private static void writeProjection(StringBuilder sb, int depth, CanonicalLineageModel m) { 048 sb.append("{\n"); 049 writeKey(sb, depth + 1, "edges"); 050 writeEdges(sb, depth + 1, m); 051 sb.append(",\n"); 052 writeKey(sb, depth + 1, "outputNames"); 053 writeStringArray(sb, sortedCopy(m.getOutputNames())); 054 sb.append(",\n"); 055 writeKey(sb, depth + 1, "aggregates"); 056 writeAggregates(sb, depth + 1, m.getAggregateByOutput()); 057 sb.append("\n"); 058 indent(sb, depth); 059 sb.append("}"); 060 } 061 062 private static void writeEdges(StringBuilder sb, int depth, CanonicalLineageModel m) { 063 List<CanonicalLineageEdge> sorted = new ArrayList<>(m.getEdges()); 064 Collections.sort(sorted, CanonicalLineageEdge.ORDER); 065 if (sorted.isEmpty()) { 066 sb.append("[]"); 067 return; 068 } 069 sb.append("[\n"); 070 for (int i = 0; i < sorted.size(); i++) { 071 indent(sb, depth + 1); 072 writeEdgeInline(sb, sorted.get(i)); 073 if (i < sorted.size() - 1) sb.append(","); 074 sb.append("\n"); 075 } 076 indent(sb, depth); 077 sb.append("]"); 078 } 079 080 private static void writeEdgeInline(StringBuilder sb, CanonicalLineageEdge e) { 081 sb.append("{"); 082 writeKeyInline(sb, "role"); 083 writeString(sb, e.getRole().name()); 084 sb.append(", "); 085 writeKeyInline(sb, "outputName"); 086 if (e.getOutputName() == null) { 087 sb.append("null"); 088 } else { 089 writeString(sb, e.getOutputName()); 090 } 091 sb.append(", "); 092 writeKeyInline(sb, "baseTable"); 093 writeString(sb, e.getBaseTable()); 094 sb.append(", "); 095 writeKeyInline(sb, "baseColumn"); 096 writeString(sb, e.getBaseColumn()); 097 sb.append("}"); 098 } 099 100 private static void writeAggregates(StringBuilder sb, int depth, Map<String, Boolean> aggregates) { 101 if (aggregates.isEmpty()) { 102 sb.append("{}"); 103 return; 104 } 105 List<String> keys = sortedCopy(aggregates.keySet()); 106 sb.append("{\n"); 107 for (int i = 0; i < keys.size(); i++) { 108 String k = keys.get(i); 109 indent(sb, depth + 1); 110 writeKeyInline(sb, k); 111 sb.append(aggregates.get(k) ? "true" : "false"); 112 if (i < keys.size() - 1) sb.append(","); 113 sb.append("\n"); 114 } 115 indent(sb, depth); 116 sb.append("}"); 117 } 118 119 private static void writeStringArray(StringBuilder sb, List<String> values) { 120 if (values.isEmpty()) { 121 sb.append("[]"); 122 return; 123 } 124 sb.append("["); 125 for (int i = 0; i < values.size(); i++) { 126 if (i > 0) sb.append(", "); 127 writeString(sb, values.get(i)); 128 } 129 sb.append("]"); 130 } 131 132 private static void writeDivergences(StringBuilder sb, int depth, List<Divergence> divs) { 133 if (divs.isEmpty()) { 134 sb.append("[]"); 135 return; 136 } 137 sb.append("[\n"); 138 for (int i = 0; i < divs.size(); i++) { 139 indent(sb, depth + 1); 140 writeDivergenceInline(sb, divs.get(i)); 141 if (i < divs.size() - 1) sb.append(","); 142 sb.append("\n"); 143 } 144 indent(sb, depth); 145 sb.append("]"); 146 } 147 148 private static void writeDivergenceInline(StringBuilder sb, Divergence d) { 149 sb.append("{"); 150 writeKeyInline(sb, "kind"); 151 writeString(sb, d.getKind().name()); 152 sb.append(", "); 153 writeKeyInline(sb, "outputName"); 154 if (d.getOutputName() == null) { 155 sb.append("null"); 156 } else { 157 writeString(sb, d.getOutputName()); 158 } 159 sb.append(", "); 160 writeKeyInline(sb, "detail"); 161 writeString(sb, d.getDetail()); 162 sb.append("}"); 163 } 164 165 private static void writeSummary(StringBuilder sb, int depth, Map<DivergenceClass, Integer> summary) { 166 sb.append("{\n"); 167 // Iterate in DivergenceClass declaration order — provided by the report. 168 int i = 0; 169 int n = summary.size(); 170 for (Map.Entry<DivergenceClass, Integer> e : summary.entrySet()) { 171 indent(sb, depth + 1); 172 writeKeyInline(sb, e.getKey().name()); 173 sb.append(e.getValue()); 174 if (i < n - 1) sb.append(","); 175 sb.append("\n"); 176 i++; 177 } 178 indent(sb, depth); 179 sb.append("}"); 180 } 181 182 private static List<String> sortedCopy(java.util.Collection<String> values) { 183 List<String> sorted = new ArrayList<>(values); 184 Collections.sort(sorted); 185 return sorted; 186 } 187 188 private static void writeKey(StringBuilder sb, int depth, String key) { 189 indent(sb, depth); 190 sb.append('"').append(escape(key)).append("\": "); 191 } 192 193 private static void writeKeyInline(StringBuilder sb, String key) { 194 sb.append('"').append(escape(key)).append("\": "); 195 } 196 197 private static void writeString(StringBuilder sb, String value) { 198 sb.append('"').append(escape(value)).append('"'); 199 } 200 201 private static void indent(StringBuilder sb, int depth) { 202 for (int i = 0; i < depth; i++) sb.append(INDENT); 203 } 204 205 private static String escape(String s) { 206 StringBuilder out = new StringBuilder(s.length() + 2); 207 for (int i = 0; i < s.length(); i++) { 208 char c = s.charAt(i); 209 switch (c) { 210 case '"': out.append("\\\""); break; 211 case '\\': out.append("\\\\"); break; 212 case '\n': out.append("\\n"); break; 213 case '\r': out.append("\\r"); break; 214 case '\t': out.append("\\t"); break; 215 case '\b': out.append("\\b"); break; 216 case '\f': out.append("\\f"); break; 217 default: 218 if (c < 0x20) { 219 out.append(String.format("\\u%04x", (int) c)); 220 } else { 221 out.append(c); 222 } 223 } 224 } 225 return out.toString(); 226 } 227}