Skip to main content

openauth_core/api/
schema.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{json, Value};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "lowercase")]
6pub enum JsonSchemaType {
7    String,
8    Number,
9    Boolean,
10    Array,
11    Object,
12}
13
14impl JsonSchemaType {
15    fn as_str(self) -> &'static str {
16        match self {
17            Self::String => "string",
18            Self::Number => "number",
19            Self::Boolean => "boolean",
20            Self::Array => "array",
21            Self::Object => "object",
22        }
23    }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct BodyField {
28    pub name: String,
29    pub schema_type: JsonSchemaType,
30    pub required: bool,
31    pub format: Option<String>,
32    pub description: Option<String>,
33}
34
35impl BodyField {
36    pub fn new(name: impl Into<String>, schema_type: JsonSchemaType) -> Self {
37        Self {
38            name: name.into(),
39            schema_type,
40            required: true,
41            format: None,
42            description: None,
43        }
44    }
45
46    pub fn optional(name: impl Into<String>, schema_type: JsonSchemaType) -> Self {
47        Self {
48            required: false,
49            ..Self::new(name, schema_type)
50        }
51    }
52
53    #[must_use]
54    pub fn format(mut self, format: impl Into<String>) -> Self {
55        self.format = Some(format.into());
56        self
57    }
58
59    #[must_use]
60    pub fn description(mut self, description: impl Into<String>) -> Self {
61        self.description = Some(description.into());
62        self
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct BodySchema {
68    pub fields: Vec<BodyField>,
69}
70
71impl BodySchema {
72    pub fn object(fields: impl IntoIterator<Item = BodyField>) -> Self {
73        Self {
74            fields: fields.into_iter().collect(),
75        }
76    }
77
78    pub(super) fn validate(&self, value: &Value) -> Result<(), String> {
79        let Some(object) = value.as_object() else {
80            return Err("request body must be an object".to_owned());
81        };
82        for field in &self.fields {
83            let Some(value) = object.get(&field.name) else {
84                if field.required {
85                    return Err(format!("missing required field `{}`", field.name));
86                }
87                continue;
88            };
89            if !field.required && value.is_null() {
90                continue;
91            }
92            if !json_type_matches(value, field.schema_type) {
93                return Err(format!(
94                    "field `{}` must be {}",
95                    field.name,
96                    field.schema_type.as_str()
97                ));
98            }
99        }
100        Ok(())
101    }
102
103    pub(super) fn openapi_schema(&self) -> Value {
104        let mut properties = serde_json::Map::new();
105        let mut required = Vec::new();
106        for field in &self.fields {
107            let mut schema = serde_json::Map::new();
108            schema.insert(
109                "type".to_owned(),
110                Value::String(field.schema_type.as_str().to_owned()),
111            );
112            if let Some(format) = &field.format {
113                schema.insert("format".to_owned(), Value::String(format.clone()));
114            }
115            if let Some(description) = &field.description {
116                schema.insert("description".to_owned(), Value::String(description.clone()));
117            }
118            properties.insert(field.name.clone(), Value::Object(schema));
119            if field.required {
120                required.push(Value::String(field.name.clone()));
121            }
122        }
123        json!({
124            "type": "object",
125            "properties": properties,
126            "required": required,
127        })
128    }
129}
130
131fn json_type_matches(value: &Value, schema_type: JsonSchemaType) -> bool {
132    match schema_type {
133        JsonSchemaType::String => value.is_string(),
134        JsonSchemaType::Number => value.is_number(),
135        JsonSchemaType::Boolean => value.is_boolean(),
136        JsonSchemaType::Array => value.is_array(),
137        JsonSchemaType::Object => value.is_object(),
138    }
139}