1mod check;
6mod parse;
7mod render;
8
9use rustev_contract::{decimal::Decimal, time::Timestamp};
10use std::collections::{BTreeMap, BTreeSet};
11
12pub const LANGUAGE: &str = "rustev.template/1";
13pub const HELPERS: &str = "rustev.template-helpers/1";
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum Error {
17 Syntax,
18 SourceBound,
19 Nesting,
20 InvalidBind,
21 UnknownPath(String),
22 ModelOutputFlow(String),
23 NotExact(String),
24 Type(String),
25 InvalidDeclaration,
26 UnsafeRaw,
27 InvalidCurrency,
28 OutputBound,
29 MayBeEmpty,
30 Arithmetic,
31 MissingValue(String),
32 InvalidValue(String),
33 Canonical,
34}
35impl std::fmt::Display for Error {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(f, "template: {self:?}")
38 }
39}
40impl std::error::Error for Error {}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Class {
44 Exact,
45 Model,
46 Mixed,
47 Generated,
48}
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Field {
51 pub kind: Type,
52 pub class: Class,
53}
54impl Field {
55 pub fn exact(kind: Type) -> Self {
56 Self {
57 kind,
58 class: Class::Exact,
59 }
60 }
61}
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum Type {
64 Bool,
65 Integer,
66 Decimal,
67 Text {
68 max_bytes: usize,
69 },
70 Enum(Vec<String>),
71 Timestamp,
72 Maybe(Box<Field>),
73 List {
74 element: Box<Field>,
75 max_items: usize,
76 },
77 Record(BTreeMap<String, Field>),
78}
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum Value {
81 Bool(bool),
82 Integer(i64),
83 Decimal(Decimal),
84 Text(String),
85 Enum(String),
86 Timestamp(Timestamp),
87 Maybe(Option<Box<Value>>),
88 List(Vec<Value>),
89 Record(BTreeMap<String, Value>),
90}
91pub type Schema = BTreeMap<String, Field>;
92pub type Values = BTreeMap<String, Value>;
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Escape {
95 Text,
96 Html,
97}
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum Overflow {
100 Refuse,
101 Truncate,
102}
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum Lines {
105 Single,
106 Multi,
107}
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum Locale {
110 Und,
111 EnUs,
112 EnGb,
113 DeDe,
114 FrFr,
115 EsEs,
116 PlPl,
117}
118impl Locale {
119 pub fn parse(s: &str) -> Result<Self, Error> {
120 match s {
121 "und" => Ok(Self::Und),
122 "en-US" => Ok(Self::EnUs),
123 "en-GB" => Ok(Self::EnGb),
124 "de-DE" => Ok(Self::DeDe),
125 "fr-FR" => Ok(Self::FrFr),
126 "es-ES" => Ok(Self::EsEs),
127 "pl-PL" => Ok(Self::PlPl),
128 _ => Err(Error::InvalidDeclaration),
129 }
130 }
131}
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub enum Mode {
134 Backstop { bind: Option<String> },
135 Prompt,
136}
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct Options {
139 pub mode: Mode,
140 pub locale: Locale,
141 pub escape: Escape,
142 pub overflow: Overflow,
143 pub lines: Lines,
144 pub max_bytes: usize,
145 pub required: bool,
146}
147impl Options {
148 pub fn prompt() -> Self {
150 Self {
151 mode: Mode::Prompt,
152 locale: Locale::Und,
153 escape: Escape::Text,
154 overflow: Overflow::Refuse,
155 lines: Lines::Multi,
156 max_bytes: 16384,
157 required: false,
158 }
159 }
160}
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub struct Bounds {
163 pub min: usize,
164 pub max: usize,
165}
166#[derive(Debug, Clone)]
167enum Node {
168 Text(String),
169 Emit(String, Format),
170 Section(String, bool, Vec<Node>),
171}
172#[derive(Debug, Clone)]
173enum Format {
174 Plain,
175 Raw,
176 Number(u32),
177 Money(u32, Currency),
178 Date(bool),
179 Join,
180}
181#[derive(Debug, Clone)]
182enum Currency {
183 Literal(String),
184 Path(String),
185}
186#[derive(Debug, Clone)]
187pub struct Template {
188 source: String,
189 nodes: Vec<Node>,
190 schema: Schema,
191 options: Options,
192 bounds: Bounds,
193 references: BTreeSet<String>,
194}
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct Rendered {
197 pub text: String,
198 pub truncated: bool,
199 pub reads: BTreeSet<String>,
200}
201impl Template {
202 pub fn compile(source: &str, schema: Schema, options: Options) -> Result<Self, Error> {
203 check::options(&options, &schema)?;
204 let limit = if options.mode == Mode::Prompt {
205 16384
206 } else {
207 4096
208 };
209 if source.len() > limit {
210 return Err(Error::SourceBound);
211 }
212 let nodes = parse::parse(source)?;
213 let mut references = BTreeSet::new();
214 let bounds = check::nodes(&nodes, &schema, &options, &mut references)?;
215 if options.overflow == Overflow::Refuse && bounds.max > options.max_bytes {
216 return Err(Error::OutputBound);
217 }
218 if options.required && bounds.min == 0 {
219 return Err(Error::MayBeEmpty);
220 }
221 Ok(Self {
222 source: source.into(),
223 nodes,
224 schema,
225 options,
226 bounds,
227 references,
228 })
229 }
230 pub fn bounds(&self) -> Bounds {
231 self.bounds
232 }
233 pub fn references(&self) -> &BTreeSet<String> {
234 &self.references
235 }
236 pub fn canonical_source(&self) -> &str {
239 &self.source
240 }
241 pub fn render(&self, values: &Values) -> Result<Rendered, Error> {
242 render::render(self, values)
243 }
244}
245pub fn prompt_canonical<T: serde::Serialize>(document: &T) -> Result<Vec<u8>, Error> {
248 rustev_contract::canonical::canonical_bytes(document).map_err(|_| Error::Canonical)
249}
250pub fn prompt_digest<T: serde::Serialize>(document: &T) -> Result<String, Error> {
253 Ok(rustev_contract::canonical::tagged_digest(
254 "rustev.prompt-template/1",
255 &prompt_canonical(document)?,
256 ))
257}
258pub fn list_backstop(
260 options: &[String],
261 vocabulary: &[String],
262 max_items: usize,
263 required: bool,
264) -> Result<Vec<String>, Error> {
265 if !(1..=64).contains(&max_items)
266 || vocabulary.is_empty()
267 || vocabulary.len() > 256
268 || vocabulary.iter().any(|s| s.is_empty() || s.len() > 64)
269 || vocabulary.iter().collect::<BTreeSet<_>>().len() != vocabulary.len()
270 {
271 return Err(Error::InvalidDeclaration);
272 }
273 if required && options.is_empty() {
274 return Err(Error::MayBeEmpty);
275 }
276 if options.len() > max_items {
277 return Err(Error::OutputBound);
278 }
279 if options.iter().collect::<BTreeSet<_>>().len() != options.len()
280 || options.iter().any(|s| !vocabulary.contains(s))
281 {
282 return Err(Error::InvalidDeclaration);
283 }
284 Ok(options.to_vec())
285}
286pub fn prompt_header(id: &str, version: &str) -> Result<(), Error> {
289 let valid_id = (1..=128).contains(&id.len())
290 && id
291 .bytes()
292 .next()
293 .is_some_and(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
294 && id
295 .bytes()
296 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b"._-".contains(&b));
297 let parts: Vec<_> = version.split('.').collect();
298 if !valid_id
299 || parts.len() != 3
300 || parts.iter().any(|s| {
301 s.is_empty()
302 || !s.bytes().all(|b| b.is_ascii_digit())
303 || (s.len() > 1 && s.starts_with('0'))
304 })
305 {
306 return Err(Error::InvalidDeclaration);
307 }
308 Ok(())
309}
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum BindingSource {
312 Literal,
313 Profile,
314 ExactReference,
315}
316pub fn parameter_binding(
319 parameter: &Type,
320 supplied: &Field,
321 source: BindingSource,
322) -> Result<(), Error> {
323 if supplied.class == Class::Generated {
324 return Err(Error::ModelOutputFlow("parameter".into()));
325 }
326 if supplied.class != Class::Exact {
327 return Err(Error::NotExact("parameter".into()));
328 }
329 let matches = match (parameter, &supplied.kind) {
330 (Type::Bool, Type::Bool)
331 | (Type::Integer, Type::Integer)
332 | (Type::Decimal, Type::Decimal) => true,
333 (Type::Enum(options), Type::Enum(actual)) => {
334 !actual.is_empty() && actual.iter().all(|s| options.contains(s))
335 }
336 (Type::Text { max_bytes }, Type::Text { max_bytes: actual }) => {
337 source != BindingSource::ExactReference && *max_bytes <= 1024 && actual <= max_bytes
338 }
339 _ => false,
340 };
341 if matches {
342 Ok(())
343 } else {
344 Err(Error::Type("parameter".into()))
345 }
346}