1use crate::links;
38use crate::yaml::Value;
39use std::fmt;
40
41pub const ATTESTED_COMPUTATION_TYPE: &str = "Attested Computation";
43
44pub const COMPUTATION_HEADING: &str = "Computation";
46
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct Parameter {
55 pub name: Option<String>,
57 pub type_: Option<String>,
59 pub required: Option<bool>,
61}
62
63impl Parameter {
64 pub fn from_value(value: &Value) -> Option<Self> {
66 let map = value.as_mapping()?;
67 Some(Self {
68 name: map.get("name").and_then(Value::as_display_string),
69 type_: map.get("type").and_then(Value::as_display_string),
70 required: map.get("required").and_then(Value::as_bool),
71 })
72 }
73
74 pub fn list_from_value(value: &Value) -> Vec<Self> {
76 match value {
77 Value::Sequence(items) => items.iter().filter_map(Self::from_value).collect(),
78 Value::Mapping(_) => Self::from_value(value).into_iter().collect(),
79 _ => Vec::new(),
80 }
81 }
82
83 #[must_use]
85 pub fn is_required(&self) -> bool {
86 self.required.unwrap_or(false)
87 }
88}
89
90impl fmt::Display for Parameter {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "{}", self.name.as_deref().unwrap_or("(unnamed)"))?;
93 if let Some(t) = &self.type_ {
94 write!(f, ": {t}")?;
95 }
96 if self.is_required() {
97 f.write_str(" (required)")?;
98 }
99 Ok(())
100 }
101}
102
103#[derive(Clone, Debug, Default, PartialEq, Eq)]
110pub struct Executor {
111 pub resource: Option<String>,
113 pub receipt: Vec<String>,
115}
116
117impl Executor {
118 pub fn from_value(value: &Value) -> Option<Self> {
120 let map = value.as_mapping()?;
121 Some(Self {
122 resource: map.get("resource").and_then(Value::as_display_string),
123 receipt: match map.get("receipt") {
124 Some(Value::Sequence(items)) => {
125 items.iter().filter_map(Value::as_display_string).collect()
126 }
127 Some(other) => other.as_display_string().into_iter().collect(),
128 None => Vec::new(),
129 },
130 })
131 }
132}
133
134#[derive(Clone, Debug, Default, PartialEq, Eq)]
139pub struct Attester {
140 pub resource: Option<String>,
142}
143
144impl Attester {
145 pub fn from_value(value: &Value) -> Option<Self> {
147 let map = value.as_mapping()?;
148 Some(Self {
149 resource: map.get("resource").and_then(Value::as_display_string),
150 })
151 }
152}
153
154#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct InlineComputation {
157 pub code: String,
159 pub language: Option<String>,
162 pub fenced: bool,
166}
167
168#[derive(Clone, Debug, PartialEq, Eq)]
170pub enum ComputationSource {
171 Inline(InlineComputation),
173 File(String),
175 Missing,
177}
178
179impl ComputationSource {
180 #[must_use]
182 pub fn code(&self) -> Option<&str> {
183 match self {
184 Self::Inline(c) => Some(&c.code),
185 _ => None,
186 }
187 }
188
189 #[must_use]
191 pub fn path(&self) -> Option<&str> {
192 match self {
193 Self::File(p) => Some(p),
194 _ => None,
195 }
196 }
197
198 #[must_use]
200 pub fn is_missing(&self) -> bool {
201 *self == Self::Missing
202 }
203}
204
205impl fmt::Display for ComputationSource {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 match self {
208 Self::Inline(c) => {
209 write!(f, "inline ({} line(s))", c.code.lines().count())
210 }
211 Self::File(p) => write!(f, "file {p}"),
212 Self::Missing => f.write_str("(missing)"),
213 }
214 }
215}
216
217#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct AttestedComputation {
221 pub runtime: Option<String>,
224 pub parameters: Vec<Parameter>,
226 pub computation: ComputationSource,
228 pub executor: Option<Executor>,
230 pub attester: Option<Attester>,
232 pub has_redundant_inline: bool,
236}
237
238impl AttestedComputation {
239 pub fn from_parts(frontmatter: &crate::Frontmatter, body: &str) -> Self {
247 let inline = extract_inline_computation(body);
248 let path = frontmatter
249 .get("computation")
250 .and_then(Value::as_display_string)
251 .filter(|p| !p.trim().is_empty());
252
253 let (computation, has_redundant_inline) = match (path, inline) {
254 (Some(p), Some(_)) => (ComputationSource::File(p), true),
255 (Some(p), None) => (ComputationSource::File(p), false),
256 (None, Some(c)) => (ComputationSource::Inline(c), false),
257 (None, None) => (ComputationSource::Missing, false),
258 };
259
260 Self {
261 runtime: frontmatter
262 .get("runtime")
263 .and_then(Value::as_display_string)
264 .filter(|r| !r.trim().is_empty()),
265 parameters: frontmatter
266 .get("parameters")
267 .map(Parameter::list_from_value)
268 .unwrap_or_default(),
269 computation,
270 executor: frontmatter.get("executor").and_then(Executor::from_value),
271 attester: frontmatter.get("attester").and_then(Attester::from_value),
272 has_redundant_inline,
273 }
274 }
275
276 pub fn required_parameters(&self) -> impl Iterator<Item = &Parameter> {
278 self.parameters.iter().filter(|p| p.is_required())
279 }
280
281 #[must_use]
285 pub fn path_fields(&self) -> Vec<(&'static str, &str)> {
286 let mut out = Vec::new();
287 if let Some(p) = self.computation.path() {
288 out.push(("computation", p));
289 }
290 if let Some(r) = self.executor.as_ref().and_then(|e| e.resource.as_deref()) {
291 out.push(("executor.resource", r));
292 }
293 if let Some(r) = self.attester.as_ref().and_then(|a| a.resource.as_deref()) {
294 out.push(("attester.resource", r));
295 }
296 out
297 }
298}
299
300#[must_use]
308pub fn extract_inline_computation(body: &str) -> Option<InlineComputation> {
309 let section = computation_section(body)?;
310 fenced_block(§ion).or_else(|| indented_block(§ion))
311}
312
313fn computation_section(body: &str) -> Option<Vec<&str>> {
315 let mut lines = body.lines();
316 let mut level = 0;
317 for line in lines.by_ref() {
318 if let Some((l, title)) = heading(line)
319 && title.eq_ignore_ascii_case(COMPUTATION_HEADING)
320 {
321 level = l;
322 break;
323 }
324 }
325 if level == 0 {
326 return None;
327 }
328 let mut section = Vec::new();
329 for line in lines {
330 if let Some((l, _)) = heading(line)
331 && l <= level
332 {
333 break;
334 }
335 section.push(line);
336 }
337 Some(section)
338}
339
340fn heading(line: &str) -> Option<(usize, &str)> {
342 let t = line.trim_start();
343 let hashes = t.len() - t.trim_start_matches('#').len();
344 if hashes == 0 || hashes > 6 {
345 return None;
346 }
347 let rest = &t[hashes..];
348 if !rest.is_empty() && !rest.starts_with([' ', '\t']) {
349 return None;
350 }
351 Some((hashes, rest.trim().trim_end_matches('#').trim()))
352}
353
354fn fenced_block(section: &[&str]) -> Option<InlineComputation> {
356 for (i, line) in section.iter().enumerate() {
357 let t = line.trim_start();
358 for marker in ["```", "~~~"] {
359 if let Some(info) = t.strip_prefix(marker) {
360 let info = info.trim();
361 let language = (!info.is_empty()).then(|| info.to_string());
362 return finish_fenced(section, i, marker, language);
363 }
364 }
365 }
366 None
367}
368
369fn finish_fenced(
370 section: &[&str],
371 open: usize,
372 marker: &str,
373 language: Option<String>,
374) -> Option<InlineComputation> {
375 let indent = section[open].len() - section[open].trim_start().len();
376 let mut code: Vec<String> = Vec::new();
377 for line in §ion[open + 1..] {
378 if line.trim_start().starts_with(marker) {
379 break;
380 }
381 code.push(dedent(line, indent));
382 }
383 let code = trim_blank_edges(code);
384 (!code.is_empty()).then(|| InlineComputation {
385 code: code.join("\n"),
386 language,
387 fenced: true,
388 })
389}
390
391fn indented_block(section: &[&str]) -> Option<InlineComputation> {
393 let mut code: Vec<String> = Vec::new();
394 let mut started = false;
395 for line in section {
396 let is_code = line.starts_with(" ") || line.starts_with('\t');
397 if is_code {
398 started = true;
399 code.push(dedent(line, 4));
400 } else if line.trim().is_empty() {
401 if started {
402 code.push(String::new());
403 }
404 } else if started {
405 break;
406 }
407 }
408 let code = trim_blank_edges(code);
409 (!code.is_empty()).then(|| InlineComputation {
410 code: code.join("\n"),
411 language: None,
412 fenced: false,
413 })
414}
415
416fn dedent(line: &str, n: usize) -> String {
418 if let Some(rest) = line.strip_prefix('\t') {
419 return rest.to_string();
420 }
421 let strip = line.len() - line.trim_start_matches(' ').len();
422 line[strip.min(n)..].to_string()
423}
424
425fn trim_blank_edges(mut lines: Vec<String>) -> Vec<String> {
426 while lines.first().is_some_and(|l| l.trim().is_empty()) {
427 lines.remove(0);
428 }
429 while lines.last().is_some_and(|l| l.trim().is_empty()) {
430 lines.pop();
431 }
432 lines
433}
434
435#[must_use]
438pub fn contract_path_candidates(
439 contract: &AttestedComputation,
440 from: &crate::ConceptId,
441) -> Vec<(&'static str, String, Vec<String>)> {
442 contract
443 .path_fields()
444 .into_iter()
445 .map(|(field, raw)| {
446 let candidates = links::field_path_candidates(raw, from);
447 (field, raw.to_string(), candidates)
448 })
449 .collect()
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use crate::Document;
456
457 const REVENUE: &str = "\
458---
459type: Attested Computation
460title: Revenue for fiscal year
461runtime: bigquery
462parameters:
463 - { name: year, type: integer, required: true }
464executor:
465 resource: references/skills/run-on-bq.md
466 receipt: [job_id, executed_sql, result]
467attester:
468 resource: references/attesters/revenue.py
469---
470
471# Computation
472
473 SELECT SUM(amount) AS revenue
474 FROM finance.recognized_revenue
475 WHERE fiscal_year = @year
476
477The computation binds only the declared `parameters`, per the recognition
478policy.[^rev-policy]
479
480[^rev-policy]: Revenue recognition policy
481";
482
483 #[test]
484 fn reads_the_spec_contract() {
485 let doc = Document::parse(REVENUE).unwrap();
486 assert!(doc.frontmatter.is_attested_computation());
487 let c = AttestedComputation::from_parts(&doc.frontmatter, &doc.body);
488
489 assert_eq!(c.runtime.as_deref(), Some("bigquery"));
490 assert_eq!(c.parameters.len(), 1);
491 assert_eq!(c.parameters[0].name.as_deref(), Some("year"));
492 assert_eq!(c.parameters[0].type_.as_deref(), Some("integer"));
493 assert!(c.parameters[0].is_required());
494 assert_eq!(c.required_parameters().count(), 1);
495
496 let executor = c.executor.as_ref().unwrap();
497 assert_eq!(
498 executor.resource.as_deref(),
499 Some("references/skills/run-on-bq.md")
500 );
501 assert_eq!(executor.receipt, vec!["job_id", "executed_sql", "result"]);
502 assert_eq!(
503 c.attester.as_ref().unwrap().resource.as_deref(),
504 Some("references/attesters/revenue.py")
505 );
506
507 let code = c.computation.code().unwrap();
508 assert!(code.starts_with("SELECT SUM(amount) AS revenue"));
509 assert!(code.ends_with("WHERE fiscal_year = @year"));
510 assert!(!code.contains("binds only"));
512 assert!(!c.has_redundant_inline);
513 }
514
515 #[test]
516 fn fenced_blocks_win_and_carry_a_language() {
517 let body = "# Computation\n\n```sql\nSELECT 1\n```\n\nProse.\n";
518 let c = extract_inline_computation(body).unwrap();
519 assert!(c.fenced);
520 assert_eq!(c.language.as_deref(), Some("sql"));
521 assert_eq!(c.code, "SELECT 1");
522 }
523
524 #[test]
525 fn file_form_replaces_the_body_block() {
526 let doc = Document::parse(
527 "---\ntype: Attested Computation\nruntime: bigquery\n\
528 computation: references/computations/lib/revenue.sql\n---\n\n# Definition\n\nProse.\n",
529 )
530 .unwrap();
531 let c = AttestedComputation::from_parts(&doc.frontmatter, &doc.body);
532 assert_eq!(
533 c.computation.path(),
534 Some("references/computations/lib/revenue.sql")
535 );
536 assert!(!c.has_redundant_inline);
537 assert_eq!(
538 c.path_fields(),
539 vec![("computation", "references/computations/lib/revenue.sql")]
540 );
541 }
542
543 #[test]
544 fn both_forms_present_is_flagged() {
545 let doc = Document::parse(
546 "---\ntype: Attested Computation\ncomputation: x.sql\n---\n\n# Computation\n\n SELECT 1\n",
547 )
548 .unwrap();
549 let c = AttestedComputation::from_parts(&doc.frontmatter, &doc.body);
550 assert!(c.has_redundant_inline);
551 assert_eq!(c.computation.path(), Some("x.sql"));
552 }
553
554 #[test]
555 fn missing_computation_is_representable() {
556 let doc =
557 Document::parse("---\ntype: Attested Computation\n---\n\n# Definition\n").unwrap();
558 let c = AttestedComputation::from_parts(&doc.frontmatter, &doc.body);
559 assert!(c.computation.is_missing());
560 assert!(c.runtime.is_none());
561 }
562
563 #[test]
564 fn section_ends_at_the_next_same_level_heading() {
565 let body = "# Computation\n\n## Detail\n\n SELECT 1\n\n# Notes\n\n SELECT 2\n";
566 let c = extract_inline_computation(body).unwrap();
567 assert_eq!(c.code, "SELECT 1");
568 }
569
570 #[test]
571 fn dbt_template_syntax_survives() {
572 let body = "# Computation\n\n SELECT gross_profit\n FROM {{ ref('fct_income_statement') }}\n WHERE fiscal_year = {{ var('year') }}\n";
573 let c = extract_inline_computation(body).unwrap();
574 assert!(c.code.contains("{{ ref('fct_income_statement') }}"));
575 assert_eq!(c.code.lines().count(), 3);
576 }
577}