Skip to main content

onetaskgraph_core/
schema.rs

1//! The JSON Schema bundle both SDKs are generated from.
2//!
3//! It is emitted by `onetaskgraph schema` rather than committed, so the schema a
4//! consumer generates against can never drift from the types this binary actually
5//! serialises: they are the same types.
6
7use std::collections::BTreeMap;
8
9use onetaskgraph_plugin_api::{
10    Capabilities, DependencyEdge, DependencyEndpoint, DependencyKind, Direction, Document,
11    DocumentQuery, Health, ItemKind, Label, Location, Page, PageRequest, Project, ProjectQuery,
12    Repository, SourceError, Status, StatusCategory, Task, TaskQuery, TextFields,
13};
14use schemars::{Schema, schema_for};
15use serde_json::{Value, json};
16
17use crate::config::{EffectiveConfig, Origin, OutputFormat, Setting};
18use crate::registry::registry;
19use crate::secrets::{CredentialLayer, ResolvedCredential, SecretsReport};
20use crate::{
21    CopyAction, CopyOutcome, CopyReport, GlobalId, PageToken, Predicate, Qualified, QualifiedEdge,
22    QualifiedEndpoint, QueryPlan, QueryResponse, SearchHit, SearchKind, SourceFailure,
23    SourceListing, SourcePlan,
24};
25
26/// The bundle's own version, bumped whenever any root's schema changes — added, removed,
27/// renamed, **or altered inside**.
28///
29/// Consumers generate code from this document, so the version is part of the
30/// contract rather than a convenience: an SDK can refuse a bundle it was not
31/// generated against instead of silently emitting the wrong models. A property added to an
32/// existing root is a new field in both SDKs' generated models exactly as a new root is a
33/// new model, which is why the reach is the whole document rather than the set of names.
34///
35/// What each version brought is what `git log` answers; what this number owes a reader is
36/// that it moves whenever [`schema_bundle`] below emits a different document. The golden
37/// that holds it to that is `PUBLISHED_BUNDLES` in `tests/engine.rs`, which records every
38/// root's schema by digest from this version on.
39pub const SCHEMA_BUNDLE_VERSION: u32 = 10;
40
41/// Every contract root, keyed by name, plus each registered plugin's config schema.
42#[must_use]
43pub fn schema_bundle() -> Value {
44    let mut roots: BTreeMap<&'static str, Schema> = BTreeMap::new();
45
46    roots.insert("Task", schema_for!(Task));
47    roots.insert("Project", schema_for!(Project));
48    roots.insert("Document", schema_for!(Document));
49    // A root of its own although both `Task` and `Project` reach it inside their own
50    // definitions, for the reason `TextFields` is one: a consumer acts on a location by
51    // asking which of the two keys is present, so the shape it switches on has to be
52    // nameable rather than only reachable.
53    roots.insert("Location", schema_for!(Location));
54    roots.insert("Label", schema_for!(Label));
55    roots.insert("Status", schema_for!(Status));
56    roots.insert("StatusCategory", schema_for!(StatusCategory));
57    roots.insert("DependencyEdge", schema_for!(DependencyEdge));
58    roots.insert("DependencyEndpoint", schema_for!(DependencyEndpoint));
59    roots.insert("QualifiedEndpoint", schema_for!(QualifiedEndpoint));
60    roots.insert("ItemKind", schema_for!(ItemKind));
61    roots.insert("Repository", schema_for!(Repository));
62    roots.insert("DependencyKind", schema_for!(DependencyKind));
63    roots.insert("Direction", schema_for!(Direction));
64    // Roots of their own although both are reachable inside `TaskQuery`'s definitions,
65    // which is enough for a generator and not enough for a reconciliation: the command
66    // line spells both deliberately differently (`both` for `title-or-content`, `task`
67    // for `tasks`), so a variant added to either would leave the command line quietly
68    // unable to name it. A root apiece gives that gate one document to read.
69    roots.insert("TextFields", schema_for!(TextFields));
70    roots.insert("SearchKind", schema_for!(SearchKind));
71
72    roots.insert("TaskQuery", schema_for!(TaskQuery));
73    roots.insert("ProjectQuery", schema_for!(ProjectQuery));
74    roots.insert("DocumentQuery", schema_for!(DocumentQuery));
75    roots.insert("PageRequest", schema_for!(PageRequest));
76    roots.insert("PageOfTask", schema_for!(Page<Task>));
77    roots.insert("PageOfProject", schema_for!(Page<Project>));
78    roots.insert("PageOfDocument", schema_for!(Page<Document>));
79    roots.insert("PageOfLabel", schema_for!(Page<Label>));
80    roots.insert("PageOfDependencyEdge", schema_for!(Page<DependencyEdge>));
81
82    roots.insert("Capabilities", schema_for!(Capabilities));
83    roots.insert("Health", schema_for!(Health));
84    roots.insert("SourceError", schema_for!(SourceError));
85
86    roots.insert("GlobalId", schema_for!(GlobalId));
87    roots.insert("PageToken", schema_for!(PageToken));
88    roots.insert("QueryPlan", schema_for!(QueryPlan));
89    roots.insert("SourcePlan", schema_for!(SourcePlan));
90    roots.insert("Predicate", schema_for!(Predicate));
91    roots.insert("SourceFailure", schema_for!(SourceFailure));
92    roots.insert("QualifiedTask", schema_for!(Qualified<Task>));
93    roots.insert("QualifiedProject", schema_for!(Qualified<Project>));
94    roots.insert("QualifiedDocument", schema_for!(Qualified<Document>));
95    roots.insert("QualifiedLabel", schema_for!(Qualified<Label>));
96    roots.insert("QualifiedEdge", schema_for!(QualifiedEdge));
97    roots.insert("SearchHit", schema_for!(SearchHit));
98    roots.insert("SourceListing", schema_for!(SourceListing));
99    roots.insert("SourceListings", schema_for!(Vec<SourceListing>));
100    roots.insert(
101        "QueryResponseOfQualifiedTask",
102        schema_for!(QueryResponse<Qualified<Task>>),
103    );
104    roots.insert(
105        "QueryResponseOfQualifiedProject",
106        schema_for!(QueryResponse<Qualified<Project>>),
107    );
108    roots.insert(
109        "QueryResponseOfQualifiedDocument",
110        schema_for!(QueryResponse<Qualified<Document>>),
111    );
112    roots.insert(
113        "QueryResponseOfQualifiedLabel",
114        schema_for!(QueryResponse<Qualified<Label>>),
115    );
116    roots.insert(
117        "QueryResponseOfQualifiedEdge",
118        schema_for!(QueryResponse<QualifiedEdge>),
119    );
120    roots.insert(
121        "QueryResponseOfSearchHit",
122        schema_for!(QueryResponse<SearchHit>),
123    );
124
125    roots.insert("CopyReport", schema_for!(CopyReport));
126    roots.insert("CopyOutcome", schema_for!(CopyOutcome));
127    roots.insert("CopyAction", schema_for!(CopyAction));
128
129    roots.insert("EffectiveConfig", schema_for!(EffectiveConfig));
130    roots.insert("Setting", schema_for!(Setting));
131    roots.insert("Origin", schema_for!(Origin));
132    roots.insert("OutputFormat", schema_for!(OutputFormat));
133    roots.insert("SecretsReport", schema_for!(SecretsReport));
134    roots.insert("ResolvedCredential", schema_for!(ResolvedCredential));
135    roots.insert("CredentialLayer", schema_for!(CredentialLayer));
136
137    let plugins: BTreeMap<String, Schema> = registry()
138        .iter()
139        .map(|plugin| (plugin.kind().to_owned(), plugin.config_schema()))
140        .collect();
141
142    json!({
143        "version": SCHEMA_BUNDLE_VERSION,
144        "roots": roots,
145        "plugin_config": plugins,
146    })
147}