Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rohas-cli/src/utils/file_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub fn parse_directory(dir: &PathBuf) -> anyhow::Result<Schema> {
Ok(schema) => {
// Merge schemas
combined_schema.models.extend(schema.models);
combined_schema.types.extend(schema.types);
combined_schema.inputs.extend(schema.inputs);
combined_schema.apis.extend(schema.apis);
combined_schema.events.extend(schema.events);
Expand Down
38 changes: 30 additions & 8 deletions crates/rohas-codegen/src/python.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::Result;
use crate::templates;
use rohas_parser::{Api, Event, FieldType, Model, Schema, WebSocket};
use rohas_parser::{Api, Event, FieldType, Model, Schema, Type, WebSocket};
use std::fs;
use std::path::Path;

Expand Down Expand Up @@ -58,14 +58,24 @@ pub fn generate_dtos(schema: &Schema, output_dir: &Path) -> Result<()> {
fs::write(dto_dir.join(file_name), content)?;
}

for type_def in &schema.types {
let content = generate_model_content(&rohas_parser::Model {
name: type_def.name.clone(),
fields: type_def.fields.clone(),
attributes: vec![],
});
let file_name = format!("{}.py", templates::to_snake_case(&type_def.name));
fs::write(dto_dir.join(file_name), content)?;
}

Ok(())
}

pub fn generate_apis(schema: &Schema, output_dir: &Path) -> Result<()> {
let api_dir = output_dir.join("generated/api");

for api in &schema.apis {
let content = generate_api_content(api);
let content = generate_api_content(api, schema);
let file_name = format!("{}.py", templates::to_snake_case(&api.name));
fs::write(api_dir.join(file_name), content)?;
}
Expand Down Expand Up @@ -114,7 +124,7 @@ fn extract_path_params(path: &str) -> Vec<String> {
params
}

fn generate_api_content(api: &Api) -> String {
fn generate_api_content(api: &Api, schema: &Schema) -> String {
let mut content = String::new();

content.push_str("from pydantic import BaseModel\n");
Expand All @@ -125,11 +135,23 @@ fn generate_api_content(api: &Api) -> String {

let is_custom_type = matches!(response_field_type, FieldType::Custom(_));
if is_custom_type {
content.push_str(&format!(
"from ..models.{} import {}\n",
templates::to_snake_case(&api.response),
api.response
));
// Check if it's a type (DTO) or a model
let is_type = schema.types.iter().any(|t| t.name == api.response);
let is_input = schema.inputs.iter().any(|i| i.name == api.response);

if is_type || is_input {
content.push_str(&format!(
"from ..dto.{} import {}\n",
templates::to_snake_case(&api.response),
api.response
));
} else {
content.push_str(&format!(
"from ..models.{} import {}\n",
templates::to_snake_case(&api.response),
api.response
));
}
}

if let Some(body) = &api.body {
Expand Down
31 changes: 27 additions & 4 deletions crates/rohas-codegen/src/rust.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::Result;
use crate::templates;
use rohas_parser::{Api, Event, FieldType, Model, Schema, WebSocket};
use rohas_parser::{Api, Event, FieldType, Model, Schema, Type, WebSocket};
use std::fs;
use std::path::Path;

Expand Down Expand Up @@ -89,13 +89,28 @@ pub fn generate_dtos(schema: &Schema, output_dir: &Path) -> Result<()> {
fs::write(dto_dir.join(file_name), content)?;
}

for type_def in &schema.types {
let content = generate_model_content(&rohas_parser::Model {
name: type_def.name.clone(),
fields: type_def.fields.clone(),
attributes: vec![],
});
let file_name = format!("{}.rs", templates::to_snake_case(&type_def.name));
fs::write(dto_dir.join(file_name), content)?;
}

let mut mod_content = String::new();
mod_content.push_str("// Auto-generated module declarations\n");
for input in &schema.inputs {
let mod_name = templates::to_snake_case(&input.name);
mod_content.push_str(&format!("pub mod {};\n", mod_name));
mod_content.push_str(&format!("pub use {}::{};\n", mod_name, input.name));
}
for type_def in &schema.types {
let mod_name = templates::to_snake_case(&type_def.name);
mod_content.push_str(&format!("pub mod {};\n", mod_name));
mod_content.push_str(&format!("pub use {}::{};\n", mod_name, type_def.name));
}
fs::write(dto_dir.join("mod.rs"), mod_content)?;

Ok(())
Expand All @@ -105,7 +120,7 @@ pub fn generate_apis(schema: &Schema, output_dir: &Path) -> Result<()> {
let api_dir = output_dir.join("generated/api");

for api in &schema.apis {
let content = generate_api_content(api);
let content = generate_api_content(api, schema);
let file_name = format!("{}.rs", templates::to_snake_case(&api.name));
fs::write(api_dir.join(file_name), content)?;
}
Expand Down Expand Up @@ -133,7 +148,7 @@ pub fn generate_apis(schema: &Schema, output_dir: &Path) -> Result<()> {
Ok(())
}

fn generate_api_content(api: &Api) -> String {
fn generate_api_content(api: &Api, schema: &Schema) -> String {
let mut content = String::new();

content.push_str("use serde::{Deserialize, Serialize};\n");
Expand All @@ -151,7 +166,15 @@ fn generate_api_content(api: &Api) -> String {
let is_custom_response = matches!(response_field_type, rohas_parser::FieldType::Custom(_));
if is_custom_response {
let response_type_snake = templates::to_snake_case(&api.response);
content.push_str(&format!("use crate::generated::models::{}::{};\n", response_type_snake, api.response));

let is_type = schema.types.iter().any(|t| t.name == api.response);
let is_input = schema.inputs.iter().any(|i| i.name == api.response);

if is_type || is_input {
content.push_str(&format!("use crate::generated::dto::{}::{};\n", response_type_snake, api.response));
} else {
content.push_str(&format!("use crate::generated::models::{}::{};\n", response_type_snake, api.response));
}
}
content.push_str("\n");

Expand Down
40 changes: 31 additions & 9 deletions crates/rohas-codegen/src/typescript.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::Result;
use crate::templates;
use rohas_parser::{Api, Event, FieldType, Model, Schema, WebSocket};
use rohas_parser::{Api, Event, FieldType, Model, Schema, Type, WebSocket};
use std::fs;
use std::path::Path;

Expand Down Expand Up @@ -91,14 +91,24 @@ pub fn generate_dtos(schema: &Schema, output_dir: &Path) -> Result<()> {
fs::write(dto_dir.join(file_name), content)?;
}

for type_def in &schema.types {
let content = generate_model_content(&rohas_parser::Model {
name: type_def.name.clone(),
fields: type_def.fields.clone(),
attributes: vec![],
});
let file_name = format!("{}.ts", templates::to_snake_case(&type_def.name));
fs::write(dto_dir.join(file_name), content)?;
}

Ok(())
}

pub fn generate_apis(schema: &Schema, output_dir: &Path) -> Result<()> {
let api_dir = output_dir.join("generated/api");

for api in &schema.apis {
let content = generate_api_content(api);
let content = generate_api_content(api, schema);
let file_name = format!("{}.ts", templates::to_snake_case(&api.name));
fs::write(api_dir.join(file_name), content)?;
}
Expand All @@ -117,7 +127,7 @@ pub fn generate_apis(schema: &Schema, output_dir: &Path) -> Result<()> {
Ok(())
}

fn generate_api_content(api: &Api) -> String {
fn generate_api_content(api: &Api, schema: &Schema) -> String {
let mut content = String::new();

content.push_str("import { z } from 'zod';\n");
Expand All @@ -129,12 +139,24 @@ fn generate_api_content(api: &Api) -> String {
let response_is_primitive = is_primitive_type(&api.response);

if !response_is_primitive {
content.push_str(&format!(
"import {{ {}, {}Schema }} from '@generated/models/{}';\n",
api.response,
api.response,
templates::to_snake_case(&api.response)
));
let is_type = schema.types.iter().any(|t| t.name == api.response);
let is_input = schema.inputs.iter().any(|i| i.name == api.response);

if is_type || is_input {
content.push_str(&format!(
"import {{ {}, {}Schema }} from '@generated/dto/{}';\n",
api.response,
api.response,
templates::to_snake_case(&api.response)
));
} else {
content.push_str(&format!(
"import {{ {}, {}Schema }} from '@generated/models/{}';\n",
api.response,
api.response,
templates::to_snake_case(&api.response)
));
}
}

if let Some(body) = &api.body {
Expand Down
1 change: 1 addition & 0 deletions crates/rohas-dev-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,7 @@ fn parse_directory(dir: &PathBuf) -> anyhow::Result<Schema> {
match Parser::parse_file(path) {
Ok(schema) => {
combined_schema.models.extend(schema.models);
combined_schema.types.extend(schema.types);
combined_schema.inputs.extend(schema.inputs);
combined_schema.apis.extend(schema.apis);
combined_schema.events.extend(schema.events);
Expand Down
17 changes: 17 additions & 0 deletions crates/rohas-parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Schema {
pub models: Vec<Model>,
pub types: Vec<Type>,
pub apis: Vec<Api>,
pub events: Vec<Event>,
pub crons: Vec<Cron>,
Expand All @@ -14,6 +15,7 @@ impl Schema {
pub fn new() -> Self {
Self {
models: Vec::new(),
types: Vec::new(),
apis: Vec::new(),
events: Vec::new(),
crons: Vec::new(),
Expand All @@ -34,6 +36,15 @@ impl Schema {
}
}

for type_def in &self.types {
if !names.insert(&type_def.name) {
return Err(crate::ParseError::DuplicateDefinition(format!(
"Type '{}'",
type_def.name
)));
}
}

for api in &self.apis {
if !names.insert(&api.name) {
return Err(crate::ParseError::DuplicateDefinition(format!(
Expand Down Expand Up @@ -219,6 +230,12 @@ pub struct Cron {
pub triggers: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Type {
pub name: String,
pub fields: Vec<Field>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Input {
pub name: String,
Expand Down
44 changes: 44 additions & 0 deletions crates/rohas-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ impl Parser {
let model = Self::parse_model(inner_pair)?;
schema.models.push(model);
}
Rule::type_def => {
let type_def = Self::parse_type(inner_pair)?;
schema.types.push(type_def);
}
Rule::api => {
let api = Self::parse_api(inner_pair)?;
schema.apis.push(api);
Expand Down Expand Up @@ -307,6 +311,46 @@ impl Parser {
})
}

fn parse_type(pair: pest::iterators::Pair<Rule>) -> Result<Type> {
let mut inner = pair.into_inner();
let name = inner
.next()
.ok_or_else(|| ParseError::InvalidModel("Missing type name".into()))?
.as_str()
.to_string();

let mut fields = Vec::new();

for field_pair in inner {
if field_pair.as_rule() == Rule::input_field {
let mut field_inner = field_pair.into_inner();

let field_name = field_inner
.next()
.ok_or_else(|| ParseError::InvalidModel("Missing field name".into()))?
.as_str()
.to_string();

let field_type_pair = field_inner
.next()
.ok_or_else(|| ParseError::InvalidModel("Missing field type".into()))?;

let field_type = Self::parse_field_type(field_type_pair)?;

let optional = field_inner.next().is_some();

fields.push(Field {
name: field_name,
field_type,
optional,
attributes: Vec::new(),
});
}
}

Ok(Type { name, fields })
}

fn parse_input(pair: pest::iterators::Pair<Rule>) -> Result<Input> {
let mut inner = pair.into_inner();
let name = inner
Expand Down
5 changes: 4 additions & 1 deletion crates/rohas-parser/src/rohas.pest
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
COMMENT = _{ "//" ~ (!"\n" ~ ANY)* ~ "\n" | "/*" ~ (!"*/" ~ ANY)* ~ "*/" }

// Top-level schema
schema = { SOI ~ (model | api | event | cron | input | ws)* ~ EOI }
schema = { SOI ~ (model | type_def | api | event | cron | input | ws)* ~ EOI }

// Identifiers and literals
ident = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
Expand Down Expand Up @@ -59,6 +59,9 @@ cron_property = {
| ("triggers:" ~ trigger_list)
}

// Type definition (DTO for responses)
type_def = { "type" ~ ident ~ "{" ~ input_field* ~ "}" }

// Input definition (DTO)
input = { "input" ~ ident ~ "{" ~ input_field* ~ "}" }
input_field = { ident ~ ":" ~ field_type ~ optional? }
Expand Down
6 changes: 3 additions & 3 deletions examples/hello-world/schema/api/health.ro
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
model HealthResponse {
status String
timestamp String
type HealthResponse {
status: String
timestamp: String
}

api Health {
Expand Down
24 changes: 24 additions & 0 deletions examples/hello-world/src/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# EditorConfig is awesome: https://EditorConfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{ts,tsx,js,jsx,json}]
indent_style = space
indent_size = 2

[*.{py}]
indent_style = space
indent_size = 4

[*.{yml,yaml}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false
Loading