openauth_core/api/
schema.rs1use 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}