Skip to content

Commit

Permalink
Remove janky schema from http API
Browse files Browse the repository at this point in the history
  • Loading branch information
coolreader18 committed Jan 27, 2025
1 parent afdc0d6 commit 2cde444
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 477 deletions.
236 changes: 59 additions & 177 deletions crates/cli/src/subcommands/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ use crate::util;
use crate::util::{add_auth_header_opt, database_identity, get_auth_header};
use anyhow::{bail, Context, Error};
use clap::{Arg, ArgMatches};
use itertools::Either;
use serde_json::Value;
use convert_case::{Case, Casing};
use spacetimedb::Identity;
use spacetimedb_lib::de::serde::deserialize_from;
use spacetimedb_lib::sats::{AlgebraicType, AlgebraicTypeRef, Typespace};
use spacetimedb_lib::sats::{self, AlgebraicType, Typespace};
use spacetimedb_lib::ProductTypeElement;
use spacetimedb_schema::def::{ModuleDef, ReducerDef};
use std::fmt::Write;
use std::iter;

pub fn cli() -> clap::Command {
clap::Command::new("call")
Expand Down Expand Up @@ -43,27 +41,25 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), Error> {

let database_identity = database_identity(&config, database, server).await?;

let builder = reqwest::Client::new().post(format!(
let client = reqwest::Client::new();
let builder = client.post(format!(
"{}/database/call/{}/{}",
config.get_host_url(server)?,
database_identity.clone(),
reducer_name
));
let auth_header = get_auth_header(&config, anon_identity)?;
let builder = add_auth_header_opt(builder, &auth_header);
let describe_reducer = util::describe_reducer(
&mut config,
database_identity,
server.map(|x| x.to_string()),
reducer_name.clone(),
anon_identity,
)
.await?;
let module_def = util::fetch_module_schema(&client, &mut config, database_identity, server, anon_identity).await?;

let reducer_def = module_def
.reducer(&**reducer_name)
.ok_or_else(|| anyhow::Error::msg(no_such_reducer(&database_identity, database, reducer_name, &module_def)))?;

// String quote any arguments that should be quoted
let arguments = arguments
.unwrap_or_default()
.zip(describe_reducer.schema.elements.iter())
.zip(&*reducer_def.params.elements)
.map(|(argument, element)| match &element.algebraic_type {
AlgebraicType::String if !argument.starts_with('\"') || !argument.ends_with('\"') => {
format!("\"{}\"", argument)
Expand All @@ -84,18 +80,9 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), Error> {
let error = Err(e).context(format!("Response text: {}", response_text));

let error_msg = if response_text.starts_with("no such reducer") {
no_such_reducer(config, &database_identity, database, &auth_header, reducer_name, server).await
no_such_reducer(&database_identity, database, reducer_name, &module_def)
} else if response_text.starts_with("invalid arguments") {
invalid_arguments(
config,
&database_identity,
database,
&auth_header,
reducer_name,
&response_text,
server,
)
.await
invalid_arguments(&database_identity, database, &response_text, &module_def, &reducer_def)
} else {
return error;
};
Expand All @@ -107,18 +94,16 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), Error> {
}

/// Returns an error message for when `reducer` is called with wrong arguments.
async fn invalid_arguments(
config: Config,
fn invalid_arguments(
identity: &Identity,
db: &str,
auth_header: &Option<String>,
reducer: &str,
text: &str,
server: Option<&str>,
module_def: &ModuleDef,
reducer_def: &ReducerDef,
) -> String {
let mut error = format!(
"Invalid arguments provided for reducer `{}` for database `{}` resolving to identity `{}`.",
reducer, db, identity
reducer_def.name, db, identity
);

if let Some((actual, expected)) = find_actual_expected(text).filter(|(a, e)| a != e) {
Expand All @@ -130,12 +115,12 @@ async fn invalid_arguments(
.unwrap();
}

if let Some(sig) = schema_json(config, identity, auth_header, true, server)
.await
.and_then(|schema| reducer_signature(schema, reducer))
{
write!(error, "\n\nThe reducer has the following signature:\n\t{}", sig).unwrap();
}
write!(
error,
"\n\nThe reducer has the following signature:\n\t{}",
ReducerSignature(module_def.typespace().with_type(reducer_def))
)
.unwrap();

error
}
Expand All @@ -161,51 +146,39 @@ fn split_at_first_substring<'t>(text: &'t str, substring: &str) -> Option<(&'t s

/// Provided the `schema_json` for the database,
/// returns the signature for a reducer with `reducer_name`.
fn reducer_signature(schema_json: Value, reducer_name: &str) -> Option<String> {
let typespace = typespace(&schema_json)?;

// Fetch the matching reducer.
let elements = find_of_type_in_schema(&schema_json, "reducer")
.find(|(name, _)| *name == reducer_name)?
.1
.get("schema")?
.get("elements")?;
let params = deserialize_from::<Vec<ProductTypeElement>, _>(elements).ok()?;

// Print the arguments to `args`.
let mut args = String::new();
fn ctx(typespace: &Typespace, r: AlgebraicTypeRef) -> String {
let ty = &typespace[r];
let mut ty_str = String::new();
write_type::write_type(&|r| ctx(typespace, r), &mut ty_str, ty).unwrap();
ty_str
}
write_type::write_arglist_no_delimiters(&|r| ctx(&typespace, r), &mut args, &params, None).unwrap();
let args = args.trim().trim_end_matches(',').replace('\n', " ");
struct ReducerSignature<'a>(sats::WithTypespace<'a, ReducerDef>);
impl std::fmt::Display for ReducerSignature<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let reducer_def = self.0.ty();
let typespace = self.0.typespace();

write!(f, "{}(", reducer_def.name)?;

// Print the arguments to `args`.
let mut comma = false;
for arg in &*reducer_def.params.elements {
if comma {
write!(f, ", ")?;
}
comma = true;
if let Some(name) = arg.name() {
write!(f, "{}: ", name.to_case(Case::Snake))?;
}
write_type::write_type(typespace, f, &arg.algebraic_type)?;
}

// Print the full signature to `reducer_fmt`.
let mut reducer_fmt = String::new();
write!(&mut reducer_fmt, "{}({})", reducer_name, args).unwrap();
Some(reducer_fmt)
write!(f, ")")
}
}

/// Returns an error message for when `reducer` does not exist in `db`.
async fn no_such_reducer(
config: Config,
database_identity: &Identity,
db: &str,
auth_header: &Option<String>,
reducer: &str,
server: Option<&str>,
) -> String {
fn no_such_reducer(database_identity: &Identity, db: &str, reducer: &str, module_def: &ModuleDef) -> String {
let mut error = format!(
"No such reducer `{}` for database `{}` resolving to identity `{}`.",
reducer, db, database_identity
);

if let Some(schema) = schema_json(config, database_identity, auth_header, false, server).await {
add_reducer_ctx_to_err(&mut error, schema, reducer);
}
add_reducer_ctx_to_err(&mut error, module_def, reducer);

error
}
Expand All @@ -214,13 +187,13 @@ const REDUCER_PRINT_LIMIT: usize = 10;

/// Provided the schema for the database,
/// decorate `error` with more helpful info about reducers.
fn add_reducer_ctx_to_err(error: &mut String, schema_json: Value, reducer_name: &str) {
let mut reducers = find_of_type_in_schema(&schema_json, "reducer")
.map(|kv| kv.0)
fn add_reducer_ctx_to_err(error: &mut String, module_def: &ModuleDef, reducer_name: &str) {
let mut reducers = module_def
.reducers()
.filter(|reducer| reducer.lifecycle.is_none())
.map(|reducer| &*reducer.name)
.collect::<Vec<_>>();

// TODO(noa): exclude lifecycle reducers

if let Some(best) = find_best_match_for_name(&reducers, reducer_name, None) {
write!(error, "\n\nA reducer with a similar name exists: `{}`", best).unwrap();
} else if reducers.is_empty() {
Expand Down Expand Up @@ -248,98 +221,32 @@ fn add_reducer_ctx_to_err(error: &mut String, schema_json: Value, reducer_name:
}
}

/// Fetch the schema as JSON for the database at `identity`.
///
/// The value of `expand` determines how detailed information to fetch.
async fn schema_json(
config: Config,
identity: &Identity,
auth_header: &Option<String>,
expand: bool,
server: Option<&str>,
) -> Option<Value> {
let builder = reqwest::Client::new().get(format!(
"{}/database/schema/{}",
config.get_host_url(server).ok()?,
identity
));
let builder = add_auth_header_opt(builder, auth_header);

builder
.query(&[("expand", expand)])
.send()
.await
.ok()?
.json::<serde_json::Value>()
.await
.ok()
}

/// Returns all the names of items in `value` that match `type`.
///
/// For example, `type` can be `"reducer"`.
fn find_of_type_in_schema<'v, 't: 'v>(
value: &'v serde_json::Value,
ty: &'t str,
) -> impl Iterator<Item = (&'v str, &'v Value)> {
let Some(entities) = value
.as_object()
.and_then(|o| o.get("entities"))
.and_then(|e| e.as_object())
else {
return Either::Left(iter::empty());
};

let iter = entities
.into_iter()
.filter(move |(_, value)| {
let Some(obj) = value.as_object() else {
return false;
};
obj.get("type").filter(|x| x.as_str() == Some(ty)).is_some()
})
.map(|(key, value)| (key.as_str(), value));
Either::Right(iter)
}

/// Returns the `Typespace` in the provided json schema.
fn typespace(value: &serde_json::Value) -> Option<Typespace> {
let types = value.as_object()?.get("typespace")?;
deserialize_from(types).map(Typespace::new).ok()
}

// this is an old version of code in generate::rust that got
// refactored, but reducer_signature() was using it
// TODO: port reducer_signature() to use AlgebraicTypeUse et al, somehow.
mod write_type {
use super::*;
use convert_case::{Case, Casing};
use spacetimedb_lib::sats::ArrayType;
use sats::ArrayType;
use spacetimedb_lib::ProductType;
use std::fmt;
use std::ops::Deref;

pub fn write_type<W: fmt::Write>(
ctx: &impl Fn(AlgebraicTypeRef) -> String,
out: &mut W,
ty: &AlgebraicType,
) -> fmt::Result {
pub fn write_type<W: fmt::Write>(typespace: &Typespace, out: &mut W, ty: &AlgebraicType) -> fmt::Result {
match ty {
p if p.is_identity() => write!(out, "Identity")?,
p if p.is_address() => write!(out, "Address")?,
p if p.is_schedule_at() => write!(out, "ScheduleAt")?,
AlgebraicType::Sum(sum_type) => {
if let Some(inner_ty) = sum_type.as_option() {
write!(out, "Option<")?;
write_type(ctx, out, inner_ty)?;
write_type(typespace, out, inner_ty)?;
write!(out, ">")?;
} else {
write!(out, "enum ")?;
print_comma_sep_braced(out, &sum_type.variants, |out: &mut W, elem: &_| {
if let Some(name) = &elem.name {
write!(out, "{name}: ")?;
}
write_type(ctx, out, &elem.algebraic_type)
write_type(typespace, out, &elem.algebraic_type)
})?;
}
}
Expand All @@ -348,7 +255,7 @@ mod write_type {
if let Some(name) = &elem.name {
write!(out, "{name}: ")?;
}
write_type(ctx, out, &elem.algebraic_type)
write_type(typespace, out, &elem.algebraic_type)
})?;
}
AlgebraicType::Bool => write!(out, "bool")?,
Expand All @@ -369,11 +276,11 @@ mod write_type {
AlgebraicType::String => write!(out, "String")?,
AlgebraicType::Array(ArrayType { elem_ty }) => {
write!(out, "Vec<")?;
write_type(ctx, out, elem_ty)?;
write_type(typespace, out, elem_ty)?;
write!(out, ">")?;
}
AlgebraicType::Ref(r) => {
write!(out, "{}", ctx(*r))?;
write_type(typespace, out, &typespace[*r])?;
}
}
Ok(())
Expand Down Expand Up @@ -407,29 +314,4 @@ mod write_type {

Ok(())
}

pub fn write_arglist_no_delimiters(
ctx: &impl Fn(AlgebraicTypeRef) -> String,
out: &mut impl Write,
elements: &[ProductTypeElement],

// Written before each line. Useful for `pub`.
prefix: Option<&str>,
) -> fmt::Result {
for elt in elements {
if let Some(prefix) = prefix {
write!(out, "{prefix} ")?;
}

let Some(name) = &elt.name else {
panic!("Product type element has no name: {elt:?}");
};
let name = name.deref().to_case(Case::Snake);

write!(out, "{name}: ")?;
write_type(ctx, out, &elt.algebraic_type)?;
writeln!(out, ",")?;
}
Ok(())
}
}
Loading

0 comments on commit 2cde444

Please sign in to comment.