Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ npm run verify # lint, format, typecheck, ADR contract, tests + coverage
`npm run lint:adrs` alone runs the decision-record contract, and `npm test`
runs the suite without enforcing coverage. `npm run test:coverage` (part of
`npm run verify`) enforces the ratchet in `vitest.config.ts`: statements
99.03%, branches 96.09%, functions 100%, lines 98.95%.
99.06%, branches 96.17%, functions 100%, lines 98.97%.

## Conventions

Expand Down
7 changes: 5 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,11 @@ by either source format:
by name; a class the language writes as one word carries that word as
`scalar`.
- A **feature** carries its `name`, the `types` it admits sorted by name,
whether it is `required`, whether it holds `many` values, and whether it is a
`map` keyed by string. A type is a class name, a vocabulary name, or one of
whether it is `required`, whether it holds `many` values, whether it is a
`map` keyed by string, and whether it is a `reference`: a name the document
writes that links to a model element, whose one type is the element's class.
A map also names what one of its entries is, as `entry`, so a reference can
point at a map's entries. A type is a class name, a vocabulary name, or one of
`string`, `int` and `boolean`.
- A union is not a class: a feature whose value may be one of several classes
names them all, so an abstract class on one side and a union on the other
Expand Down
3 changes: 2 additions & 1 deletion docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test file and holds at least one test; ids are unique; the count this
document states matches the number of rows it holds; and every id cited
anywhere in the tracked tree resolves to a row here.

This ledger holds **28** rows. The compiler's behaviours join it as they land.
This ledger holds **29** rows. The compiler's behaviours join it as they land.

| id | a contributor or a consumer can rely on | proved by |
|---|---|---|
Expand Down Expand Up @@ -51,3 +51,4 @@ This ledger holds **28** rows. The compiler's behaviours join it as they land.
| REQ-026 | A pull request's shape-and-coverage comment is updated in place on a second push, rather than posted again | [test/pr-report.test.ts](../test/pr-report.test.ts) |
| REQ-027 | A pull request's release candidate version sorts above the current release and below any version release-please could choose next | [test/rc-version.test.ts](../test/rc-version.test.ts) |
| REQ-028 | A citation to a decision record marked superseded is checked for its successor in the same sentence, a term the model retired is checked outside a quotation, and a stated count is checked against the real collection it claims to count | [test/meaning-contract.test.ts](../test/meaning-contract.test.ts) |
| REQ-029 | A route's and a scrape's `process` and `surface` link to the Process and surface they name inside their Application, and a name that links to nothing is refused with `E_UNKNOWN_PROCESS` or `E_UNKNOWN_SURFACE` at the pointer of the route or scrape | [test/model/links.test.ts](../test/model/links.test.ts) |
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import org.eclipse.emf.common.util.Enumerator;
import org.eclipse.emf.ecore.EAnnotation;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;

/**
* Reads a parsed model as the JSON value the parity contract compares: every feature named as the
Expand Down Expand Up @@ -42,6 +44,10 @@ private static boolean isSet(EObject owner, EStructuralFeature feature) {

private static Object value(EObject owner, EStructuralFeature feature) {
Object value = owner.eGet(feature);
if (feature instanceof EReference reference && !reference.isContainment()) {
// A reference is written as the name that linked it: the identifier of what it points at.
return EcoreUtil.getID((EObject) value);
}
if (feature.isMany()) {
return many(feature, (List<?>) value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic;
import org.eclipse.xtext.resource.XtextResourceSet;

/**
Expand All @@ -31,9 +33,19 @@ public static Parsed intent(Path path) {
.getInstance(XtextResourceSet.class);
Resource resource =
resources.getResource(URI.createFileURI(path.toAbsolutePath().toString()), true);
// Linking is lazy: every reference is resolved before the errors are read, so a name that links to
// nothing is among them.
EcoreUtil2.resolveAll(resource);
List<Diagnostic> refusals = new ArrayList<>();
List<Diagnostic> unlinked = new ArrayList<>();
for (Resource.Diagnostic error : resource.getErrors()) {
refusals.add(new Diagnostic(Diagnostic.SCHEMA, "", "line " + error.getLine() + ": " + error.getMessage()));
if (error instanceof XtextLinkingDiagnostic linking) {
EObject owner = resource.getEObject(linking.getUriToProblem().fragment());
unlinked.add(new Diagnostic(linking.getCode(), Pointer.of(owner), linking.getMessage()));
} else {
refusals.add(
new Diagnostic(Diagnostic.SCHEMA, "", "line " + error.getLine() + ": " + error.getMessage()));
}
}
if (resource.getContents().isEmpty()) {
refusals.add(new Diagnostic(Diagnostic.SCHEMA, "", path.getFileName() + " holds no document"));
Expand All @@ -42,8 +54,9 @@ public static Parsed intent(Path path) {
return Parsed.refused(refusals);
}
EObject document = resource.getContents().get(0);
List<Diagnostic> broken =
Constraints.check(document, Constraints.beside(ProjectIntentPackage.class, CONSTRAINTS));
List<Diagnostic> broken = new ArrayList<>(
Constraints.check(document, Constraints.beside(ProjectIntentPackage.class, CONSTRAINTS)));
broken.addAll(unlinked);
return broken.isEmpty() ? Parsed.of(IntentJson.of(document)) : Parsed.refused(broken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.HttpProbe;
import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Lifecycle;
import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Match;
import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Placement;
import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Probes;
import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Process;
import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.ProjectIntentFactory;
import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Route;
import java.util.Map;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -66,6 +68,20 @@ void anOptionalFeatureTheDocumentSetIsWritten() {
entry("probes", Map.of("readiness", Map.of("path", "/healthz/ready", "port", 8080))));
}

@Test
void aReferenceIsWrittenAsTheNameItLinked() {
Process process = process();
process.getProvides().put("http", 8080);
Route route = MODEL.createRoute();
route.setPath("/");
route.setMatch(Match.PREFIX);
route.setProcess(process);
route.setSurface(process.getProvides().get(0));

assertThat(IntentJson.of(route))
.contains(entry("process", "notes-api"), entry("surface", "http"), entry("match", "prefix"));
}

@Test
void aMapEntryIsWrittenAsAnObjectAndAListAsAList() {
Process process = process();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ void yamlOutsideTheGrammarIsRefusedWithADiagnostic(@TempDir Path directory) thro
.isNotEmpty();
}

@Test
void aNameThatLinksToNothingIsRefusedAtThePointerOfWhatWroteIt(@TempDir Path directory) throws IOException {
String routed = MINIMAL.replace("applications:\n - id: notes\n", """
applications:
- id: notes
exposure:
- name: public
host: notes.jorisjonkers.dev
audience: lan
routes:
- { path: /, match: prefix, process: notes-api, surface: https }
""")
.replace(" runtime: node\n", " runtime: node\n provides: { http: 8080 }\n");

Parsed parsed = Pipeline.intent(file(directory, routed));

assertThat(parsed.diagnostics())
.extracting(Diagnostic::code, Diagnostic::path)
.containsExactly(org.assertj.core.groups.Tuple.tuple(
"E_UNKNOWN_SURFACE", "/applications/0/exposure/0/routes/0"));
}

@Test
void anEmptyDocumentIsRefused(@TempDir Path directory) throws IOException {
Parsed parsed = Pipeline.intent(file(directory, ""));
Expand Down
8 changes: 8 additions & 0 deletions emf/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ factory by the last extension alone; which document a file holds is the file
name's to say, and that lands with the Platform document. There is no inferred syntax
metamodel and no mapping step between parsing and validation.

A route's and a scrape's `process` and `surface` are cross-references, linked by
a scope provider that offers the Processes of the Application holding them and the
surfaces the linked Process provides. A name that links to nothing becomes the
specification's code, `E_UNKNOWN_PROCESS` or `E_UNKNOWN_SURFACE`, at the pointer
of the route or scrape; a surface whose Process did not link is not reported as
well. A dependency edge's names reach other documents and stay names until the
composed union links them.

Indentation is not the grammar's concern: a token source turns the block
structure into the synthetic `BEGIN` and `END` tokens the rules read, and folds
a scalar written over several lines into one token. A line
Expand Down
3 changes: 2 additions & 1 deletion emf/docs/witnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ names the JUnit test that proves the same behaviour here
has no witness here, when a witness names an id that is not a model row, or
when it names a test method that does not exist.

This list holds **3** witnesses.
This list holds **4** witnesses.

| id | JUnit test |
|---|---|
| REQ-021 | `ParityTest#theParsedIntentEqualsTheCommittedOracle` |
| REQ-023 | `ParityTest#theMetamodelsStructureEqualsTheCommittedDescriptor` |
| REQ-024 | `ParityTest#aRefusedDocumentEqualsItsCommittedDiagnostics` |
| REQ-029 | `LinkingTest#aRouteAndAScrapeLinkToTheVeryProcessAndSurfaceTheirApplicationHolds` |
16 changes: 8 additions & 8 deletions emf/metamodel/model/project-intent.ecore
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
<eStructuralFeatures xsi:type="ecore:EReference" name="scrape" eType="#//Scrape" containment="true"/>
</eClassifiers>
<eClassifiers xsi:type="ecore:EClass" name="Scrape">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="process" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="surface" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="process" lowerBound="1" eType="#//Process"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="surface" lowerBound="1" eType="#//Surface"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="path" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
</eClassifiers>
<eClassifiers xsi:type="ecore:EClass" name="Exposure">
Expand All @@ -43,18 +43,18 @@
<eClassifiers xsi:type="ecore:EClass" name="Route">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="path" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="match" lowerBound="1" eType="#//Match"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="process" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="surface" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="process" lowerBound="1" eType="#//Process"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="surface" lowerBound="1" eType="#//Surface"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" unsettable="true" name="audience" eType="#//Audience"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" unsettable="true" name="redirectTo" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
</eClassifiers>
<eClassifiers xsi:type="ecore:EClass" name="Process">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="name" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="name" lowerBound="1" iD="true" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="lifecycle" lowerBound="1" eType="#//Lifecycle"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="image" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="runtime" lowerBound="1" eType="#//Runtime"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" unsettable="true" name="engine" eType="#//Engine"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="provides" upperBound="-1" eType="#//SurfacePort" containment="true"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="provides" upperBound="-1" eType="#//Surface" containment="true"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="placement" lowerBound="1" eType="#//Placement" containment="true"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="writablePaths" upperBound="-1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="sidecars" upperBound="-1" eType="#//Sidecar" containment="true"/>
Expand All @@ -67,8 +67,8 @@
<eStructuralFeatures xsi:type="ecore:EAttribute" unsettable="true" name="startupBudget" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="cutover" lowerBound="1" eType="#//Cutover"/>
</eClassifiers>
<eClassifiers xsi:type="ecore:EClass" name="SurfacePort" instanceClassName="java.util.Map$Entry">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="key" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eClassifiers xsi:type="ecore:EClass" name="Surface" instanceClassName="java.util.Map$Entry">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="key" lowerBound="1" iD="true" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="value" lowerBound="1" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EIntegerObject"/>
</eClassifiers>
<eClassifiers xsi:type="ecore:EClass" name="Placement">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,20 @@ private static String scalar(EClass owner) {
}

private static Map<String, Object> feature(EStructuralFeature feature) {
boolean map = feature instanceof EReference reference && isMapEntry(reference.getEReferenceType());
boolean map = feature instanceof EReference containment
&& containment.isContainment()
&& isMapEntry(containment.getEReferenceType());
boolean linked = feature instanceof EReference reference && !reference.isContainment();
Map<String, Object> json = new LinkedHashMap<>();
json.put("name", feature.getName());
json.put("types", types(feature, map));
json.put("types", linked ? List.of(feature.getEType().getName()) : types(feature, map));
json.put("required", feature.isRequired());
json.put("many", feature.isMany() && !map);
json.put("map", map);
json.put("reference", linked);
if (map) {
json.put("entry", feature.getEType().getName());
}
return json;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,23 @@ void anAbstractClassIsAUnionRatherThanAClassOfItsOwn() {

@Test
void aMapEntryIsAMapRatherThanAClassOfItsOwn() {
assertThat(entries("classes").stream().map(entry -> entry.get("name"))).doesNotContain("SurfacePort");
assertThat(entries("classes").stream().map(entry -> entry.get("name"))).doesNotContain("Surface");
assertThat(feature("Process", "provides"))
.isEqualTo(Map.of(
"name", "provides", "types", List.of("int"), "required", false, "many", false, "map", true));
"name",
"provides",
"types",
List.of("int"),
"required",
false,
"many",
false,
"map",
true,
"reference",
false,
"entry",
"Surface"));
}

@Test
Expand All @@ -79,6 +92,8 @@ void aFeatureCarriesItsTypeAndItsMultiplicity() {
"many",
true,
"map",
false,
"reference",
false));
assertThat(feature("Process", "startupBudget").get("types")).isEqualTo(List.of("string"));
assertThat(feature("Process", "cutover").get("types")).isEqualTo(List.of("Cutover"));
Expand All @@ -87,6 +102,27 @@ void aFeatureCarriesItsTypeAndItsMultiplicity() {
assertThat(feature("Exposure", "contentPolicy").get("required")).isEqualTo(false);
}

@Test
void aNameTheModelLinksIsAReferenceToItsTarget() {
assertThat(feature("Route", "process"))
.isEqualTo(Map.of(
"name",
"process",
"types",
List.of("Process"),
"required",
true,
"many",
false,
"map",
false,
"reference",
true));
assertThat(feature("Scrape", "surface").get("types")).isEqualTo(List.of("Surface"));
assertThat(feature("Scrape", "surface").get("reference")).isEqualTo(true);
assertThat(feature("DependencyEdge", "surface").get("reference")).isEqualTo(false);
}

@Test
void aClassWrittenAsOneWordCarriesThatWord() {
assertThat(named("classes", "NoProbes"))
Expand Down
1 change: 1 addition & 0 deletions emf/syntax/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Bundle-RequiredExecutionEnvironment: JavaSE-21
Automatic-Module-Name: dev.jorisjonkers.deploykit.emf.syntax
Export-Package: dev.jorisjonkers.deploykit.emf.syntax,
dev.jorisjonkers.deploykit.emf.syntax.blocks,
dev.jorisjonkers.deploykit.emf.syntax.linking,
dev.jorisjonkers.deploykit.emf.syntax.values,
dev.jorisjonkers.deploykit.emf.syntax.parser.antlr,
dev.jorisjonkers.deploykit.emf.syntax.services
Expand Down
Loading
Loading