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 !json_type_matches(value, field.schema_type) {
90                return Err(format!(
91                    "field `{}` must be {}",
92                    field.name,
93                    field.schema_type.as_str()
94                ));
95            }
96        }
97        Ok(())
98    }
99
100    pub(super) fn openapi_schema(&self) -> Value {
101        let mut properties = serde_json::Map::new();
102        let mut required = Vec::new();
103        for field in &self.fields {
104            let mut schema = serde_json::Map::new();
105            schema.insert(
106                "type".to_owned(),
107                Value::String(field.schema_type.as_str().to_owned()),
108            );
109            if let Some(format) = &field.format {
110                schema.insert("format".to_owned(), Value::String(format.clone()));
111            }
112            if let Some(description) = &field.description {
113                schema.insert("description".to_owned(), Value::String(description.clone()));
114            }
115            properties.insert(field.name.clone(), Value::Object(schema));
116            if field.required {
117                required.push(Value::String(field.name.clone()));
118            }
119        }
120        json!({
121            "type": "object",
122            "properties": properties,
123            "required": required,
124        })
125    }
126}
127
128fn json_type_matches(value: &Value, schema_type: JsonSchemaType) -> bool {
129    match schema_type {
130        JsonSchemaType::String => value.is_string(),
131        JsonSchemaType::Number => value.is_number(),
132        JsonSchemaType::Boolean => value.is_boolean(),
133        JsonSchemaType::Array => value.is_array(),
134        JsonSchemaType::Object => value.is_object(),
135    }
136}