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