LCOV - code coverage report
Current view: top level - dashql-core/src/catalog - clfn.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 99.5 % 394 392
Test Date: 2026-11-09 20:22:13

            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              : }
        

Generated by: LCOV version 2.0-1