Skip to content
Closed
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ The project is under active development, with the core infrastructure operationa
| Public Go API (`client/opensysml`) | ✅ Complete for its v1 scope: parse, diagnostics, symbols, evaluation, instantiation and capability negotiation, answered in process or over Connect, with the edit API, conversion, verification, behaviour execution and Query out of scope ([client/opensysml/README.md](client/opensysml/README.md)) |
| Python client library | ✅ Complete for the RPCs that exist (connection lifecycle, parse/symbols/eval/instantiate/execute, constraint/requirement/satisfaction/calc verification, conversion, edits, Query, IPython hooks, DataFrame) |
| Rust client library | 🚧 Blocking v1 client for parse, diagnostics, symbols, evaluation and instantiation; see the [Rust client README](clients/rust/README.md) |
| Java client library | ✅ Complete for its v1 scope, with the remaining scope stated explicitly: connection lifecycle, parse/symbols/eval/instantiate and capability negotiation, with the edit API, conversion, verification, behaviour execution and Query out of scope. Connect protocol over the JDK's own HTTP client, so no gRPC or Netty reaches a host application ([clients/java/README.md](clients/java/README.md)) |
| Java client library | ✅ Connection lifecycle, parse/symbols/eval/instantiate and capability negotiation, plus typed immutable results for behaviour execution and exploration, verification and validation, calculation, analysis with engine selection, and structured and OSLC query; the edit API, multi-document parsing, conversion, sweeps and the document RPCs are stated as out of scope. Connect protocol over the JDK's own HTTP client, so no gRPC or Netty reaches a host application ([clients/java/README.md](clients/java/README.md)) |
| Node/TypeScript client library | ✅ Complete for the same v1 scope, in Node and the browser, over the Connect protocol with protobuf bodies and no native addon; values arrive as discriminated unions ([clients/node/README.md](clients/node/README.md)) |

<!-- doc-counts:begin refereed-figures -->
Expand Down Expand Up @@ -548,9 +548,10 @@ how to choose; [guide chapter 9](docs/guide/09-clients.md) works through each on
| Java, `org.openmbee:opensysml-client` | Connect, over the JDK's own HTTP client | not yet | [Java API](docs/reference/java-api.md) |
| Rust, `opensysml` | Connect, blocking, no async runtime | not yet | [Rust API](docs/reference/rust-api.md) |

The Go and Python clients cover every RPC the service serves; Node, Java and Rust cover a v1
The Go and Python clients cover every RPC the service serves; Node and Rust cover a v1
subset — connection lifecycle, capability negotiation, parsing, diagnostics, symbol lookup,
evaluation and instantiation — enumerated in the client libraries page.
evaluation and instantiation — and Java adds execution, verification, calculation, analysis and
query to it, as enumerated in the client libraries page.

### Python

Expand Down
1 change: 1 addition & 0 deletions changes/unreleased/java-client-runtime-api.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **The Java client wraps the execution, verification, calculation, analysis and query RPCs.** `Model` gains `executeAction`/`executeState` and `exploreAction`/`exploreState`, `verifyConstraint`/`verifyRequirement`/`verifySatisfaction`/`validateInstance`, `evaluateCalc`, `runAnalysis`/`exploreAnalysis`, `query`/`queryOslc` and `withEngine`, and `Connection` gains `listEngines`, each answering an immutable record (`ActionRun`, `StateRun`, `Exploration` of `Outcome`s, `Verification`, `Satisfaction`, `Validation`, `Verdict`, `VerificationVerdict`, `Calculation`, `Analysis`, `CaseEvaluation`, `Standing`, `QueryElement`, `EngineInfo`) with no generated protobuf type in the public API. A false verdict is returned as a decided answer rather than thrown; `ModelException.failureReason()` classifies an in-band failure, and `AnalysisException.partial()` keeps what a failed analysis computed before it stopped. The Java conformance runner now covers these RPCs through the public API, so 94 of the 134 scenarios run per protocol, and the remaining skips name the uncovered RPCs and the requests the public API cannot express.
65 changes: 42 additions & 23 deletions clients/java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ try (Connection connection = Connection.open()) { // starts a private sysml
Symbol vehicle = model.symbol("Demo::Vehicle"); // findSymbol returns Optional
Instantiation built = model.instantiate("Demo::Vehicle");

ActionRun run = model.executeAction("Test::addFive"); // outputs, final time, diagnostics
Verification v = model.verifyConstraint("Demo::Vehicle::massLight");
boolean holds = v.holds(); // false is an answer, not a failure
Analysis study = model.runAnalysis("Trade::lightest"); // outputs, verdicts, case evaluations
List<QueryElement> parts = model.query(
Query.all().where(Condition.equal("@type", List.of("PartUsage"))));

connection.capabilities().require(Capabilities.FEATURE_VALUES);
}
```
Expand All @@ -40,7 +47,12 @@ records (`IntegerValue`, `RealValue`, `ComplexValue`, `QuantityValue`, `ArrayVal
`FunctionValue`, `MetaobjectValue`, `EnumerationValue` (whose `EnumLiteral` carries the scalar a
`high = 3` literal was given as `value()`), `InstanceReference`, `Sequence`, `NullValue`,
`UnsetValue`, `UndeterminedValue`, `InfinityValue`), and `Symbol`,
`Diagnostic`, `Instance` and `Instantiation` are records with copied collections.
`Diagnostic`, `Instance` and `Instantiation` are records with copied collections, as are
the answers of execution (`ActionRun`, `StateRun`, `Exploration` of `Outcome`s),
verification (`Verification`, `Satisfaction`, `Validation`, each over `Verdict`s and
`VerificationVerdict`s), calculation and analysis (`Calculation`, `Analysis` of
`CaseEvaluation`s, with the engine `Standing`), query (`Query`, `Condition`,
`QueryElement`) and `EngineInfo`.
No generated protobuf message or builder appears in the public API. A `Diagnostic`
is `(severity, message, code, span)`; `code()` is the identifier to branch on
(`"syntax"`, a validation code such as `"unresolved"`, `"choice-point"`,
Expand All @@ -57,15 +69,19 @@ call, and `AutoCloseable`'s `close()` here throws nothing.
| exception | what happened |
| ---------------------- | ----------------------------------------------------------------- |
| `ServiceException` | the call was refused, with a `StatusCode` (`NOT_FOUND`, …) |
| `ModelException` | the call succeeded and the answer reports a model failure |
| `ModelException` | the call succeeded and the answer reports a model failure; `failureReason()` classifies it |
| `AnalysisException` | a `ModelException` from `runAnalysis` whose `partial()` holds what the run computed before it stopped |
| `TransportException` | HTTP or IO failure; the service was not reached or answered. `UNAVAILABLE`, except `DEADLINE_EXCEEDED` for a call that outlived its `requestTimeout` |
| `CapabilityException` | the service does not advertise a capability the call needs |
| `ServiceStartException`| no binary, a digest mismatch, or a child that would not start |
| `ChecksumMismatchException` | a binary's bytes are not the digest required of them ([the service binary](#the-service-binary)) |

The `ServiceException`/`ModelException` split is the one the conformance suite
draws too: an expression that will not evaluate is a successful call carrying an
error, not a service problem.
error, not a service problem. A verdict that is false is neither: the model has
answered, and `verifyConstraint` returns it with `Verdict.decided()` true. Only a
verdict carrying an `error` — the condition could not be evaluated, the symbol is
of another kind, the subject is ambiguous — is undecided.

## Maven, and a JDK 17 baseline

Expand Down Expand Up @@ -305,20 +321,20 @@ them. That is why `Model.evalWithSubject` checks before it calls: a
`CapabilityException` names the missing capability and the service that lacks
it, before a round trip.

## What v1 does not do
## What the client does not do

Deliberately out of scope, rather than half-implemented:

- **the edit API** (`ApplyEdits`) — authoring notation from Java;
- **models of several documents** (`ParseSources`) — one document is parsed at a time;
- **RDF conversion** (`Convert`) — Turtle/RDF export;
- **verification helpers** (`VerifyConstraint`, `VerifyRequirement`,
`VerifySatisfaction`), **behaviour execution** (`ExecuteAction`,
`ExecuteState`), **`EvaluateCalc`**, **`RunAnalysis`** and **`Query`**/OSLC;
- **parameter sweeps** (`RunSweep`);
- **native document queries and rendering** (`RunDocumentQuery`, `RenderDocument`);
- **generated model-ergonomics types** — no code generation from a model into
Java classes.

The service still serves all of them; reach them from another client, or from the
generated stubs in `org.openmbee.opensysml.proto` with `curl`, until a v2 wraps them.
generated stubs in `org.openmbee.opensysml.proto` with `curl`, until this client wraps them.

## Generated messages

Expand Down Expand Up @@ -352,35 +368,38 @@ unnoticed.

Or as a test, which is what CI runs: `mvn -f clients/java/pom.xml test`.

Per protocol, of 59 scenarios:
Per protocol, of 134 scenarios:

| protocol | ran | passed | failed | skipped |
| -------------- | --: | -----: | -----: | ------: |
| `connect` | 25 | 25 | 0 | 34 |
| `connect-json` | 25 | 25 | 0 | 34 |

**34 skipped**, and they are the scenarios of the RPCs v1 does not cover:
`ExecuteAction` (3), `ExecuteState` (2), `Convert` (5), `ApplyEdits` (5),
`VerifyConstraint` (4), `VerifyRequirement` (2), `VerifySatisfaction` (2),
`EvaluateCalc` (2), `Query` (8) — 33 — plus
`parse/naming_no_source_is_invalid`, which asserts that a request naming no
source at all is refused: the public API always names one, so the client cannot
send that request. gRPC is not run at all: this client does not speak it.
| `connect` | 94 | 94 | 0 | 40 |
| `connect-json` | 94 | 94 | 0 | 40 |

**40 skipped**: the scenarios of the RPCs the client does not cover —
`ApplyEdits` (10), `RunSweep` (7), `RunDocumentQuery` (6), `Convert` (5),
`ParseSources` (4), `RenderDocument` (3) — 35 — plus five requests the public API
cannot express: `parse/naming_no_source_is_invalid` (the API always names a
source), a `Query` carrying both a structured and an OSLC query and one whose
comparison has no operator (`Query` and `Condition` are values that cannot be built
that way), and two `EvaluateCalc` arguments malformed on the wire, which the
client's own `Value` reader refuses before any request could carry them. The suite
test asserts that no scenario of a covered RPC is skipped as uncovered, so a
shrinking surface cannot pass quietly. gRPC is not run at all: this client does not speak it.

The runner is not vacuous. `-mutate` corrupts every answer before it is compared,
and `SuiteTest.aCorruptedAnswerIsCaught` asserts each corruption is caught:

| `-mutate` | what it does to every answer | scenarios that fail |
| ----------------- | -------------------------------- | ------------------: |
| `perturb-reals` | moves each real by a millionth | 4 |
| `truncate-lists` | drops the last repeated element | 7 |
| `rewrite-strings` | replaces each string | 13 |
| `perturb-reals` | moves each real by a millionth | 20 |
| `truncate-lists` | drops the last repeated element | 36 |
| `rewrite-strings` | replaces each string | 53 |

## Running the tests

```bash
make build # bin/sysml-grpc; tests skip without it
mvn -f clients/java/pom.xml test # 119 client tests, 27 conformance tests
mvn -f clients/java/pom.xml test # 200 client tests, 36 conformance tests
mvn -f clients/java/pom.xml test -Dopensysml.requireService=true # CI: absence fails
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.openmbee.opensysml;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalDouble;

/**
* What one execution of an action produced: what {@link Model#executeAction(String)} answers.
*
* @param outputs the action's output parameters by name, empty for an action producing none
* @param finalTime the run's simulation clock when it ended, in seconds from the 0 it started at;
* absent from a service without the {@code final_time} capability
* @param diagnostics what the service reported while executing
*/
public record ActionRun(
Map<String, Value> outputs, OptionalDouble finalTime, List<Diagnostic> diagnostics) {

/**
* Creates an action run, copying its collections.
*
* @param outputs the outputs by name
* @param finalTime the final time, when reported
* @param diagnostics the diagnostics
*/
public ActionRun {
outputs = Map.copyOf(outputs);
Objects.requireNonNull(finalTime, "finalTime");
diagnostics = List.copyOf(diagnostics);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.openmbee.opensysml;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* What one run of an analysis case produced: what {@link Model#runAnalysis(String)} answers.
*
* @param outputs the case's out and return parameters by name, in declaration order; a body
* returning into an unnamed result is the output {@code "result"}
* @param verdicts the case's objectives, in order, then the assertions in its body, of kinds {@link
* Verdict#KIND_OBJECTIVE} and {@link Verdict#KIND_ASSERTION}
* @param verifications the body verdicts of the case and of the verification cases it performs,
* empty for an analysis case
* @param evaluations the applications the run made of the case's own calcs, in the order made: for
* a trade study, its evaluation function applied to each alternative in subject order
* @param instances the objects the run reported: those reachable from the subject, including it,
* and those the outputs and evaluations refer to
* @param diagnostics what the service reported while running
* @param standing how strongly the run's answer stands
*/
public record Analysis(
Map<String, Value> outputs,
List<Verdict> verdicts,
List<VerificationVerdict> verifications,
List<CaseEvaluation> evaluations,
List<Instance> instances,
List<Diagnostic> diagnostics,
Standing standing) {

/**
* Creates an analysis, copying its collections and keeping the outputs' order.
*
* @param outputs the outputs by name
* @param verdicts the verdicts
* @param verifications the body verdicts
* @param evaluations the evaluations
* @param instances the objects
* @param diagnostics the diagnostics
* @param standing the standing, never {@code null}
*/
public Analysis {
outputs = Collections.unmodifiableMap(new LinkedHashMap<>(outputs));
verdicts = List.copyOf(verdicts);
verifications = List.copyOf(verifications);
evaluations = List.copyOf(evaluations);
instances = List.copyOf(instances);
diagnostics = List.copyOf(diagnostics);
Objects.requireNonNull(standing, "standing");
}

/**
* Whether every objective and assertion was decided and held. A case stating none holds
* trivially.
*
* @return {@code true} when no verdict is undecided or violated
*/
public boolean holds() {
return verdicts.stream().allMatch(verdict -> verdict.decided() && verdict.holds());
}

/**
* The case's objective.
*
* @return the first objective verdict, absent for a case stating none
*/
public Optional<Verdict> objective() {
return verdicts.stream().filter(verdict -> Verdict.KIND_OBJECTIVE.equals(verdict.kind())).findFirst();
}

/**
* The evaluation a trade study selected.
*
* @return the selected evaluation, absent when none was
*/
public Optional<CaseEvaluation> selected() {
return evaluations.stream().filter(CaseEvaluation::selected).findFirst();
}

/**
* The object a value refers to.
*
* @param reference a reference the run reported
* @return the object, absent when it is not among those reported
*/
public Optional<Instance> resolve(Value.InstanceReference reference) {
Objects.requireNonNull(reference, "reference");
return Instances.find(instances, reference.instanceId());
}

/**
* The object of an id.
*
* @param instanceId the id the service gave the object
* @return the object, absent when it is not among those reported
*/
public Optional<Instance> instance(long instanceId) {
return Instances.find(instances, instanceId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.openmbee.opensysml;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
* An analysis case that could not run to its end but left something to inspect: the outputs and
* evaluations made before an alternative failed, an objective the failure left undecided.
*
* <p>A request refused before the run, or a failure leaving nothing to report, is a plain {@link
* ModelException} whose {@link #failureReason()} says why.
*/
public class AnalysisException extends ModelException {

private static final long serialVersionUID = 1L;

private final transient Analysis partial;

/**
* Creates an analysis exception.
*
* @param message the failure, as the service worded it
* @param failureReason what kind of failure it is
* @param diagnostics diagnostics the answer carried
* @param partial what the run left
*/
public AnalysisException(
String message, FailureReason failureReason, List<Diagnostic> diagnostics, Analysis partial) {
super(message, failureReason, diagnostics);
this.partial = Objects.requireNonNull(partial, "partial");
}

/**
* What the run left: the outputs and evaluations made, the objects they name, each verdict
* undecided.
*
* @return the partial result; absent after Java serialization, which does not carry it
*/
public Optional<Analysis> partial() {
return Optional.ofNullable(partial);
}
}
Loading
Loading