Skip to content

bluecontract/blue-language-java

Repository files navigation

Blue Language Java

Java implementation of the Blue language core: https://github.com/bluecontract/blue-spec

Blue is a deterministic document language for describing data, types, and identity. A Blue document can be parsed, resolved against its type graph, reduced to canonical content, and addressed by a stable content hash called a BlueId. Blue Contracts and Processor 1.0 is implemented as a separate runtime target on top of the language layer for document processing, channels, handlers, events, gas, checkpoints, embedded scopes, lifecycle, and termination.

This library gives Java applications the foundations needed to work with Blue:

  • parse and serialize Blue YAML/JSON;
  • compute deterministic BlueIds;
  • resolve type chains and { blueId: ... } references;
  • validate deterministic schema constraints;
  • support list control forms such as $previous, $pos, and $empty;
  • build immutable FrozenNode and ResolvedSnapshot runtime views;
  • match nodes against type/shape patterns efficiently;
  • apply canonical patches;
  • run the generic snapshot-backed document processor;
  • register custom channel, handler, and marker processors.

Blue Language 1.0 and Blue Contracts and Processor 1.0 have separate conformance suites and reports.

Installation

Gradle:

repositories {
    mavenCentral()
}

dependencies {
    implementation "blue.language:blue-language-java:3.0.0"
}

Maven:

<dependency>
    <groupId>blue.language</groupId>
    <artifactId>blue-language-java</artifactId>
    <version>3.0.0</version>
</dependency>

Core Concepts

Nodes

A Blue document is a tree of nodes. A node has one payload kind:

  • scalar value;
  • list items;
  • object fields.

Nodes can also carry language metadata such as name, description, type, schema, itemType, keyType, valueType, and blueId.

name: Counter
description: Small document with one integer field
counter:
  type: Integer
  value: 0

The Java representation is blue.language.model.Node. It is mutable and useful for parsing, authoring, serialization, and compatibility APIs.

Types

In Blue, a type is also a Blue node. A document with type inherits and must conform to that type.

name: Price
amount:
  type: Integer
currency:
  type: Text

An instance can point to the type by BlueId:

type:
  blueId: <PriceBlueId>
amount: 150
currency: EUR

Resolving the instance makes inherited fields, type metadata, and constraints available in the runtime view.

BlueIds

A BlueId is a deterministic content address. It is calculated from canonical Blue content using RFC 8785-style canonical JSON input and SHA-256/Base58 output.

In canonical Blue, { blueId: X } is a pure reference. It cannot be mixed with sibling content:

# valid
type:
  blueId: 4th6...

# invalid
type:
  blueId: 4th6...
  name: Price

This keeps reference identity unambiguous.

Canonical Versus Resolved

Blue distinguishes two useful views:

  • canonical content: minimized content used for identity and storage;
  • resolved content: runtime view with inherited type state available.

ResolvedSnapshot contains both views as immutable FrozenNode graphs:

ResolvedSnapshot
  canonicalRoot  -> minimized identity source
  resolvedRoot   -> runtime view
  blueId         -> canonicalRoot.blueId()

Use snapshots for hot processing paths. Use mutable Node values at the edges where you parse, serialize, or build documents programmatically.

Quick Start

Parse YAML And Serialize It Back

import blue.language.Blue;
import blue.language.model.Node;

Blue blue = new Blue();

Node node = blue.yamlToNode(
        "name: Counter\n" +
        "counter: 0\n");

String json = blue.nodeToJson(node);
String yaml = blue.nodeToYaml(node);

System.out.println(json);
System.out.println(yaml);

Compute A Structural BlueId

Use calculateBlueId when the node itself is the content you want to address.

String blueId = blue.calculateBlueId(node);
System.out.println(blueId);

Structural BlueIds are sensitive to authored content. If a document contains a redundant inherited override, that override is part of the structural input.

Compute A Semantic BlueId

Use calculateSemanticBlueId when you want identity after preprocess, resolve, and minimization.

String semanticBlueId = blue.calculateSemanticBlueId(node);
System.out.println(semanticBlueId);

Semantic identity is useful when different authored forms should be treated as the same document because they resolve to the same minimized meaning.

Reference Providers

Blue resolves { blueId: ... } references through a NodeProvider.

For tests and local tools, BasicNodeProvider is often enough:

import blue.language.Blue;
import blue.language.model.Node;
import blue.language.provider.BasicNodeProvider;

Blue bootstrap = new Blue();

Node priceType = bootstrap.yamlToNode(
        "name: Price\n" +
        "amount:\n" +
        "  type: Integer\n" +
        "currency:\n" +
        "  type: Text\n");

BasicNodeProvider provider = new BasicNodeProvider(priceType);
String priceTypeBlueId = provider.getBlueIdByName("Price");

Blue blue = new Blue(provider);

Node price = blue.yamlToNode(
        "type:\n" +
        "  blueId: " + priceTypeBlueId + "\n" +
        "amount: 150\n" +
        "currency: EUR\n");

Node resolved = blue.resolve(price);
System.out.println(blue.nodeToYaml(resolved));

For production storage, implement NodeProvider:

import blue.language.NodeProvider;
import blue.language.model.Node;

import java.util.Collections;
import java.util.List;

public final class DatabaseNodeProvider implements NodeProvider {
    private final BlueDocumentStore store;

    public DatabaseNodeProvider(BlueDocumentStore store) {
        this.store = store;
    }

    @Override
    public List<Node> fetchByBlueId(String blueId) {
        Node content = store.fetchCanonicalNode(blueId);
        return content == null ? Collections.emptyList() : Collections.singletonList(content);
    }
}

The provider should return canonical Blue content for a requested BlueId. The library wraps providers internally to support single-document and multi-document reference forms.

Schema

schema provides deterministic core validation. Supported keywords include:

  • required
  • minLength
  • maxLength
  • minimum
  • maximum
  • exclusiveMinimum
  • exclusiveMaximum
  • multipleOf
  • minItems
  • maxItems
  • uniqueItems
  • minFields
  • maxFields
  • enum

Example:

name: Product
sku:
  type: Text
  schema:
    required: true
    minLength: 3
    maxLength: 32
quantity:
  type: Integer
  schema:
    minimum: 0

Lists

Blue list resolution supports overlays over inherited lists.

Positional Overlay

type:
  blueId: <BaseListType>
items:
  - $previous:
      blueId: <InheritedItemsBlueId>
  - $pos: 1
    value: replacement
  - value: appended

$previous anchors the inherited list. $pos replaces a specific inherited position. Normal items after the overlay append to the result.

Empty List Placeholder

items:
  - $empty: true

$empty: true is content. It is not the same as an absent list.

Merge Policies

type: List
mergePolicy: append-only
items:
  - value: first

Supported list policies:

  • positional
  • append-only

The resolver, minimizer, and BlueId calculator all understand these list-control forms.

Immutable Snapshots

ResolvedSnapshot is the preferred runtime representation.

import blue.language.snapshot.ResolvedSnapshot;

ResolvedSnapshot snapshot = blue.resolveToSnapshot(price);

System.out.println(snapshot.blueId());
System.out.println(snapshot.frozenCanonicalRoot().blueId());
System.out.println(snapshot.frozenResolvedRoot().blueId());

Snapshots provide:

  • immutable canonical root;
  • immutable resolved root;
  • cached per-node BlueIds;
  • path indexes for fast reads;
  • structural sharing for resolved references and type graphs.

Read a node by JSON Pointer:

import blue.language.snapshot.FrozenNode;

FrozenNode amount = snapshot.resolvedAt("/amount");
System.out.println(amount.getValue());

Use JSON Pointer escaping for literal / and ~ in field names:

FrozenNode value = snapshot.resolvedAt("/a~1b/c~0d");

This addresses the object path:

a/b:
  c~d: value

Snapshot Caches

Blue keeps a resolved snapshot cache and a resolved reference cache.

ResolvedSnapshot first = blue.resolveToSnapshot(price);
ResolvedSnapshot second = blue.loadSnapshot(first.blueId());

System.out.println(first == second); // true when loaded from the in-memory cache
System.out.println(blue.resolvedSnapshotCacheSize());
System.out.println(blue.resolvedReferenceCacheSize());

You can preload snapshots at startup:

blue.cacheResolvedSnapshot(first);

Cache hits improve performance but do not change document identity or processor gas accounting.

Dictionary-Aware Export

A dictionary is a named collection of known Blue type definitions. When you send a document to another system, that system may tell you which dictionaries it understands. The exporter can then keep supported types as compact BlueId references and inline unsupported type definitions so the receiver still gets a self-describing document.

Register dictionaries through the generic TypeDictionary SPI:

import blue.language.dictionary.TypeDictionary;

blue.registerTypeDictionary(myDictionary);

Export for a receiver that supports one dictionary version:

import blue.language.dictionary.ExportContext;

ExportContext context = ExportContext.builder()
        .dictionary("example.types", "ExampleDictionaryBlueId")
        .build();

String yaml = blue.nodeToYaml(document, context);
String json = blue.nodeToJson(document, context);

If a referenced type belongs to example.types and is representable by ExampleDictionaryBlueId, the exported document keeps the compact reference:

request:
  type:
    blueId: <SupportedRequestTypeBlueId>

If a referenced type is known locally but not supported by the receiver, the exporter inlines the current type definition:

request:
  type:
    name: Custom Request
    amount:
      type:
        blueId: <IntegerBlueId>
    memo:
      type:
        blueId: <TextBlueId>

Inlining is recursive and cycle-checked. The exporter transforms only type metadata fields: type, itemType, keyType, and valueType. Ordinary data references remain ordinary data references.

Disable fallback in strict integrations:

ExportContext strictContext = ExportContext.builder()
        .dictionary("example.types", "ExampleDictionaryBlueId")
        .inlineUnsupportedTypes(false)
        .build();

With fallback disabled, export fails if any known type cannot be represented by the requested dictionary context.

Matching

Matching answers: does this candidate node conform to this target type or pattern?

Node event = blue.yamlToNode(
        "message:\n" +
        "  request:\n" +
        "    amount: 10\n" +
        "    currency: USD\n" +
        "  ignored:\n" +
        "    deeply: nested\n");

Node pattern = blue.yamlToNode(
        "message:\n" +
        "  request:\n" +
        "    currency: USD\n");

boolean matches = blue.nodeMatchesType(event, pattern);

For hot loops, match resolved immutable nodes:

ResolvedSnapshot eventSnapshot = blue.resolveToSnapshot(event);
ResolvedSnapshot patternSnapshot = blue.resolveToSnapshot(pattern);

boolean fast = blue.nodeMatchesType(
        eventSnapshot,
        "/message/request",
        patternSnapshot.resolvedAt("/message/request"));

The mutable compatibility matcher resolves only paths observed by the target pattern. The frozen matcher avoids mutable traversal entirely and reuses provider-backed references through local caches.

Important matching rules:

  • name and description are labels, not type-compatibility constraints;
  • pure reference pattern leaves are exact identity checks;
  • extra candidate fields are allowed unless the pattern/schema forbids them;
  • list and dictionary payload kinds are checked explicitly;
  • missing optional target fields are allowed unless they carry meaningful requirements such as schema.required: true.

Canonical Patching

Canonical patches operate on immutable roots and return new snapshots.

import blue.language.processor.model.JsonPatch;
import blue.language.snapshot.ResolvedSnapshot;

ResolvedSnapshot before = blue.resolveToSnapshot(price);

JsonPatch patch = JsonPatch.replace("/amount", new Node().value(200));
ResolvedSnapshot after = blue.applyCanonicalPatch(before, patch);

System.out.println(after.blueId());

Supported patch operations:

  • JsonPatch.add(path, value)
  • JsonPatch.replace(path, value)
  • JsonPatch.remove(path)

Patch paths are JSON Pointers. Object keys containing / or ~ must be escaped as ~1 and ~0.

Patch-time minimization removes redundant overrides where possible. If a patch writes a value equal to inherited resolved state, the canonical override can be removed rather than preserved.

Conformance And Generalization

Document processing must never commit an illegal snapshot. If a patch violates the current declared type, the processor can generalize the affected node upward through the type hierarchy.

Example:

type: Price in EUR
amount: 150
currency: EUR

If a processor changes currency to USD, the node can no longer honestly claim to be Price in EUR. It may generalize to the parent type Price, then ancestors are checked up to the root.

The generalization flow is transactional:

  1. plan the immutable patch;
  2. check conformance from changed paths upward;
  3. add canonical type/generalization patches where needed;
  4. commit the new snapshot only if the whole plan succeeds;
  5. roll back on failure.

Working Documents

WorkingDocument is a frozen preview state for processor-side read-your-writes logic. It uses the same immutable patch transaction as the processor runtime, including conformance checks, dynamic type generalization, and Type Generalization Policy enforcement, but it does not commit to the active processor runtime.

import blue.language.processor.ProcessorExecutionContext;
import blue.language.processor.WorkingDocument;
import blue.language.processor.model.JsonPatch;

WorkingDocument working = context.newWorkingDocument();

working.applyPatch(JsonPatch.replace("/price/currency", new Node().value("USD")));

String currency = (String) working.resolvedAt("/price/currency").getValue();

Working previews do not emit Document Update cascades, charge gas, update checkpoints, or write termination/marker state. Contract processors should preview first and buffer actual effects only after preview succeeds:

working.applyPatches(patches);
context.applyPatches(patches);

Use materializeCanonicalRoot(), materializeResolvedRoot(), commitToNode(), or commitSnapshot() only at explicit integration boundaries. Normal processor reads should stay on FrozenNode roots and pointer lookups.

Object Mapping

Java objects can be converted to and from Blue nodes.

import blue.language.Blue;
import blue.language.model.Node;
import blue.language.model.TypeBlueId;

@TypeBlueId("Person")
public class Person {
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

Blue blue = new Blue();

Person alice = new Person();
alice.setName("Alice");
alice.setAge(34);

Node node = blue.objectToNode(alice);
Person copy = blue.nodeToObject(node, Person.class);

@TypeBlueId declares the Blue type identity used by the mapper.

Document Processing Runtime

The library includes a generic document processor. It does not hard-code a business workflow language; instead, applications register processors for the contract types they understand.

Processor roles:

  • ChannelProcessor<T> decides whether an external event belongs to a channel;
  • HandlerProcessor<T> decides whether a handler should run and executes it;
  • ContractProcessor<T> is the base interface for marker-style contracts.

Minimal channel contract:

import blue.language.processor.model.ChannelContract;

public class ExampleChannel extends ChannelContract {
    private String eventType;

    public String getEventType() {
        return eventType;
    }

    public void setEventType(String eventType) {
        this.eventType = eventType;
    }
}

Minimal channel processor:

import blue.language.model.Node;
import blue.language.processor.ChannelEvaluationContext;
import blue.language.processor.ChannelProcessor;

public final class ExampleChannelProcessor implements ChannelProcessor<ExampleChannel> {
    @Override
    public Class<ExampleChannel> contractType() {
        return ExampleChannel.class;
    }

    @Override
    public boolean matches(ExampleChannel contract, ChannelEvaluationContext context) {
        Object eventType = context.event().getProperties().get("eventType").getValue();
        return contract.getEventType().equals(eventType);
    }

    @Override
    public String eventId(ExampleChannel contract, ChannelEvaluationContext context) {
        Node id = context.event().getProperties().get("eventId");
        return id == null ? null : String.valueOf(id.getValue());
    }
}

Minimal handler contract:

import blue.language.processor.model.HandlerContract;

public class SetCounter extends HandlerContract {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

Minimal handler processor:

import blue.language.model.Node;
import blue.language.processor.HandlerProcessor;
import blue.language.processor.ProcessorExecutionContext;
import blue.language.processor.model.JsonPatch;

public final class SetCounterProcessor implements HandlerProcessor<SetCounter> {
    @Override
    public Class<SetCounter> contractType() {
        return SetCounter.class;
    }

    @Override
    public void execute(SetCounter contract, ProcessorExecutionContext context) {
        context.applyPatch(JsonPatch.replace(
                context.resolvePointer("/counter"),
                new Node().value(contract.getValue())));
    }
}

Register processors and run a document:

import blue.language.Blue;
import blue.language.model.Node;
import blue.language.processor.DocumentProcessingResult;

Blue blue = new Blue();

Node exampleChannelType = new Node().name("ExampleChannel");
String exampleChannelBlueId = blue.calculateBlueId(exampleChannelType);
Node setCounterType = new Node().name("SetCounter");
String setCounterBlueId = blue.calculateBlueId(setCounterType);

blue.registerExternalContractType(exampleChannelBlueId, exampleChannelType, new ExampleChannelProcessor())
        .registerExternalContractType(setCounterBlueId, setCounterType, new SetCounterProcessor());

Node document = blue.yamlToNode(
        "name: Counter\n" +
        "counter: 0\n" +
        "contracts:\n" +
        "  events:\n" +
        "    type:\n" +
        "      blueId: " + exampleChannelBlueId + "\n" +
        "    eventType: counter.set\n" +
        "  setCounter:\n" +
        "    type:\n" +
        "      blueId: " + setCounterBlueId + "\n" +
        "    channel: events\n" +
        "    value: 10\n");

Node event = blue.yamlToNode(
        "eventId: evt-1\n" +
        "eventType: counter.set\n");

DocumentProcessingResult result = blue.processDocument(document, event);

System.out.println(result.blueId());
System.out.println(result.totalGas());
System.out.println(blue.nodeToYaml(result.document()));

External contract processors must register the canonical type node for the BlueId they handle. The runtime checks that every active contract in the initial processing closure is understood; if not, processing fails before state is mutated. processDocument(document, event) is the normative one-call PROCESS API and initializes scopes as part of the run when needed.

Serialization Helpers

String yaml = blue.nodeToYaml(node);
String simpleYaml = blue.nodeToSimpleYaml(node);
String json = blue.nodeToJson(node);
String simpleJson = blue.nodeToSimpleJson(node);

The normal serializers preserve Blue metadata. The simple serializers are useful when you want a simpler projection for display or application-facing output.

Main API Surface

Blue

Primary facade:

  • yamlToNode(String)
  • jsonToNode(String)
  • nodeToYaml(Node)
  • nodeToJson(Node)
  • objectToNode(Object)
  • nodeToObject(Node, Class<T>)
  • calculateBlueId(Node)
  • calculateSemanticBlueId(Node)
  • exportNode(Node, ExportContext)
  • resolve(Node)
  • canonicalize(Node)
  • resolveToSnapshot(Node)
  • loadSnapshot(String blueId)
  • applyCanonicalPatch(ResolvedSnapshot, JsonPatch)
  • nodeToJson(Node, ExportContext)
  • nodeToYaml(Node, ExportContext)
  • nodeMatchesType(Node, Node)
  • nodeMatchesType(FrozenNode, FrozenNode)
  • nodeMatchesType(ResolvedSnapshot, String, FrozenNode)
  • initializeDocument(Node)
  • processDocument(Node, Node)
  • processDocument(ResolvedSnapshot, Node)
  • conformanceReport()
  • runConformanceSuite()
  • contractsConformanceReport()
  • runContractsConformanceSuite()
  • registerContractProcessor(...)
  • registerExternalContractType(...)
  • registerTypeDictionary(...)

Node

Mutable Blue document tree. Best for parsing, authoring, compatibility, and serialization boundaries.

FrozenNode

Immutable Blue node with cached BlueId and path-index helpers. Best for runtime internals and repeated reads.

ResolvedSnapshot

Immutable canonical/resolved pair. Best for document-processing state.

NodeProvider

Reference lookup boundary for { blueId: ... } nodes.

Included providers:

  • BasicNodeProvider
  • CachingNodeProvider
  • ClasspathBasedNodeProvider
  • DirectoryBasedNodeProvider
  • SequentialNodeProvider

NodeTypeMatcher And FrozenTypeMatcher

Shared type/shape matcher. NodeTypeMatcher is the mutable compatibility adapter. FrozenTypeMatcher is the fast path for resolved immutable graphs.

Implementation Status

Implemented and covered by tests:

  • strict canonical language core;
  • RFC 8785-style canonical BlueId hashing for supported scalar/list/object cases;
  • deterministic integer and typed-Double handling;
  • reference-only blueId semantics;
  • payload-kind exclusivity;
  • schema validation for deterministic core keywords;
  • list control forms and reverse minimization;
  • circular self-reference ingestion;
  • immutable snapshots with path indexes and resolved type cache reuse;
  • canonical overlay patching and patch-time minimization;
  • dynamic type generalization with rollback;
  • fast frozen type/pattern matching;
  • snapshot-backed document processing runtime;
  • Blue Contracts and Processor 1.0 runtime registry and conformance fixtures;
  • external channel/handler/marker processor SPI with explicit canonical type registration.

Known boundaries:

  • cross-language golden fixtures are still needed for independent implementation certification;
  • provider ingestion stores strict canonical/preprocessed content and does not default to semantic resolve/minimize storage;
  • conformance/generalization is snapshot-safe at the boundary but still bridges through mutable resolver internals in some checks;
  • concrete business contracts are supplied by applications through explicitly registered processors and canonical type nodes;
  • canonical-plus-bundle transport/webhook export is not part of this module yet.

For deeper design notes, see:

Build And Test

The project currently compiles for Java 8 source/target compatibility and uses JUnit 5 for tests. Build and test with a JDK that can run Gradle 8.4.

Run the full CI-style verification command:

./gradlew clean test

Run the test suite without cleaning:

./gradlew test

Run only the Blue Language 1.0 conformance fixtures:

./gradlew test --tests '*BlueLanguageConformanceFixtureTest'

Run only the Blue Contracts and Processor 1.0 conformance fixtures:

./gradlew test --tests '*BlueContractsConformanceFixtureTest'

At runtime, new Blue().conformanceReport() returns static Blue Language 1.0 metadata: language version, core registry BlueIds, fixture package identity, fixture IDs, and fixture categories. new Blue().runConformanceSuite() executes the manifest-driven fixture suite and returns passed fixture IDs plus detailed failures with fixture ID, category, operation, exception class, and message. The fixture package under src/test/resources/blue-language-1.0/fixtures is a vendored copy of the canonical Blue Language 1.0 fixture package; its manifest identity must match the fixture package identity published by the Blue Language 1.0 specification release. The current Java fixture package identity is a SHA-256 content digest over manifest.yaml with the identity field blanked plus each manifest-listed fixture file in manifest order; verify it with BlueConformanceReport.fixturePackageIdentityMatchesFixtureFiles().

At runtime, new Blue().contractsConformanceReport() returns static Blue Contracts and Processor 1.0 metadata: fixture package identity, required fixture IDs, fixture IDs, categories, and coverage checks. new Blue().runContractsConformanceSuite() executes the separate contracts fixture suite. The contracts fixture package under src/test/resources/blue-contracts-1.0/fixtures is vendored from the official Blue Contracts 1.0 spec repository. Its release identity is sha256:2f197ca3bbdc41b75e772777cc48e51019754347e1bee26b5f3209b71d9bd9ca. The runtime registry resources are vendored from contract/1.0/registry/blue-contracts-1.0. The fixture package uses the same SHA-256 content digest scheme; verify it with BlueContractsConformanceReport.fixturePackageIdentityMatchesFixtureFiles() and contractsConformanceReport().isOfficialContracts10FixturePackage(). For release checks, both language and contracts reports should have no failures, all fixture IDs passed, required fixture coverage, exact required fixture sets, and matching fixture package identities.

Build jars:

./gradlew build

Publish to local Maven:

./gradlew publishToMavenLocal

The Gradle wrapper uses the distribution declared in gradle/wrapper/gradle-wrapper.properties. Local and CI environments need either network access for that first wrapper download or a cached Gradle distribution; offline verification works once the wrapper distribution and normal dependency cache are already present.

Project Layout

src/main/java/blue/language
  Blue.java                         primary facade
  model/                            Node, Schema, serializers, annotations
  merge/                            type resolution and merge pipeline
  preprocess/                       alias/default-blue preprocessing
  provider/                         BlueId content providers
  snapshot/                         FrozenNode and ResolvedSnapshot
  processor/                        generic document processor runtime
  conformance/                      type conformance and generalization
  utils/                            BlueId, matching, JSON pointer, helpers

docs/
  canonical-language-core.md
  frozen-type-matching.md
  processor-contract-matching.md
  snapshots-patching-and-generalization.md
  specification-implementation-gaps.md

Links

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages