diff --git a/Cargo.lock b/Cargo.lock index 310f745..4c1a6b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,6 +733,7 @@ version = "0.1.0" dependencies = [ "convert_case", "libninja_mir", + "prettyplease", "proc-macro2", "quote", "regex", @@ -995,9 +996,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", "syn", diff --git a/libninja/src/rust.rs b/libninja/src/rust.rs index ea5f001..dbf4dd4 100644 --- a/libninja/src/rust.rs +++ b/libninja/src/rust.rs @@ -15,7 +15,7 @@ use tracing::debug; use ::mir::{File, Import, Visibility}; use codegen::ToRustType; -use format::format_code; +use mir_rust::format_code; use hir::{AuthStrategy, HirSpec, Location, Oauth2Auth, Parameter, qualified_env_var}; use ln_core::{copy_builtin_files, copy_builtin_templates, create_context, get_template_file, prepare_templates}; use ln_core::fs; @@ -206,12 +206,12 @@ fn write_file_with_template(mut file: File, template: Option Result<()> { #date_as_int #int_as_str }; - let code = format_code(code).unwrap(); + let code = format_code(code); fs::write_file(&src_path, &code) } diff --git a/libninja/src/rust/client.rs b/libninja/src/rust/client.rs index 167cc53..15be9f1 100644 --- a/libninja/src/rust/client.rs +++ b/libninja/src/rust/client.rs @@ -108,14 +108,14 @@ pub fn struct_Client(spec: &HirSpec, opt: &PackageConfig) -> Class let mut instance_fields = vec![ Field { - name: "client".to_string(), + name: Ident::new("client"), ty: quote!(Cow<'static, httpclient::Client>), ..Field::default() } ]; if spec.has_security() { instance_fields.push(Field { - name: "authentication".to_string(), + name: Ident::new("authentication"), ty: quote!(#auth_struct_name), ..Field::default() }); diff --git a/libninja/src/rust/codegen/example.rs b/libninja/src/rust/codegen/example.rs index 320e911..df3318e 100644 --- a/libninja/src/rust/codegen/example.rs +++ b/libninja/src/rust/codegen/example.rs @@ -9,7 +9,7 @@ use mir::{File, Import, Ty}; use crate::PackageConfig; use crate::rust::codegen::{ToRustCode, ToRustType}; use crate::rust::codegen::ToRustIdent; -use crate::rust::format::format_code; +use mir_rust::format_code; pub trait ToRustExample { fn to_rust_example(&self, spec: &HirSpec) -> anyhow::Result; @@ -67,7 +67,7 @@ pub fn generate_example(operation: &Operation, opt: &PackageConfig, spec: &HirSp ..File::default() }; let code = example.to_rust_code(); - format_code(code) + Ok(format_code(code)) } pub fn to_rust_example_value(ty: &Ty, name: &str, spec: &HirSpec, use_ref_value: bool) -> anyhow::Result { diff --git a/libninja/src/rust/format.rs b/libninja/src/rust/format.rs index 9f4d2df..533f529 100644 --- a/libninja/src/rust/format.rs +++ b/libninja/src/rust/format.rs @@ -1,32 +1,12 @@ -use anyhow::Result; use proc_macro2::TokenStream; -use std::fs::File; use std::io::Write; -pub fn format_code(code: TokenStream) -> Result { - let code = code.to_string(); - let syntax_tree = match syn::parse_file(&code) { - Ok(syntax_tree) => syntax_tree, - Err(e) => { - println!("{}", code); - return Err(anyhow::anyhow!( - "Failed to parse generated code: {}", - e - )) - } - }; - let mut code = prettyplease::unparse(&syntax_tree); - if code.ends_with('\n') { - code.pop(); - } - Ok(code) -} - #[cfg(test)] mod tests { use super::*; use proc_macro2::TokenStream; use quote::quote; + use mir_rust::format_code; fn codegen_example() -> TokenStream { quote! { @@ -41,7 +21,7 @@ mod tests { #[test] fn test_codegen() { let code = codegen_example(); - let code = format_code(code).unwrap(); + let code = format_code(code); assert_eq!( code, r#" diff --git a/libninja/src/rust/io.rs b/libninja/src/rust/io.rs index 873c178..bc93e00 100644 --- a/libninja/src/rust/io.rs +++ b/libninja/src/rust/io.rs @@ -1,7 +1,7 @@ use std::path::Path; use proc_macro2::TokenStream; use ln_core::fs; -use crate::rust::format::format_code; +use mir_rust::format_code; use mir_rust::ToRustCode; pub fn write_rust_file_to_path(path: &Path, file: mir::File) -> anyhow::Result<()> { @@ -14,7 +14,7 @@ pub fn write_rust_code_to_path(path: &Path, code: TokenStream) -> anyhow::Result } pub fn write_rust_to_path(path: &Path, code: TokenStream, template: &str) -> anyhow::Result<()> { - let code = format_code(code)?; + let code = format_code(code); let mut f = fs::open(path)?; let mut s = template.to_string(); if !s.is_empty() && !s.ends_with('\n') { diff --git a/libninja/src/rust/lower_hir.rs b/libninja/src/rust/lower_hir.rs index 02c0129..41d1740 100644 --- a/libninja/src/rust/lower_hir.rs +++ b/libninja/src/rust/lower_hir.rs @@ -134,7 +134,7 @@ impl StructExt for Struct { _ => {} } Field { - name: name.clone(), + name: name.to_rust_ident(), ty, vis: Visibility::Public, decorators, @@ -336,7 +336,7 @@ mod tests { use hir::HirField; use mir::Ty; - use crate::rust::format::format_code; + use mir_rust::format_code; use super::*; @@ -352,7 +352,7 @@ mod tests { docs: None, }; let code = create_newtype_struct(&schema, &HirSpec::default()); - let code = format_code(code).unwrap(); + let code = format_code(code); assert_eq!(&code, " #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct NewType(pub String); diff --git a/libninja/src/rust/request.rs b/libninja/src/rust/request.rs index 872bb94..f787f84 100644 --- a/libninja/src/rust/request.rs +++ b/libninja/src/rust/request.rs @@ -177,7 +177,7 @@ pub fn build_struct_fields( tok = quote! { Option<#tok> } } Field { - name: input.name.clone(), + name: input.name.to_rust_ident(), ty: tok, vis: Visibility::Public, ..Field::default() diff --git a/libninja/tests/all_of/main.rs b/libninja/tests/all_of/main.rs index 58cd75a..87dd5aa 100644 --- a/libninja/tests/all_of/main.rs +++ b/libninja/tests/all_of/main.rs @@ -21,7 +21,7 @@ fn record_for_schema(name: &str, schema: &str, spec: &OpenAPI) -> Record { fn formatted_code(record: Record, spec: &HirSpec) -> String { let config = ConfigFlags::default(); let code = libninja::rust::lower_hir::create_struct(&record, &config, spec); - libninja::rust::format::format_code(code).unwrap() + mir_rust::format_code(code) } #[test] diff --git a/mir/src/class.rs b/mir/src/class.rs new file mode 100644 index 0000000..ef8ae9f --- /dev/null +++ b/mir/src/class.rs @@ -0,0 +1,73 @@ +use std::fmt::{Debug, Formatter}; +use crate::{Doc, Function, Ident, Visibility}; + +pub struct Class { + pub name: Ident, + pub doc: Option, + /// `code` is for Python, where we need code like this: + /// class Account(BaseModel): + /// class Config: + /// this_is_a_config_for_pydantic = True + pub code: Option, + pub instance_fields: Vec>, + pub static_fields: Vec>, + pub constructors: Vec>, + /// Use `class_methods` in Rust. + pub class_methods: Vec>, + pub static_methods: Vec>, + pub public: bool, + + pub lifetimes: Vec, + pub decorators: Vec, + pub superclasses: Vec, +} + +#[derive(Debug, Default)] +pub struct Field { + pub name: Ident, + pub ty: T, + pub default: Option, + pub vis: Visibility, + pub doc: Option, + pub optional: bool, + pub decorators: Vec, +} + + +impl Debug for Class { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let public = self.public; + write!(f, "Class {{ name: {name:?}, \ + doc: {doc:?}, \ + instance_fields: todo!, \ + static_fields: todo!, \ + constructors: todo!, \ + class_methods: todo!, \ + static_methods: todo!, \ + public: {public}, \ + lifetimes: todo!, \ + superclasses: todo! }}", + name = self.name, + doc = self.doc, + ) + } +} + +impl Default for Class { + fn default() -> Self { + Self { + name: Ident("".to_string()), + code: None, + doc: None, + instance_fields: vec![], + static_fields: vec![], + constructors: vec![], + class_methods: vec![], + static_methods: vec![], + public: false, + lifetimes: vec![], + decorators: vec![], + superclasses: vec![], + } + } +} diff --git a/mir/src/doc.rs b/mir/src/doc.rs index b0375d9..4de4aff 100644 --- a/mir/src/doc.rs +++ b/mir/src/doc.rs @@ -1,6 +1,12 @@ #[derive(Debug, Clone)] pub struct Doc(pub String); +impl Into for &str { + fn into(self) -> Doc { + Doc(self.to_string()) + } +} + pub enum DocFormat { Markdown, Rst, diff --git a/mir/src/function.rs b/mir/src/function.rs index 03d92c4..cb999a4 100644 --- a/mir/src/function.rs +++ b/mir/src/function.rs @@ -192,7 +192,7 @@ impl Default for Function { fn default() -> Self { Self { - name: Ident::new(""), + name: Ident("".to_string()), args: vec![], ret: T::default(), body: T::default(), diff --git a/mir/src/ident.rs b/mir/src/ident.rs new file mode 100644 index 0000000..c5bff72 --- /dev/null +++ b/mir/src/ident.rs @@ -0,0 +1,23 @@ +use std::fmt::Formatter; + +/// Localized string +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] +pub struct Ident(pub String); + +impl Ident { + pub fn new(s: &'static str) -> Self { + Ident(s.into()) + } +} + +impl std::fmt::Display for Ident { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl PartialEq for Ident { + fn eq(&self, other: &str) -> bool { + self.0 == *other + } +} \ No newline at end of file diff --git a/mir/src/import.rs b/mir/src/import.rs new file mode 100644 index 0000000..9715f62 --- /dev/null +++ b/mir/src/import.rs @@ -0,0 +1,51 @@ +use crate::{Ident, Visibility}; + +pub struct Import { + /// Path that we're importing from + /// e.g. plaid.model in `from plaid.model import ...` + pub path: String, + /// Specific items that are imported + /// e.g. `Account` in `from plaid.model import Account` + pub imports: Vec, + /// If a wildcard import and if we want to alias, then alias + pub alias: Option, + pub vis: Visibility, + pub feature: Option +} + +pub struct ImportItem { + /// This might not conform to standard ident rules for the language, so its a string, not an ident. + pub name: String, + pub alias: Option, +} + +impl ImportItem { + pub fn alias(name: &str, alias: &str) -> Self { + Self { name: name.to_string(), alias: Some(alias.to_string()) } + } +} + +impl From<&String> for ImportItem { + fn from(s: &String) -> Self { + Self { name: s.clone(), alias: None } + } +} + +impl From for ImportItem { + fn from(s: String) -> Self { + Self { name: s, alias: None } + } +} + +impl From<&str> for ImportItem { + fn from(s: &str) -> Self { + Self { name: s.to_string(), alias: None } + } +} + +impl From for ImportItem { + fn from(s: Ident) -> Self { + Self { name: s.0, alias: None } + } +} + diff --git a/mir/src/lib.rs b/mir/src/lib.rs index a830439..3d43058 100644 --- a/mir/src/lib.rs +++ b/mir/src/lib.rs @@ -1,58 +1,24 @@ use core::default::Default; -use core::fmt::{Debug, Formatter}; -use core::option::Option; -use core::option::Option::None; -use quote::{TokenStreamExt}; +use core::fmt::Formatter; -pub use function::{ArgIdent, build_dict, build_struct, FnArg2, Function}; +use quote::TokenStreamExt; + +pub use class::*; pub use doc::{Doc, DocFormat}; +pub use function::{ArgIdent, build_dict, build_struct, FnArg2, Function}; +pub use ident::*; +pub use import::*; pub use ty::*; +pub use visibility::*; mod doc; mod function; mod r#macro; mod ty; - -/// Localized string -#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] -pub struct Ident(pub String); - -impl std::fmt::Display for Ident { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl PartialEq for Ident { - fn eq(&self, other: &str) -> bool { - self.0 == *other - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Visibility { - Public, - Crate, - Private, -} - -impl Default for Visibility { - fn default() -> Self { - Self::Private - } -} - - -#[derive(Debug, Default)] -pub struct Field { - pub name: String, - pub ty: T, - pub default: Option, - pub vis: Visibility, - pub doc: Option, - pub optional: bool, - pub decorators: Vec, -} +mod class; +mod import; +mod visibility; +mod ident; pub struct Interface { pub name: String, @@ -69,26 +35,6 @@ pub struct NewType { pub public: bool, } -pub struct Class { - pub name: Ident, - pub doc: Option, - /// `code` is for Python, where we need code like this: - /// class Account(BaseModel): - /// class Config: - /// this_is_a_config_for_pydantic = True - pub code: Option, - pub instance_fields: Vec>, - pub static_fields: Vec>, - pub constructors: Vec>, - pub class_methods: Vec>, - pub static_methods: Vec>, - pub public: bool, - - pub lifetimes: Vec, - pub decorators: Vec, - pub superclasses: Vec, -} - pub struct File { pub doc: Option, /// Code that is before function and class declarations @@ -101,122 +47,12 @@ pub struct File { pub package: Option, } -pub struct Import { - /// Path that we're importing from - /// e.g. plaid.model in `from plaid.model import ...` - pub path: String, - /// Specific items that are imported - /// e.g. `Account` in `from plaid.model import Account` - pub imports: Vec, - /// If a wildcard import and if we want to alias, then alias - pub alias: Option, - pub vis: Visibility, - pub feature: Option -} - -pub struct ImportItem { - /// This might not conform to standard ident rules for the language, so its a string, not an ident. - pub name: String, - pub alias: Option, -} - pub struct Literal(pub T); pub struct Grave(String); pub struct FString(String); -impl Visibility { - pub fn public(&self) -> bool { - match self { - Visibility::Public => true, - Visibility::Crate => false, - Visibility::Private => false, - } - } -} - -impl From for Ident where T: AsRef { - fn from(s: T) -> Self { - Self::new(s.as_ref()) - } -} - -impl Ident { - pub fn new(s: &str) -> Self { - Self(s.to_string()) - } -} - -impl Default for Class { - fn default() -> Self { - Self { - name: Ident::new(""), - code: None, - doc: None, - instance_fields: vec![], - static_fields: vec![], - constructors: vec![], - class_methods: vec![], - static_methods: vec![], - public: false, - lifetimes: vec![], - decorators: vec![], - superclasses: vec![], - } - } -} - -impl Debug for Class { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let public = self.public; - write!(f, "Class {{ name: {name:?}, \ - doc: {doc:?}, \ - instance_fields: todo!, \ - static_fields: todo!, \ - constructors: todo!, \ - class_methods: todo!, \ - static_methods: todo!, \ - public: {public}, \ - lifetimes: todo!, \ - superclasses: todo! }}", - name = self.name, - doc = self.doc, - ) - } -} - -impl ImportItem { - pub fn alias(name: &str, alias: &str) -> Self { - Self { name: name.to_string(), alias: Some(alias.to_string()) } - } -} - -impl From<&String> for ImportItem { - fn from(s: &String) -> Self { - Self { name: s.clone(), alias: None } - } -} - -impl From for ImportItem { - fn from(s: String) -> Self { - Self { name: s, alias: None } - } -} - -impl From<&str> for ImportItem { - fn from(s: &str) -> Self { - Self { name: s.to_string(), alias: None } - } -} - -impl From for ImportItem { - fn from(s: Ident) -> Self { - Self { name: s.0, alias: None } - } -} - - impl Import { pub fn package(path: &str) -> Self { Self { diff --git a/mir/src/visibility.rs b/mir/src/visibility.rs new file mode 100644 index 0000000..ac628e3 --- /dev/null +++ b/mir/src/visibility.rs @@ -0,0 +1,24 @@ + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Visibility { + Public, + Crate, + Private, +} + + +impl Visibility { + pub fn public(&self) -> bool { + match self { + Visibility::Public => true, + Visibility::Crate => false, + Visibility::Private => false, + } + } +} + +impl Default for Visibility { + fn default() -> Self { + Self::Private + } +} diff --git a/mir_rust/Cargo.toml b/mir_rust/Cargo.toml index 71b7791..23cb030 100644 --- a/mir_rust/Cargo.toml +++ b/mir_rust/Cargo.toml @@ -16,3 +16,4 @@ quote = "1.0.9" syn = "2.0.48" convert_case = "0.6.0" regex = "1.10.3" +prettyplease = "0.2.16" diff --git a/mir_rust/src/class.rs b/mir_rust/src/class.rs index 0b2f10b..3179459 100644 --- a/mir_rust/src/class.rs +++ b/mir_rust/src/class.rs @@ -3,30 +3,48 @@ use quote::quote; use mir::{Class, Field}; -use crate::{FluentBool, ToRustCode, ToRustIdent}; +use crate::{FluentBool, ToRustCode}; impl ToRustCode for Class { fn to_rust_code(self) -> TokenStream { - let vis = self.public.to_value(|| quote!(pub)); - let fields = self.instance_fields.into_iter().map(|f| f.to_rust_code()); - let class_methods = self.class_methods.into_iter().map(|m| m.to_rust_code()); + let Class { + name, + doc, + code, + instance_fields, + static_fields, + constructors, + class_methods, + static_methods, + public, + lifetimes, + decorators, + superclasses + } = self; + assert!(superclasses.is_empty(), "superclasses not supported in Rust"); + assert!(static_fields.is_empty(), "static fields not supported in Rust"); + assert!(constructors.is_empty(), "constructors not supported in Rust"); + assert!(code.is_none(), "code in class body not supported in Rust"); + assert!(static_methods.is_empty(), "static methods not supported in Rust"); - let doc = self.doc.to_rust_code(); - let lifetimes = if self.lifetimes.is_empty() { + let vis = public.to_value(|| quote!(pub)); + let fields = instance_fields.into_iter().map(|f| f.to_rust_code()); + let class_methods = class_methods.into_iter().map(|m| m.to_rust_code()); + + let doc = doc.to_rust_code(); + let lifetimes = if lifetimes.is_empty() { quote! {} } else { - let lifetimes = self.lifetimes.iter().map(|l| { + let lifetimes = lifetimes.iter().map(|l| { let name = syn::Lifetime::new(l, Span::call_site()); quote! { # name } }); quote! { < # ( # lifetimes), * > } }; - let decorator = self.decorators; - let name = self.name; quote! { #doc #( - #decorator + #decorators )* #vis struct #name #lifetimes { #(#fields,)* @@ -40,7 +58,7 @@ impl ToRustCode for Class { impl ToRustCode for Field { fn to_rust_code(self) -> TokenStream { - let name = self.name.to_rust_ident(); + let name = self.name; let ty = if self.optional { let ty = self.ty; quote! { Option<#ty> } diff --git a/mir_rust/src/lib.rs b/mir_rust/src/lib.rs index 2d9b693..b2eb050 100644 --- a/mir_rust/src/lib.rs +++ b/mir_rust/src/lib.rs @@ -176,4 +176,20 @@ mod tests { assert_eq!(String::from(s).to_rust_ident().0, "sd_address_contractor1099"); assert_eq!(sanitize_filename(s), "sd_address_contractor1099"); } -} \ No newline at end of file +} + +pub fn format_code(code: TokenStream) -> String { + let code = code.to_string(); + let syntax_tree = match syn::parse_file(&code) { + Ok(syntax_tree) => syntax_tree, + Err(e) => { + eprintln!("{}", code); + panic!("Failed to parse generated code: {}", e); + } + }; + let mut code = prettyplease::unparse(&syntax_tree); + if code.ends_with('\n') { + code.pop(); + } + code +}