Line data Source code
1 : //! Client functions: a declarative description of an API that shadows a namespace of `CALL` on a connection.
2 : //!
3 : //! A host registers a spec on the catalog of a connection (see `docs/design/client_functions.md`). The core reads the
4 : //! generic part of the spec: namespaces, functions, typed named options, nested schemas and result columns. Everything
5 : //! a request needs (methods, paths, API names) stays in the document for the runtime of the host. The core only checks
6 : //! the parts of it that would otherwise fail at run time, or send the wrong request.
7 :
8 : use crate::error::{Error, Result};
9 : use crate::hash::HashMap;
10 : use serde::{Deserialize, Deserializer};
11 :
12 : /// The version of the format that the core reads
13 : pub const CLIENT_FUNCTION_FORMAT: u32 = 1;
14 :
15 : /// The standard option that takes the next page of a paged function
16 : pub const OPTION_NEXT: &str = "_next";
17 : /// The standard option that limits the pages of a call
18 : pub const OPTION_PAGES: &str = "_pages";
19 : /// The standard option that polls a pending result
20 : pub const OPTION_POLL: &str = "_poll";
21 : /// The standard option that waits for a pending result
22 : pub const OPTION_WAIT: &str = "_wait";
23 :
24 : /// The standard column of the HTTP status
25 : pub const COLUMN_STATUS: &str = "_status";
26 : /// The standard column of the value for the next page
27 : pub const COLUMN_NEXT: &str = "_next";
28 : /// The standard column of the value to poll, only on functions that can answer before they finish
29 : pub const COLUMN_POLL: &str = "_poll";
30 : /// The standard column of the response fields outside the rows
31 : pub const COLUMN_META: &str = "_meta";
32 : /// The standard column of the row fields that the spec does not list
33 : pub const COLUMN_RAW: &str = "_raw";
34 :
35 : /// What an option or attribute that the spec does not list does
36 : #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
37 : #[serde(rename_all = "lowercase")]
38 : pub enum ClientFunctionUnknownPolicy {
39 : /// An error
40 : #[default]
41 : Error,
42 : /// A warning, it is sent as written. For schemas that may be incomplete.
43 : Warning,
44 : /// Accepted silently, for free-form maps
45 : Allow,
46 : }
47 :
48 : /// Does a function change anything?
49 : #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
50 : #[serde(rename_all = "lowercase")]
51 : pub enum ClientFunctionEffect {
52 : Read,
53 : Write,
54 : }
55 :
56 : /// An option of a function or an attribute of a schema
57 : #[derive(Clone, Debug, Default, Deserialize)]
58 : #[serde(default)]
59 : pub struct ClientFunctionAttribute {
60 : /// The name in a script
61 : pub name: String,
62 : /// The spelling of the API. Only the checks of a spec read it, the runtime sends it.
63 : pub api_name: Option<String>,
64 : /// Options only: where the value goes
65 : #[serde(rename = "in")]
66 : pub location: Option<String>,
67 : /// The type of a scalar value
68 : #[serde(rename = "type")]
69 : pub value_type: Option<String>,
70 : /// The schema of an object value
71 : pub schema: Option<String>,
72 : /// Is the value a map? Its keys are free, the type or schema holds for its values.
73 : pub map: bool,
74 : /// How many arrays the value is nested in. Only completion reads it.
75 : pub array: u32,
76 : /// The values of a scalar. Only completion reads them, they aren't checked.
77 : #[serde(rename = "enum")]
78 : pub values: Vec<serde_json::Value>,
79 : /// The format of a scalar, such as `date-time`. Only completion reads it.
80 : pub format: Option<String>,
81 : /// Is the attribute required?
82 : pub required: bool,
83 : /// The description that completion shows
84 : pub description: Option<String>,
85 : }
86 :
87 : /// The attribute of a union whose value selects the variant
88 : #[derive(Clone, Debug, Default, Deserialize)]
89 : #[serde(default)]
90 : pub struct ClientFunctionTag {
91 : /// The attribute
92 : pub attribute: String,
93 : /// The variant of a value, whatever its case. A value that is not listed selects nothing.
94 : pub values: HashMap<String, String>,
95 : }
96 :
97 : /// A named object, or a union of named objects
98 : #[derive(Clone, Debug, Default, Deserialize)]
99 : #[serde(default)]
100 : pub struct ClientFunctionSchema {
101 : /// The attributes, a union has none
102 : pub attributes: Vec<ClientFunctionAttribute>,
103 : /// What an attribute that is not listed does
104 : pub unknown: ClientFunctionUnknownPolicy,
105 : /// The schemas that a value of a union can be
106 : pub variants: Vec<String>,
107 : /// What selects the variant of a union, without it only the attributes of a value decide
108 : pub tag: Option<ClientFunctionTag>,
109 : }
110 :
111 : impl ClientFunctionSchema {
112 : /// Does an object of this schema take a name?
113 146 : fn takes(&self, name: &str, quoted: bool) -> bool {
114 146 : self.unknown == ClientFunctionUnknownPolicy::Allow || find_attribute(&self.attributes, name, quoted).is_some()
115 146 : }
116 : }
117 :
118 : /// A field of an object, as far as it decides a variant
119 : #[derive(Clone, Debug, Default)]
120 : pub struct ClientFunctionField {
121 : /// The name in the script
122 : pub name: String,
123 : /// Was the name quoted?
124 : pub quoted: bool,
125 : /// The value, if it is a string
126 : pub text: Option<String>,
127 : }
128 :
129 : /// What an object can be
130 : #[derive(Debug)]
131 : pub struct ClientFunctionFit<'a> {
132 : /// The variants that fit, the only one of a schema that is no union. Never empty.
133 : pub variants: Vec<&'a ClientFunctionSchema>,
134 : /// The field whose value selected the variant
135 : pub tag_field: Option<usize>,
136 : /// The fields that another variant takes, but none that fits the fields before them
137 : pub conflicts: Vec<usize>,
138 : }
139 :
140 : impl ClientFunctionFit<'_> {
141 : /// What an attribute that no variant lists does: the most lenient variant decides
142 9 : pub fn unknown(&self) -> ClientFunctionUnknownPolicy {
143 19 : let policy = |p: ClientFunctionUnknownPolicy| self.variants.iter().any(|v| v.unknown == p);
144 9 : if policy(ClientFunctionUnknownPolicy::Allow) {
145 1 : ClientFunctionUnknownPolicy::Allow
146 8 : } else if policy(ClientFunctionUnknownPolicy::Warning) {
147 1 : ClientFunctionUnknownPolicy::Warning
148 : } else {
149 7 : ClientFunctionUnknownPolicy::Error
150 : }
151 9 : }
152 : }
153 :
154 : /// A result column
155 : #[derive(Clone, Debug, Default, Deserialize)]
156 : #[serde(default)]
157 : pub struct ClientFunctionColumn {
158 : /// The name in a script
159 : pub name: String,
160 : /// The column type
161 : #[serde(rename = "type")]
162 : pub column_type: Option<String>,
163 : }
164 :
165 : /// The paging of a function, as far as checks need it
166 : #[derive(Clone, Debug, Default, Deserialize)]
167 : #[serde(default)]
168 : pub struct ClientFunctionPaging {
169 : /// The kind: `url`, `token`, `offset` or `page`
170 : pub kind: String,
171 : /// The option that `_next` sets, none for `url`
172 : pub option: Option<String>,
173 : /// Where the value for the next page is
174 : pub from: Option<serde_json::Value>,
175 : /// The option with the page size
176 : pub size: Option<String>,
177 : /// When the list ends, a documented rule if it's an object
178 : pub end: Option<serde_json::Value>,
179 : }
180 :
181 : /// A function of a spec
182 : #[derive(Clone, Debug, Default, Deserialize)]
183 : #[serde(default)]
184 : pub struct ClientFunction {
185 : /// The name in a script
186 : pub name: String,
187 : /// The grouping for documentation and completion
188 : pub family: Option<String>,
189 : /// The description that completion shows
190 : pub description: Option<String>,
191 : /// A call that completion shows
192 : pub example: Option<String>,
193 : /// What a result holds, completion shows it
194 : pub result_description: Option<String>,
195 : /// The HTTP method, it decides the default effect
196 : pub method: Option<String>,
197 : /// The path below the host, with `{placeholders}`
198 : pub path: Option<String>,
199 : /// The options
200 : pub options: Vec<ClientFunctionAttribute>,
201 : /// What an option that is not listed does
202 : pub unknown: ClientFunctionUnknownPolicy,
203 : /// The JSON pointer to the rows of a response, absent means one row
204 : pub rows: Option<String>,
205 : /// The result columns
206 : pub columns: Vec<ClientFunctionColumn>,
207 : /// The paging, if the function pages
208 : pub paging: Option<ClientFunctionPaging>,
209 : /// Absent takes the default of the spec, null means none
210 : #[serde(deserialize_with = "present")]
211 : pub pending: Option<serde_json::Value>,
212 : /// The declared effect
213 : pub effect: Option<ClientFunctionEffect>,
214 : /// The escape hatch: any option goes
215 : pub raw: bool,
216 : }
217 :
218 : /// Tell a null value from an absent field
219 110 : fn present<'de, D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Option<serde_json::Value>, D::Error> {
220 110 : serde_json::Value::deserialize(deserializer).map(Some)
221 110 : }
222 :
223 : impl ClientFunction {
224 : /// The effect, a write without a declaration unless the method only reads. A raw function can do anything.
225 281 : pub fn effect(&self) -> ClientFunctionEffect {
226 281 : match (self.raw, self.effect, self.method.as_deref()) {
227 15 : (true, ..) => ClientFunctionEffect::Write,
228 10 : (false, Some(effect), _) => effect,
229 254 : (false, None, Some(method)) if method.eq_ignore_ascii_case("GET") => ClientFunctionEffect::Read,
230 103 : (false, None, _) => ClientFunctionEffect::Write,
231 : }
232 281 : }
233 :
234 : /// What an option that is not listed does
235 45 : pub fn unknown_options(&self) -> ClientFunctionUnknownPolicy {
236 45 : if self.raw { ClientFunctionUnknownPolicy::Allow } else { self.unknown }
237 45 : }
238 :
239 : /// The standard options of the function
240 210 : pub fn standard_options(&self, spec: &ClientFunctionSpec) -> Vec<&'static str> {
241 210 : let mut options = Vec::new();
242 210 : if self.paging.is_some() {
243 67 : options.push(OPTION_NEXT);
244 67 : if self.effect() == ClientFunctionEffect::Read {
245 67 : options.push(OPTION_PAGES);
246 67 : }
247 143 : }
248 210 : if spec.has_pending(self) {
249 5 : options.push(OPTION_POLL);
250 5 : options.push(OPTION_WAIT);
251 205 : }
252 210 : options
253 210 : }
254 : }
255 :
256 : /// The spec of client functions
257 : #[derive(Clone, Debug, Default, Deserialize)]
258 : #[serde(default)]
259 : pub struct ClientFunctionSpec {
260 : /// The version of the format
261 : pub format: u32,
262 : /// The prefix of the functions
263 : pub namespace: String,
264 : /// The title of the service
265 : pub title: Option<String>,
266 : /// The default for functions that can answer before they finish
267 : pub pending: Option<serde_json::Value>,
268 : /// The named objects
269 : pub schemas: HashMap<String, ClientFunctionSchema>,
270 : /// The functions
271 : pub functions: Vec<ClientFunction>,
272 : /// The functions by name
273 : #[serde(skip)]
274 : functions_by_name: HashMap<String, usize>,
275 : }
276 :
277 : /// Is a name a plain identifier in lower case?
278 1208 : fn is_plain_lowercase(name: &str) -> bool {
279 1208 : let mut bytes = name.bytes();
280 11048 : bytes.next().is_some_and(|c| c.is_ascii_lowercase() || c == b'_') && bytes.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_')
281 1208 : }
282 :
283 : /// Check the attributes of a function or a schema
284 2129 : fn check_attributes(owner: &str, attributes: &[ClientFunctionAttribute], schemas: &HashMap<String, ClientFunctionSchema>) -> Result<()> {
285 3991 : for (i, attribute) in attributes.iter().enumerate() {
286 3950 : if attribute.name.is_empty() {
287 0 : return Err(Error::invalid_argument(format!("{owner}: an attribute has no name")));
288 3950 : }
289 7418 : if attributes[..i].iter().any(|a| a.name.eq_ignore_ascii_case(&attribute.name)) {
290 1 : return Err(Error::invalid_argument(format!("{owner}: duplicate attribute {}", attribute.name)));
291 3949 : }
292 3949 : if let Some(schema) = attribute.schema.as_ref().filter(|s| !schemas.contains_key(*s)) {
293 1 : return Err(Error::invalid_argument(format!("{owner}.{}: unknown schema {schema}", attribute.name)));
294 3948 : }
295 : }
296 2127 : Ok(())
297 2129 : }
298 :
299 : /// Check the variants and the tag of a union
300 1096 : fn check_union(owner: &str, schema: &ClientFunctionSchema, schemas: &HashMap<String, ClientFunctionSchema>) -> Result<()> {
301 1096 : let invalid = |message: String| Err(Error::invalid_argument(format!("{owner}: {message}")));
302 1096 : if schema.variants.is_empty() {
303 868 : return if schema.tag.is_some() { invalid("a tag needs variants".to_string()) } else { Ok(()) };
304 228 : }
305 228 : if !schema.attributes.is_empty() {
306 1 : return invalid("a union has variants instead of attributes".to_string());
307 227 : }
308 227 : let mut attributes: Vec<&ClientFunctionAttribute> = Vec::new();
309 448 : for name in &schema.variants {
310 448 : let Some(variant) = schemas.get(name) else { return invalid(format!("unknown variant {name}")) };
311 447 : if !variant.variants.is_empty() {
312 1 : return invalid(format!("the variant {name} is a union"));
313 446 : }
314 : // A name means one thing in a union, so that neither a request nor a nested check depends on the variant
315 962 : for attribute in &variant.attributes {
316 1370 : match attributes.iter().find(|a| a.name.eq_ignore_ascii_case(&attribute.name)) {
317 208 : Some(other) if other.api_name != attribute.api_name || other.schema != attribute.schema => {
318 2 : return invalid(format!("the variants disagree on the attribute {}", attribute.name));
319 : }
320 206 : Some(_) => {}
321 754 : None => attributes.push(attribute),
322 : }
323 : }
324 : }
325 223 : for (value, name) in schema.tag.iter().flat_map(|t| t.values.iter()) {
326 209 : let tag = schema.tag.as_ref().map_or("", |t| t.attribute.as_str());
327 209 : if !schema.variants.contains(name) {
328 1 : return invalid(format!("the tag value {value} selects {name}, which is no variant"));
329 208 : }
330 208 : if schemas.get(name).is_some_and(|v| find_attribute(&v.attributes, tag, false).is_none()) {
331 1 : return invalid(format!("the variant {name} has no attribute {tag}"));
332 207 : }
333 : }
334 221 : Ok(())
335 1096 : }
336 :
337 : /// Check how a function pages. An offset advances by the rows a page returned, and a list without a documented end ends
338 : /// with a page without rows, so without `rows` they would count the whole response as one row and never end.
339 1029 : fn check_paging(function: &ClientFunction) -> Result<()> {
340 1029 : let Some(paging) = &function.paging else { return Ok(()) };
341 229 : let invalid = |message: String| Err(Error::invalid_argument(format!("{}: {message}", function.name)));
342 229 : let documented_end = paging.end.as_ref().is_some_and(serde_json::Value::is_object);
343 229 : let (from, option, rows) = match paging.kind.as_str() {
344 229 : "url" => (true, false, false),
345 112 : "token" => (true, true, false),
346 8 : "offset" => (false, true, true),
347 4 : "page" => (false, true, !documented_end),
348 1 : kind => return invalid(format!("the unknown paging kind {kind:?}")),
349 : };
350 228 : if from && paging.from.is_none() {
351 1 : return invalid(format!("paging by {} needs `from`", paging.kind));
352 227 : }
353 227 : if option && paging.option.is_none() {
354 1 : return invalid(format!("paging by {} needs `option`", paging.kind));
355 226 : }
356 226 : if rows && function.rows.is_none() {
357 3 : return invalid(format!("paging by {} needs `rows`", paging.kind));
358 223 : }
359 223 : for name in paging.option.iter().chain(paging.size.iter()) {
360 312 : if !function.options.iter().any(|o| o.name == *name) {
361 2 : return invalid(format!("the paging names the unknown option {name}"));
362 106 : }
363 : }
364 221 : Ok(())
365 1029 : }
366 :
367 : /// The placeholders of a path
368 1240 : fn placeholders(path: &str) -> impl Iterator<Item = &str> {
369 1240 : path.split('{').skip(1).filter_map(|rest| rest.split_once('}').map(|(name, _)| name))
370 1240 : }
371 :
372 : /// Check that the path options of a function fill its path: a required option for every placeholder, and no option that
373 : /// has no placeholder and wouldn't be sent.
374 1021 : fn check_path(function: &ClientFunction) -> Result<()> {
375 1021 : let invalid = |message: String| Err(Error::invalid_argument(format!("{}: {message}", function.name)));
376 1021 : let path = function.path.as_deref().unwrap_or("");
377 1021 : let api_name = |option: &'_ ClientFunctionAttribute| option.api_name.clone().unwrap_or_else(|| option.name.clone());
378 2705 : let is_path = |option: &&ClientFunctionAttribute| option.location.as_deref() == Some("path");
379 1021 : for placeholder in placeholders(path) {
380 221 : match function.options.iter().filter(is_path).find(|o| api_name(o) == placeholder) {
381 218 : Some(option) if option.required => {}
382 1 : Some(option) => return invalid(format!("the path option {} has to be required", option.name)),
383 2 : None => return invalid(format!("no option fills the placeholder {{{placeholder}}}")),
384 : }
385 : }
386 1018 : for option in function.options.iter().filter(is_path) {
387 219 : if !placeholders(path).any(|placeholder| placeholder == api_name(option)) {
388 1 : return invalid(format!("the path has no placeholder for the option {}", option.name));
389 218 : }
390 : }
391 1017 : Ok(())
392 1021 : }
393 :
394 : impl ClientFunctionSpec {
395 : /// Check how a function answers before it finishes. Repeating a request is only safe if it reads.
396 1017 : fn check_pending(&self, function: &ClientFunction) -> Result<()> {
397 1017 : let invalid = |message: String| Err(Error::invalid_argument(format!("{}: {message}", function.name)));
398 1017 : let Some(pending) = function.pending.as_ref().or(self.pending.as_ref()).filter(|p| !p.is_null()) else { return Ok(()) };
399 110 : let poll = &pending["poll"];
400 110 : if !pending["status"].is_number() || !(poll["header"].is_string() || poll["repeat"] == true) {
401 2 : return invalid("a pending rule needs a `status`, and a `poll` with a `header` or `repeat`".to_string());
402 108 : }
403 108 : if poll["repeat"] == true && function.effect() != ClientFunctionEffect::Read {
404 2 : return invalid("only a function that reads can repeat its request while it's pending".to_string());
405 106 : }
406 106 : Ok(())
407 1017 : }
408 :
409 : /// Read and check a spec
410 176 : pub fn parse(json: &str) -> Result<Self> {
411 175 : let mut spec: ClientFunctionSpec =
412 176 : serde_json::from_str(json).map_err(|e| Error::invalid_argument(format!("invalid client function spec: {e}")))?;
413 175 : if spec.format != CLIENT_FUNCTION_FORMAT {
414 1 : return Err(Error::invalid_argument(format!("unsupported client function spec format {}", spec.format)));
415 174 : }
416 174 : if !is_plain_lowercase(&spec.namespace) {
417 1 : return Err(Error::invalid_argument(format!("invalid namespace {:?}", spec.namespace)));
418 173 : }
419 1130 : for (name, schema) in &spec.schemas {
420 1096 : check_attributes(name, &schema.attributes, &spec.schemas)?;
421 1096 : check_union(name, schema, &spec.schemas)?;
422 : }
423 1034 : for (i, function) in spec.functions.iter().enumerate() {
424 1034 : if !is_plain_lowercase(&function.name) {
425 1 : return Err(Error::invalid_argument(format!("invalid function name {:?}", function.name)));
426 1033 : }
427 1033 : if spec.functions_by_name.insert(function.name.clone(), i).is_some() {
428 0 : return Err(Error::invalid_argument(format!("duplicate function {}", function.name)));
429 1033 : }
430 1033 : check_attributes(&function.name, &function.options, &spec.schemas)?;
431 : // The standard options and columns start with an underscore
432 2485 : if let Some(option) = function.options.iter().find(|o| o.name.starts_with('_')) {
433 1 : return Err(Error::invalid_argument(format!("{}: the name {} is reserved", function.name, option.name)));
434 1030 : }
435 1031 : if let Some(column) = function.columns.iter().find(|c| c.name.starts_with('_')) {
436 1 : return Err(Error::invalid_argument(format!("{}: the column {} is reserved", function.name, column.name)));
437 1029 : }
438 1029 : check_paging(function)?;
439 1021 : check_path(function)?;
440 1017 : spec.check_pending(function)?;
441 : }
442 144 : Ok(spec)
443 176 : }
444 :
445 : /// A function by its lower-case name
446 305 : pub fn function(&self, name: &str) -> Option<&ClientFunction> {
447 305 : self.functions_by_name.get(name).map(|&i| &self.functions[i])
448 305 : }
449 :
450 : /// A schema by name
451 185 : pub fn schema(&self, name: &str) -> Option<&ClientFunctionSchema> {
452 185 : self.schemas.get(name)
453 185 : }
454 :
455 : /// The variants of a schema, a schema that is no union is its own only variant
456 131 : fn variants_of<'a>(&'a self, schema: &'a ClientFunctionSchema) -> Vec<&'a ClientFunctionSchema> {
457 131 : if schema.variants.is_empty() {
458 76 : return vec![schema];
459 55 : }
460 110 : schema.variants.iter().filter_map(|name| self.schema(name)).collect()
461 131 : }
462 :
463 : /// The attribute that a name selects in a schema. A name means one thing in a union, so any variant can answer.
464 76 : pub fn attribute_of<'a>(&'a self, schema: &'a ClientFunctionSchema, name: &str, quoted: bool) -> Option<&'a ClientFunctionAttribute> {
465 90 : self.variants_of(schema).into_iter().find_map(|variant| find_attribute(&variant.attributes, name, quoted))
466 76 : }
467 :
468 : /// What an object with these fields can be. A tag selects its variant. Then every field in script order keeps the
469 : /// variants that take it, and one that would leave none is a conflict. A name that no variant takes says nothing.
470 55 : pub fn fit<'a>(&'a self, schema: &'a ClientFunctionSchema, fields: &[ClientFunctionField]) -> ClientFunctionFit<'a> {
471 55 : let variants = self.variants_of(schema);
472 55 : let mut fit = ClientFunctionFit { variants: variants.clone(), tag_field: None, conflicts: Vec::new() };
473 55 : if let Some(tag) = &schema.tag {
474 12 : let is_tag = |f: &ClientFunctionField| if f.quoted { f.name == tag.attribute } else { f.name.eq_ignore_ascii_case(&tag.attribute) };
475 12 : if let Some(i) = fields.iter().position(is_tag) {
476 10 : let selected = fields[i].text.as_ref().and_then(|text| tag.values.iter().find(|(value, _)| value.eq_ignore_ascii_case(text)));
477 10 : if let Some(variant) = selected.and_then(|(_, name)| self.schema(name)) {
478 6 : fit.variants = vec![variant];
479 6 : fit.tag_field = Some(i);
480 6 : }
481 2 : }
482 43 : }
483 75 : for (i, field) in fields.iter().enumerate() {
484 77 : if !variants.iter().any(|v| v.takes(&field.name, field.quoted)) {
485 9 : continue;
486 54 : }
487 69 : let fitting: Vec<_> = fit.variants.iter().copied().filter(|v| v.takes(&field.name, field.quoted)).collect();
488 54 : if fitting.is_empty() {
489 : // A quoted name means what it says
490 6 : if !field.quoted {
491 5 : fit.conflicts.push(i);
492 5 : }
493 48 : } else {
494 48 : fit.variants = fitting;
495 48 : }
496 : }
497 55 : fit
498 55 : }
499 :
500 : /// Can a function answer before it finishes?
501 216 : pub fn has_pending(&self, function: &ClientFunction) -> bool {
502 216 : let is_set = |value: &serde_json::Value| !value.is_null();
503 216 : match &function.pending {
504 7 : Some(pending) => is_set(pending),
505 209 : None => self.pending.as_ref().is_some_and(is_set),
506 : }
507 216 : }
508 :
509 : /// The result columns of a function in the order of the runtime: the columns of the function, then the standard
510 : /// columns. A raw function has no columns of its own, its response is in `_raw`.
511 4 : pub fn result_columns<'a>(&self, function: &'a ClientFunction) -> Vec<&'a str> {
512 5 : let mut columns: Vec<&'a str> = if function.raw { Vec::new() } else { function.columns.iter().map(|c| c.name.as_str()).collect() };
513 4 : columns.extend([COLUMN_STATUS, COLUMN_NEXT]);
514 4 : if self.has_pending(function) {
515 1 : columns.push(COLUMN_POLL);
516 3 : }
517 4 : columns.extend([COLUMN_META, COLUMN_RAW]);
518 4 : columns
519 4 : }
520 : }
521 :
522 : /// The attribute that a name in a script selects. An unquoted name folds its case, a quoted one is exact.
523 909 : pub fn find_attribute<'a>(attributes: &'a [ClientFunctionAttribute], name: &str, quoted: bool) -> Option<&'a ClientFunctionAttribute> {
524 909 : if quoted {
525 41 : attributes.iter().find(|a| a.name == name)
526 : } else {
527 1601 : attributes.iter().find(|a| a.name.eq_ignore_ascii_case(name))
528 : }
529 909 : }
530 :
531 : /// A name without case and underscores: `batchSize` and `batch_size` are the same mistake
532 171 : fn normalized(name: &str) -> Vec<u8> {
533 1494 : name.bytes().filter(|&c| c != b'_').map(|c| c.to_ascii_lowercase()).collect()
534 171 : }
535 :
536 : /// The edit distance of two byte strings
537 107 : fn edit_distance(l: &[u8], r: &[u8]) -> usize {
538 107 : let mut row: Vec<usize> = (0..=r.len()).collect();
539 1211 : for (i, lc) in l.iter().enumerate() {
540 1211 : let mut diagonal = row[0];
541 1211 : row[0] = i + 1;
542 12184 : for (j, rc) in r.iter().enumerate() {
543 12184 : let cost = diagonal + usize::from(lc != rc);
544 12184 : diagonal = row[j + 1];
545 12184 : row[j + 1] = cost.min(row[j] + 1).min(row[j + 1] + 1);
546 12184 : }
547 : }
548 107 : row[r.len()]
549 107 : }
550 :
551 : /// The candidate closest to a misspelled name, if one is close enough
552 64 : pub fn closest_name<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<&'a str> {
553 64 : let name = normalized(name);
554 64 : let limit = (name.len() / 3).max(2);
555 64 : candidates
556 107 : .map(|candidate| (edit_distance(&name, &normalized(candidate)), candidate))
557 107 : .filter(|(distance, _)| *distance <= limit)
558 64 : .min_by_key(|(distance, candidate)| (*distance, *candidate))
559 64 : .map(|(_, candidate)| candidate)
560 64 : }
561 :
562 : #[cfg(test)]
563 : mod tests {
564 : use super::*;
565 :
566 33 : fn function(name: &str, extra: &str) -> String {
567 33 : format!(r#"{{"format": 1, "namespace": "t", "functions": [{{"name": "{name}"{extra}}}]}}"#)
568 33 : }
569 :
570 : #[test]
571 1 : fn rejects_invalid_specs() {
572 1 : assert!(ClientFunctionSpec::parse("{").is_err());
573 1 : assert!(ClientFunctionSpec::parse(r#"{"format": 2, "namespace": "t"}"#).is_err());
574 1 : assert!(ClientFunctionSpec::parse(r#"{"format": 1, "namespace": "T"}"#).is_err());
575 1 : assert!(ClientFunctionSpec::parse(&function("F", "")).is_err());
576 1 : assert!(ClientFunctionSpec::parse(&function("f", r#", "options": [{"name": "_next"}]"#)).is_err());
577 1 : assert!(ClientFunctionSpec::parse(&function("f", r#", "columns": [{"name": "_raw"}]"#)).is_err());
578 1 : assert!(ClientFunctionSpec::parse(&function("f", r#", "options": [{"name": "a", "schema": "Nope"}]"#)).is_err());
579 1 : assert!(ClientFunctionSpec::parse(&function("f", r#", "options": [{"name": "a"}, {"name": "A"}]"#)).is_err());
580 1 : }
581 :
582 : #[test]
583 1 : fn rejects_paging_that_could_not_work() {
584 1 : let option = r#", "options": [{"name": "o"}]"#;
585 12 : let valid = |extra: &str| ClientFunctionSpec::parse(&function("f", extra)).is_ok();
586 1 : assert!(!valid(r#", "paging": {"kind": "next"}"#));
587 1 : assert!(valid(r#", "paging": {"kind": "url", "from": {"body": "/n"}}"#));
588 1 : assert!(!valid(r#", "paging": {"kind": "url"}"#));
589 1 : assert!(valid(&format!(r#"{option}, "paging": {{"kind": "token", "from": {{"body": "/n"}}, "option": "o"}}"#)));
590 1 : assert!(!valid(&format!(r#"{option}, "paging": {{"kind": "token", "from": {{"body": "/n"}}}}"#)));
591 : // An offset moves by the rows of a page, so it needs them
592 1 : assert!(valid(&format!(r#"{option}, "rows": "/items", "paging": {{"kind": "offset", "option": "o"}}"#)));
593 1 : assert!(!valid(&format!(r#"{option}, "paging": {{"kind": "offset", "option": "o"}}"#)));
594 : // A page number only needs them if a page without rows ends the list
595 1 : assert!(!valid(&format!(r#"{option}, "paging": {{"kind": "page", "option": "o"}}"#)));
596 1 : assert!(!valid(&format!(r#"{option}, "paging": {{"kind": "page", "option": "o", "end": "short_page"}}"#)));
597 1 : assert!(valid(&format!(r#"{option}, "paging": {{"kind": "page", "option": "o", "end": {{"total": "/t"}}}}"#)));
598 : // The options it names exist
599 1 : assert!(!valid(r#", "rows": "/items", "paging": {"kind": "offset", "option": "o"}"#));
600 1 : assert!(!valid(&format!(r#"{option}, "rows": "/items", "paging": {{"kind": "offset", "option": "o", "size": "s"}}"#)));
601 1 : }
602 :
603 : #[test]
604 1 : fn rejects_a_path_that_its_options_do_not_fill() {
605 5 : let valid = |extra: &str| ClientFunctionSpec::parse(&function("f", extra)).is_ok();
606 1 : let path = r#", "path": "/a/{id}/b""#;
607 1 : assert!(valid(&format!(r#"{path}, "options": [{{"name": "x", "api_name": "id", "in": "path", "required": true}}]"#)));
608 1 : assert!(!valid(path));
609 1 : assert!(!valid(&format!(r#"{path}, "options": [{{"name": "id", "in": "path"}}]"#)));
610 1 : assert!(!valid(&format!(r#"{path}, "options": [{{"name": "id", "in": "query", "required": true}}]"#)));
611 1 : assert!(!valid(r#", "path": "/a", "options": [{"name": "id", "in": "path", "required": true}]"#));
612 1 : }
613 :
614 : #[test]
615 1 : fn only_repeats_a_pending_request_that_reads() {
616 5 : let valid = |extra: &str| ClientFunctionSpec::parse(&function("f", extra)).is_ok();
617 1 : let repeat = r#""pending": {"status": 202, "poll": {"repeat": true}}"#;
618 1 : assert!(valid(&format!(r#", "method": "GET", {repeat}"#)));
619 1 : assert!(!valid(&format!(r#", "method": "POST", {repeat}"#)));
620 1 : assert!(valid(r#", "method": "POST", "pending": {"status": 202, "poll": {"header": "Location"}}"#));
621 1 : assert!(!valid(r#", "method": "GET", "pending": {"status": 202}"#));
622 1 : assert!(!valid(r#", "method": "GET", "pending": {"poll": {"header": "Location"}}"#));
623 : // The default of the spec holds for every function
624 1 : let default = format!(r#"{{"format": 1, "namespace": "t", {repeat}, "functions": [{{"name": "f", "method": "POST"}}]}}"#);
625 1 : assert!(ClientFunctionSpec::parse(&default).is_err());
626 1 : let opted_out = format!(r#"{{"format": 1, "namespace": "t", {repeat}, "functions": [{{"name": "f", "method": "POST", "pending": null}}]}}"#);
627 1 : assert!(ClientFunctionSpec::parse(&opted_out).is_ok());
628 1 : }
629 :
630 : const UNION: &str = r#"{"format": 1, "namespace": "t", "schemas": {
631 : "A": {"attributes": [{"name": "kind"}, {"name": "a"}, {"name": "both"}]},
632 : "B": {"attributes": [{"name": "kind"}, {"name": "b"}, {"name": "both"}]},
633 : "C": {"attributes": [{"name": "c"}]},
634 : "Tagged": {"variants": ["A", "B"], "tag": {"attribute": "kind", "values": {"IsA": "A", "IsB": "B"}}},
635 : "Shape": {"variants": ["A", "C"]}
636 : }}"#;
637 :
638 10 : fn union(schemas: &str) -> Result<ClientFunctionSpec> {
639 10 : ClientFunctionSpec::parse(&format!(r#"{{"format": 1, "namespace": "t", "schemas": {schemas}}}"#))
640 10 : }
641 :
642 : #[test]
643 1 : fn rejects_invalid_unions() {
644 1 : let a = r#""A": {"attributes": [{"name": "x", "api_name": "X"}]}"#;
645 1 : assert!(union(&format!(r#"{{{a}, "U": {{"variants": ["A"]}}}}"#)).is_ok());
646 1 : assert!(union(&format!(r#"{{{a}, "U": {{"variants": ["Nope"]}}}}"#)).is_err());
647 1 : assert!(union(&format!(r#"{{{a}, "U": {{"variants": ["A"], "attributes": [{{"name": "y"}}]}}}}"#)).is_err());
648 1 : assert!(union(&format!(r#"{{{a}, "U": {{"variants": ["A"]}}, "V": {{"variants": ["U"]}}}}"#)).is_err());
649 1 : assert!(union(&format!(r#"{{{a}, "U": {{"tag": {{"attribute": "x", "values": {{}}}}}}}}"#)).is_err());
650 : // A tag selects a variant of the union that has the tag
651 1 : assert!(union(&format!(r#"{{{a}, "U": {{"variants": ["A"], "tag": {{"attribute": "x", "values": {{"v": "A"}}}}}}}}"#)).is_ok());
652 1 : assert!(union(&format!(r#"{{{a}, "Z": {{}}, "U": {{"variants": ["A"], "tag": {{"attribute": "x", "values": {{"v": "Z"}}}}}}}}"#)).is_err());
653 1 : assert!(union(&format!(r#"{{{a}, "U": {{"variants": ["A"], "tag": {{"attribute": "y", "values": {{"v": "A"}}}}}}}}"#)).is_err());
654 : // A name means one thing in a union
655 1 : let api_name = r#""B": {"attributes": [{"name": "x", "api_name": "Other"}]}"#;
656 1 : assert!(union(&format!(r#"{{{a}, {api_name}, "U": {{"variants": ["A", "B"]}}}}"#)).is_err());
657 1 : let schema = r#""B": {"attributes": [{"name": "x", "api_name": "X", "schema": "A"}]}"#;
658 1 : assert!(union(&format!(r#"{{{a}, {schema}, "U": {{"variants": ["A", "B"]}}}}"#)).is_err());
659 1 : }
660 :
661 14 : fn field(name: &str, text: Option<&str>) -> ClientFunctionField {
662 14 : ClientFunctionField { name: name.to_string(), quoted: false, text: text.map(str::to_string) }
663 14 : }
664 :
665 : #[test]
666 1 : fn fits_variants() {
667 1 : let spec = ClientFunctionSpec::parse(UNION).unwrap();
668 10 : let fit = |schema: &str, fields: &[ClientFunctionField]| {
669 10 : let schema = &spec.schemas[schema];
670 10 : let fit = spec.fit(schema, fields);
671 13 : let names: Vec<&str> = fit.variants.iter().map(|v| v.attributes[1.min(v.attributes.len() - 1)].name.as_str()).collect();
672 10 : (names.join(","), fit.tag_field, fit.conflicts)
673 10 : };
674 : // Nothing yet: any variant
675 1 : assert_eq!(fit("Tagged", &[]), ("a,b".to_string(), None, vec![]));
676 : // The value selects the variant, whatever its case
677 1 : assert_eq!(fit("Tagged", &[field("kind", Some("isb"))]), ("b".to_string(), Some(0), vec![]));
678 : // A wrong attribute for the tag, in any order
679 1 : assert_eq!(fit("Tagged", &[field("a", None), field("KIND", Some("IsB"))]), ("b".to_string(), Some(1), vec![0]));
680 : // A value that isn't listed or isn't known yet selects nothing, the attributes decide
681 1 : assert_eq!(fit("Tagged", &[field("kind", Some("Other")), field("a", None)]), ("a".to_string(), None, vec![]));
682 1 : assert_eq!(fit("Tagged", &[field("kind", None), field("b", None)]), ("b".to_string(), None, vec![]));
683 : // Without a tag the attributes decide, the first one that leaves no variant is the conflict
684 1 : assert_eq!(fit("Shape", &[field("a", None), field("c", None)]), ("a".to_string(), None, vec![1]));
685 1 : assert_eq!(fit("Shape", &[field("c", None), field("a", None)]), ("c".to_string(), None, vec![1]));
686 : // A name that no variant takes says nothing
687 1 : assert_eq!(fit("Shape", &[field("nope", None)]), ("a,c".to_string(), None, vec![]));
688 : // A shared attribute keeps both
689 1 : assert_eq!(fit("Tagged", &[field("both", None)]), ("a,b".to_string(), None, vec![]));
690 : // A quoted name means what it says, it's no conflict
691 1 : let quoted = ClientFunctionField { name: "c".to_string(), quoted: true, text: None };
692 1 : assert_eq!(fit("Shape", &[field("a", None), quoted]), ("a".to_string(), None, vec![]));
693 1 : }
694 :
695 : #[test]
696 1 : fn ignores_what_the_runtime_reads() {
697 1 : let spec = ClientFunctionSpec::parse(&function("f", r#", "path": "/x", "host": "api", "rows": "", "errors": {"empty_on": [404]}"#)).unwrap();
698 1 : assert!(spec.function("f").is_some());
699 1 : }
700 :
701 : #[test]
702 1 : fn effects() {
703 5 : let effect = |extra: &str| {
704 5 : let spec = ClientFunctionSpec::parse(&function("f", extra)).unwrap();
705 5 : spec.function("f").unwrap().effect()
706 5 : };
707 1 : assert_eq!(effect(r#", "method": "GET""#), ClientFunctionEffect::Read);
708 1 : assert_eq!(effect(r#", "method": "POST""#), ClientFunctionEffect::Write);
709 1 : assert_eq!(effect(r#", "method": "POST", "effect": "read""#), ClientFunctionEffect::Read);
710 1 : assert_eq!(effect(""), ClientFunctionEffect::Write);
711 1 : assert_eq!(effect(r#", "method": "GET", "effect": "read", "raw": true"#), ClientFunctionEffect::Write);
712 1 : }
713 :
714 : #[test]
715 1 : fn pending_follows_the_default_of_the_spec() {
716 1 : let spec = ClientFunctionSpec::parse(
717 1 : r#"{"format": 1, "namespace": "t", "pending": {"status": 202, "poll": {"header": "Location"}}, "functions": [
718 1 : {"name": "a"}, {"name": "b", "pending": null}
719 1 : ]}"#,
720 : )
721 1 : .unwrap();
722 1 : assert!(spec.has_pending(spec.function("a").unwrap()));
723 1 : assert!(!spec.has_pending(spec.function("b").unwrap()));
724 1 : }
725 :
726 : #[test]
727 1 : fn result_columns_end_with_the_standard_columns() {
728 1 : let spec = ClientFunctionSpec::parse(
729 1 : r#"{"format": 1, "namespace": "t", "functions": [
730 1 : {"name": "a", "columns": [{"name": "id"}, {"name": "label"}]},
731 1 : {"name": "b", "columns": [{"name": "id"}], "pending": {"status": 202, "poll": {"header": "Location"}}},
732 1 : {"name": "c", "columns": [{"name": "id"}], "raw": true}
733 1 : ]}"#,
734 : )
735 1 : .unwrap();
736 3 : let columns = |name: &str| spec.result_columns(spec.function(name).unwrap()).join(",");
737 1 : assert_eq!(columns("a"), "id,label,_status,_next,_meta,_raw");
738 1 : assert_eq!(columns("b"), "id,_status,_next,_poll,_meta,_raw");
739 1 : assert_eq!(columns("c"), "_status,_next,_meta,_raw");
740 1 : }
741 :
742 : #[test]
743 1 : fn finds_close_names() {
744 1 : let names = ["batch_size", "offset", "segment_id"];
745 1 : assert_eq!(closest_name("batchsize", names.iter().copied()), Some("batch_size"));
746 1 : assert_eq!(closest_name("batchSize", names.iter().copied()), Some("batch_size"));
747 1 : assert_eq!(closest_name("ofset", names.iter().copied()), Some("offset"));
748 1 : assert_eq!(closest_name("completely_else", names.iter().copied()), None);
749 1 : }
750 : }
|