1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//! The OKF concept document: YAML frontmatter + markdown body.
//!
//! The parse, serialize, and validation behaviour is a faithful port of the
//! reference implementation's `OKFDocument`
//! (`okf/src/reference_agent/bundle/document.py`), so documents round-trip
//! compatibly between the two. Ported to Rust and modified from the original
//! Apache-2.0 Python source; see the NOTICE file.
//!
//! On top of parsing, [`Document`] exposes the v0.2 body conventions that pair
//! with frontmatter: footnote attribution keyed to `sources[].id` (§5.1) and
//! the `# Computation` block of an Attested Computation (§10.3).
use crate::computation::{AttestedComputation, InlineComputation};
use crate::error::DocumentError;
use crate::footnotes::{self, FootnoteDef, FootnoteRef};
use crate::frontmatter::{Frontmatter, RECOMMENDED_FRONTMATTER_KEYS, REQUIRED_FRONTMATTER_KEYS};
use crate::links::{self, Citation, Link};
use crate::provenance::{self, Attribution};
use crate::yaml::Value;
const FRONTMATTER_DELIM: &str = "---";
/// A parsed OKF concept document.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Document {
/// The YAML frontmatter block (empty if the file had none).
pub frontmatter: Frontmatter,
/// Everything after the frontmatter.
pub body: String,
}
impl Document {
/// Creates a document from frontmatter and a body.
pub fn new(frontmatter: Frontmatter, body: impl Into<String>) -> Self {
Document {
frontmatter,
body: body.into(),
}
}
/// Parses a document from raw file text.
///
/// If the file does not begin with a `---` frontmatter delimiter, the
/// entire text is treated as the body and the frontmatter is empty
/// (matching the reference parser). An opened-but-unclosed frontmatter
/// block is an error.
pub fn parse(text: &str) -> Result<Document, DocumentError> {
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() || lines[0].trim() != FRONTMATTER_DELIM {
return Ok(Document {
frontmatter: Frontmatter::new(),
body: text.to_string(),
});
}
let mut end_idx = None;
for (i, line) in lines.iter().enumerate().skip(1) {
if line.trim() == FRONTMATTER_DELIM {
end_idx = Some(i);
break;
}
}
let end_idx = end_idx.ok_or(DocumentError::UnterminatedFrontmatter)?;
let fm_text = lines[1..end_idx].join("\n");
let value = Value::parse(&fm_text)?;
let frontmatter = match value {
Value::Null => Frontmatter::new(),
Value::Mapping(m) => Frontmatter::from_mapping(m),
_ => return Err(DocumentError::FrontmatterNotMapping),
};
let mut body = lines[end_idx + 1..].join("\n");
if let Some(stripped) = body.strip_prefix('\n') {
body = stripped.to_string();
}
Ok(Document { frontmatter, body })
}
/// Serializes the document back to text: frontmatter delimited by `---`,
/// a blank line, then the body (terminated by a newline).
///
/// `parse` followed by `serialize` preserves frontmatter key order and the
/// body (modulo trailing-newline normalization), matching the reference.
/// Flow collections are re-emitted in block style, which is the same value
/// written differently.
pub fn serialize(&self) -> String {
let fm_text = Value::Mapping(self.frontmatter.as_mapping().clone())
.to_yaml_string()
.trim_end()
.to_string();
let body = if self.body.ends_with('\n') {
self.body.clone()
} else {
format!("{}\n", self.body)
};
format!("{FRONTMATTER_DELIM}\n{fm_text}\n{FRONTMATTER_DELIM}\n\n{body}")
}
/// Validates the document against §11: the frontmatter must carry a
/// non-empty `type`, and nothing else is required.
///
/// That single check is the whole of document-level validation in v0.2, and
/// it matches the reference implementation's `OKFDocument.validate`. Every
/// other field the spec describes is a SHOULD, so a concept carrying only
/// `type` passes here; see [`Document::missing_recommended`] for the
/// producer-side checklist and
/// [`validate_bundle`](crate::validate_bundle) for the full diagnostics.
pub fn validate(&self) -> Result<(), DocumentError> {
let missing: Vec<String> = REQUIRED_FRONTMATTER_KEYS
.iter()
.filter(|key| !self.has(key))
.map(|key| key.to_string())
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(DocumentError::MissingKeys(missing))
}
}
/// The [recommended](RECOMMENDED_FRONTMATTER_KEYS) frontmatter keys this
/// document leaves unset, plus `runtime` when the concept is an Attested
/// Computation, which §10.2 requires it to carry.
///
/// None of these is a conformance failure, so [`Document::validate`]
/// ignores them: §11 forbids rejecting a concept for a missing optional
/// field. This is the checklist a *producer* wants before publishing, and
/// it is what [`validate_bundle`](crate::validate_bundle) reports as
/// warnings. An empty result means the document is fully filled in.
///
/// `generated` counts as set when a legacy v0.1 `timestamp` stands in for
/// it, since §13.1 lets consumers read one for the other.
pub fn missing_recommended(&self) -> Vec<&'static str> {
let mut missing: Vec<&'static str> = RECOMMENDED_FRONTMATTER_KEYS
.iter()
.copied()
.filter(|key| match *key {
"generated" => !self.has("generated") && !self.has("timestamp"),
other => !self.has(other),
})
.collect();
if self.frontmatter.is_attested_computation() && !self.has("runtime") {
missing.push("runtime");
}
missing
}
/// Whether a frontmatter key is present and carries a non-empty value.
fn has(&self, key: &str) -> bool {
self.frontmatter
.get(key)
.is_some_and(|value| !value.is_empty_value())
}
/// Extracts all markdown links found in the body (§6.1).
pub fn links(&self) -> Vec<Link> {
links::extract_links(&self.body)
}
/// The non-blank lines under a top-level `# heading` in the body, up to the
/// next top-level heading.
///
/// §4.2 gives `# Schema`, `# Examples`, and `# Computation` conventional
/// meaning without attaching required behaviour, so this is the primitive a
/// consumer needs to read any of them. A port of the reference's
/// `_section_content_lines`, including its details: `heading` is matched in
/// full (pass `"# Schema"`), only `# ` counts as a heading so `##`
/// subheadings stay inside the section, and each line keeps its original
/// indentation.
///
/// Returns an empty vector when no such section exists. A repeated heading
/// contributes its lines to the same result.
pub fn section(&self, heading: &str) -> Vec<&str> {
let mut in_section = false;
let mut lines = Vec::new();
for line in self.body.lines() {
let trimmed = line.trim();
if trimmed.starts_with("# ") {
in_section = trimmed == heading;
continue;
}
if in_section && !trimmed.is_empty() {
lines.push(line);
}
}
lines
}
/// Extracts the body's `[^label]` attribution markers (§5.1).
pub fn footnote_refs(&self) -> Vec<FootnoteRef> {
footnotes::extract_refs(&self.body)
}
/// Extracts the body's `[^label]: text` footnote definitions (§5.1).
pub fn footnote_definitions(&self) -> Vec<FootnoteDef> {
footnotes::extract_definitions(&self.body)
}
/// Joins the body's footnotes to the `sources` entries they name, giving
/// per-claim attribution (§5.1).
///
/// Labels that match no source are still returned, with
/// [`Attribution::source`] set to `None`.
pub fn attributions(&self) -> Vec<Attribution> {
provenance::attributions(&self.frontmatter.sources(), &self.body)
}
/// The `# Computation` code block from the body, if there is one (§10.3).
pub fn inline_computation(&self) -> Option<InlineComputation> {
crate::computation::extract_inline_computation(&self.body)
}
/// The Attested Computation contract: the computation frontmatter (§10.2)
/// resolved against the body's `# Computation` block (§10.3).
///
/// Returns `None` unless `type` is `Attested Computation`; call
/// [`AttestedComputation::from_parts`] directly to read the same keys off a
/// concept of another type.
pub fn attested_computation(&self) -> Option<AttestedComputation> {
self.frontmatter
.is_attested_computation()
.then(|| AttestedComputation::from_parts(&self.frontmatter, &self.body))
}
/// Extracts numbered entries from a legacy v0.1 `# Citations` section.
///
/// v0.2 supersedes this with `sources` and footnote attribution (§5.1);
/// [`Document::attributions`] is the v0.2 equivalent. Consumers MAY keep
/// reading `# Citations` for v0.1 documents (§13.1).
pub fn citations(&self) -> Vec<Citation> {
links::extract_citations(&self.body)
}
/// `true` when the body carries a legacy `# Citations` section, which a
/// v0.2 producer should have migrated to `sources` (§13.1).
pub fn has_legacy_citations(&self) -> bool {
!self.citations().is_empty()
}
}