Skip to content

Commit 0285d7b

Browse files
authored
Add !mermaid command (#32)
1 parent 6aafc90 commit 0285d7b

File tree

5 files changed

+271
-22
lines changed

5 files changed

+271
-22
lines changed

hoptimator-catalog/src/main/java/com/linkedin/hoptimator/catalog/Resource.java

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.linkedin.hoptimator.catalog;
22

3+
import java.util.ArrayList;
4+
import java.util.Collection;
35
import java.util.HashMap;
46
import java.util.List;
57
import java.util.Map;
68
import java.util.Properties;
79
import java.util.Set;
10+
import java.util.SortedMap;
11+
import java.util.TreeMap;
12+
import java.util.UUID;
813
import java.util.regex.Pattern;
914
import java.util.regex.Matcher;
1015
import java.util.Scanner;
@@ -23,14 +28,29 @@
2328
*
2429
* Resources are injected into a Pipeline by the planner as needed. Generally,
2530
* each Resource Template corresponds to a Kubernetes controller.
31+
*
32+
* Resources may optionally link to input Resources, which is used strictly
33+
* for informational/debugging purposes.
2634
*/
2735
public abstract class Resource {
28-
private final String template;
29-
private final Map<String, Supplier<String>> properties = new HashMap<>();
36+
private final String kind;
37+
private final SortedMap<String, Supplier<String>> properties = new TreeMap<>();
38+
private final List<Resource> inputs = new ArrayList<>();
39+
40+
/** A Resource of some kind. */
41+
public Resource(String kind) {
42+
this.kind = kind;
43+
}
44+
45+
/** Copy constructor */
46+
public Resource(Resource other) {
47+
this.kind = other.kind;
48+
this.properties.putAll(other.properties);
49+
this.inputs.addAll(other.inputs);
50+
}
3051

31-
/** A Resource that will be rendered with the specified template */
32-
public Resource(String template) {
33-
this.template = template;
52+
public String kind() {
53+
return kind;
3454
}
3555

3656
/** Export a computed value to the template */
@@ -54,15 +74,16 @@ protected void export(String key, List<String> values) {
5474
export(key, values.stream().collect(Collectors.joining("\n")));
5575
}
5676

57-
/** The name of the template used to render this Resource */
58-
public String template() {
59-
return template;
77+
/** Reference an input resource */
78+
protected void input(Resource resource) {
79+
inputs.add(resource);
6080
}
6181

6282
public String property(String key) {
6383
return getOrDefault(key, null);
6484
}
6585

86+
/** Keys for all defined properties, in natural order. */
6687
public Set<String> keys() {
6788
return properties.keySet();
6889
}
@@ -76,15 +97,27 @@ public String render(Template template) {
7697
return template.render(this);
7798
}
7899

100+
public Collection<Resource> inputs() {
101+
return inputs;
102+
}
103+
104+
@Override
105+
public int hashCode() {
106+
return toString().hashCode();
107+
}
108+
79109
@Override
80110
public String toString() {
81111
StringBuilder sb = new StringBuilder();
82-
sb.append("[ ");
112+
sb.append("[ kind: " + kind() + " ");
83113
for (Map.Entry<String, Supplier<String>> entry : properties.entrySet()) {
84-
sb.append(entry.getKey());
85-
sb.append(":");
86-
sb.append(entry.getValue().get());
87-
sb.append(" ");
114+
String value = entry.getValue().get();
115+
if (value != null && !value.isEmpty()) {
116+
sb.append(entry.getKey());
117+
sb.append(":");
118+
sb.append(entry.getValue().get());
119+
sb.append(" ");
120+
}
88121
}
89122
sb.append("]");
90123
return sb.toString();
@@ -247,10 +280,10 @@ public SimpleTemplateFactory(Environment env) {
247280

248281
@Override
249282
public Template get(Resource resource) {
250-
String template = resource.template();
251-
InputStream in = getClass().getClassLoader().getResourceAsStream(template + ".yaml.template");
283+
String kind = resource.kind();
284+
InputStream in = getClass().getClassLoader().getResourceAsStream(kind + ".yaml.template");
252285
if (in == null) {
253-
throw new IllegalArgumentException("No template '" + template + "' found in jar resources");
286+
throw new IllegalArgumentException("No template '" + kind + "' found in jar resources");
254287
}
255288
StringBuilder sb = new StringBuilder();
256289
Scanner scanner = new Scanner(in);
Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,98 @@
11
package com.linkedin.hoptimator.catalog;
22

3+
import java.util.ArrayList;
34
import java.util.Collection;
5+
import java.util.Collections;
6+
import java.util.List;
7+
import java.util.function.Function;
8+
import java.util.stream.Collectors;
49

5-
/** In Hoptimator, Tables can have baggage in the form of Resources. */
10+
/**
11+
* Enables an adapter to emit arbitrary Resources for a given table.
12+
*
13+
* Optionally, establishes source->sink relationships between such Resources. These are used
14+
* strictly for debugging purposes.
15+
*/
616
public interface ResourceProvider {
717

818
/** Resources for the given table */
919
Collection<Resource> resources(String tableName);
20+
21+
/**
22+
* Establishes a source->sink relationship between this ResourceProvider and a sink Resource.
23+
*
24+
* All leaf-node Resources provided by this ResourceProvider will become inputs to the sink.
25+
*
26+
* e.g.
27+
* <pre>
28+
* ResourceProvider.empty().with(x -> a).with(x -> b).to(x -> c).to(x -> d)
29+
* </pre>
30+
*
31+
* encodes the following DAG:
32+
* <pre>
33+
* a --> c
34+
* b --> c
35+
* c --> d
36+
* </pre>
37+
*/
38+
default ResourceProvider toAll(ResourceProvider sink) {
39+
return x -> {
40+
List<Resource> combined = new ArrayList<>();
41+
List<Resource> sources = new ArrayList<>();
42+
List<Resource> sinks = new ArrayList<>();
43+
sources.addAll(resources(x));
44+
combined.addAll(sources);
45+
46+
// remove all non-leaf-node upstream Resources
47+
sources.removeAll(sources.stream().flatMap(y -> y.inputs().stream())
48+
.collect(Collectors.toList()));
49+
50+
// link all sources to all sinks
51+
sink.resources(x).forEach(y -> {
52+
combined.add(new Resource(y) {{
53+
sources.forEach(z -> input(z));
54+
}});
55+
});
56+
57+
return combined;
58+
};
59+
}
60+
61+
default ResourceProvider to(Resource resource) {
62+
return toAll(x -> Collections.singleton(resource));
63+
}
64+
65+
default ResourceProvider to(Function<String, Resource> resourceFunc) {
66+
return toAll(x -> Collections.singleton(resourceFunc.apply(x)));
67+
}
68+
69+
/** Combines this ResourceProvider with another ResourceProvider */
70+
default ResourceProvider withAll(ResourceProvider resourceProvider) {
71+
return x -> {
72+
List<Resource> combined = new ArrayList<>();
73+
combined.addAll(resources(x));
74+
combined.addAll(resourceProvider.resources(x));
75+
return combined;
76+
};
77+
}
78+
79+
default ResourceProvider with(Resource resource) {
80+
return withAll(x -> Collections.singleton(resource));
81+
}
82+
83+
default ResourceProvider with(Function<String, Resource> resourceFunc) {
84+
return withAll(x -> Collections.singleton(resourceFunc.apply(x)));
85+
}
86+
87+
static ResourceProvider empty() {
88+
return x -> Collections.emptyList();
89+
}
90+
91+
static ResourceProvider from(Collection<Resource> resources) {
92+
return x -> resources;
93+
}
94+
95+
static ResourceProvider from(Resource resource) {
96+
return x -> Collections.singleton(resource);
97+
}
1098
}

hoptimator-cli/src/main/java/com/linkedin/hoptimator/HoptimatorCliApp.java

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ protected int run(String[] args) throws IOException {
5050
commandHandlers.add(new PipelineCommandHandler());
5151
commandHandlers.add(new IntroCommandHandler());
5252
commandHandlers.add(new InsertCommandHandler());
53-
commandHandlers.add(new TestCommandHandler());
53+
commandHandlers.add(new CheckCommandHandler());
54+
commandHandlers.add(new MermaidCommandHandler());
5455
sqlline.updateCommandHandlers(commandHandlers);
5556
return sqlline.begin(args, null, true).ordinal();
5657
}
@@ -268,7 +269,7 @@ public boolean echoToFile() {
268269
}
269270
}
270271

271-
private class TestCommandHandler implements CommandHandler {
272+
private class CheckCommandHandler implements CommandHandler {
272273

273274
@Override
274275
public String getName() {
@@ -529,4 +530,79 @@ public boolean echoToFile() {
529530
return false;
530531
}
531532
}
533+
534+
private class MermaidCommandHandler implements CommandHandler {
535+
536+
@Override
537+
public String getName() {
538+
return "mermaid";
539+
}
540+
541+
@Override
542+
public List<String> getNames() {
543+
return Collections.singletonList(getName());
544+
}
545+
546+
@Override
547+
public String getHelpText() {
548+
return "Render a pipeline in mermaid format (similar to graphviz)";
549+
}
550+
551+
@Override
552+
public String matches(String line) {
553+
String sql = line;
554+
if (sql.startsWith(SqlLine.COMMAND_PREFIX)) {
555+
sql = sql.substring(1);
556+
}
557+
558+
if (sql.startsWith("mermaid")) {
559+
sql = sql.substring("mermaid".length() + 1);
560+
return sql;
561+
}
562+
563+
return null;
564+
}
565+
566+
@Override
567+
public void execute(String line, DispatchCallback dispatchCallback) {
568+
String sql = line;
569+
if (sql.startsWith(SqlLine.COMMAND_PREFIX)) {
570+
sql = sql.substring(1);
571+
}
572+
573+
if (sql.startsWith("mermaid")) {
574+
sql = sql.substring("mermaid".length() + 1);
575+
}
576+
577+
//remove semicolon from query if present
578+
if (sql.length() > 0 && sql.charAt(sql.length() - 1) == ';') {
579+
sql = sql.substring(0, sql.length() - 1);
580+
}
581+
582+
String connectionUrl = sqlline.getConnectionMetadata().getUrl();
583+
try {
584+
HoptimatorPlanner planner = HoptimatorPlanner.fromModelFile(connectionUrl, new Properties());
585+
PipelineRel plan = planner.pipeline(sql);
586+
PipelineRel.Implementor impl = new PipelineRel.Implementor(plan);
587+
HopTable outputTable = new HopTable("PIPELINE", "SINK", plan.getRowType(),
588+
Collections.singletonMap("connector", "dummy"));
589+
Pipeline pipeline = impl.pipeline(outputTable);
590+
sqlline.output(pipeline.mermaid());
591+
dispatchCallback.setToSuccess();
592+
} catch (Exception e) {
593+
sqlline.error(e.toString());
594+
dispatchCallback.setToFailure();
595+
}
596+
}
597+
598+
@Override
599+
public List<Completer> getParameterCompleters() {
600+
return Collections.emptyList();
601+
}
602+
603+
@Override
604+
public boolean echoToFile() {
605+
return false;
606+
}
607+
}
532608
}

hoptimator-planner/src/main/java/com/linkedin/hoptimator/planner/Pipeline.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import com.linkedin.hoptimator.catalog.Resource;
66

77
import java.util.Collection;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.stream.Collectors;
811

912
/** A set of Resources that deliver data.
1013
*
@@ -35,4 +38,46 @@ public String render(Resource.TemplateFactory templateFactory) {
3538
}
3639
return sb.toString();
3740
}
41+
42+
/** Render a graph of resources in mermaid format */
43+
public String mermaid() {
44+
StringBuilder sb = new StringBuilder();
45+
sb.append("flowchart\n");
46+
Map<String, List<Resource>> grouped = resources.stream()
47+
.collect(Collectors.groupingBy(x -> x.kind()));
48+
grouped.forEach((k, v) -> {
49+
sb.append(" subgraph " + k + "\n");
50+
v.forEach(x -> {
51+
String description = x.keys().stream()
52+
.filter(k2 -> x.property(k2) != null)
53+
.filter(k2 -> !x.property(k2).isEmpty())
54+
.map(k2 -> k2 + ": " + sanitize(x.property(k2)))
55+
.collect(Collectors.joining("\n"));
56+
sb.append(" " + id(x) + "[\"" + description + "\"]\n");
57+
});
58+
sb.append(" end\n");
59+
});
60+
grouped.forEach((k, v) -> {
61+
sb.append(" subgraph " + k + "\n");
62+
v.forEach(x -> {
63+
x.inputs().forEach(y -> {
64+
sb.append(" " + id(y) + " --> " + id(x) + "\n");
65+
});
66+
});
67+
sb.append(" end\n");
68+
});
69+
return sb.toString();
70+
}
71+
72+
private static String id(Resource resource) {
73+
return "R" + Integer.toString(resource.hashCode());
74+
}
75+
76+
private static String sanitize(String s) {
77+
String safe = s.replaceAll("\"", "&quot;").replaceAll("\n", " ").trim();
78+
if (safe.length() > 20) {
79+
return safe.substring(0, 17) + "...";
80+
}
81+
return safe;
82+
}
3883
}

0 commit comments

Comments
 (0)