Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate schemars derives in Rust generator #193

Merged
merged 21 commits into from
May 16, 2024
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ c = Xdrgen::Compilation.new(
namespace: "MyProgram::XDR",
options: {
rust_types_custom_str_impl: [],
rust_types_custom_jsonschema_impl: [],
},
)

Expand Down
2 changes: 2 additions & 0 deletions lib/xdrgen/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def self.run(args)
on 'l', 'language=', 'The output language', default: 'ruby'
on 'n', 'namespace=', '"namespace" to generate code within (language-specific)'
on 'rust-types-custom-str-impl=', 'Rust types that should not have str implementations generated as they will be provided via custom implementations (rust-specific)'
on 'rust-types-custom-jsonschema-impl=', 'Rust types that should not have jsonschema implementations generated as they will be provided via custom implementations (rust-specific)'
end

fail(opts) if args.blank?
Expand All @@ -22,6 +23,7 @@ def self.run(args)
namespace: opts[:namespace],
options: {
rust_types_custom_str_impl: opts[:"rust-types-custom-str-impl"]&.split(',') || [],
rust_types_custom_jsonschema_impl: opts[:"rust-types-custom-jsonschema-impl"]&.split(',') || [],
},
)
compilation.compile
Expand Down
66 changes: 66 additions & 0 deletions lib/xdrgen/generators/rust.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def render_enum_of_all_types(out, types)
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "snake_case")
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum TypeVariant {
#{types.map { |t| "#{t}," }.join("\n")}
}
Expand All @@ -123,6 +124,15 @@ def render_enum_of_all_types(out, types)
pub const fn variants() -> [TypeVariant; #{types.count}] {
Self::VARIANTS
}
#[cfg(feature = "schemars")]
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn json_schema(&self, gen: schemars::gen::SchemaGenerator) -> schemars::schema::RootSchema {
match self {
#{types.map { |t| "Self::#{t} => gen.into_root_schema_for::<#{t}>()," }.join("\n")}
}
}
}
impl Name for TypeVariant {
Expand Down Expand Up @@ -156,6 +166,7 @@ def render_enum_of_all_types(out, types)
serde(rename_all = "snake_case"),
serde(untagged),
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Type {
#{types.map { |t| "#{t}(Box<#{t}>)," }.join("\n")}
}
Expand Down Expand Up @@ -369,6 +380,9 @@ def render_struct(out, struct)
else
out.puts %{#[cfg_attr(all(feature = "serde", feature = "alloc"), derive(serde::Serialize, serde::Deserialize), serde(rename_all = "snake_case"))]}
end
if !@options[:rust_types_custom_jsonschema_impl].include?(name struct)
out.puts %{#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]}
end
out.puts "pub struct #{name struct} {"
out.indent do
struct.members.each do |m|
Expand Down Expand Up @@ -415,6 +429,9 @@ def render_enum(out, enum)
else
out.puts %{#[cfg_attr(all(feature = "serde", feature = "alloc"), derive(serde::Serialize, serde::Deserialize), serde(rename_all = "snake_case"))]}
end
if !@options[:rust_types_custom_jsonschema_impl].include?(name enum)
out.puts %{#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]}
end
out.puts "#[repr(i32)]"
out.puts "pub enum #{name enum} {"
out.indent do
Expand Down Expand Up @@ -544,6 +561,9 @@ def render_union(out, union)
else
out.puts %{#[cfg_attr(all(feature = "serde", feature = "alloc"), derive(serde::Serialize, serde::Deserialize), serde(rename_all = "snake_case"))]}
end
if !@options[:rust_types_custom_jsonschema_impl].include?(name union)
out.puts %{#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]}
end
out.puts "#[allow(clippy::large_enum_variant)]"
out.puts "pub enum #{name union} {"
union_case_count = 0
Expand Down Expand Up @@ -678,6 +698,9 @@ def render_typedef(out, typedef)
else
out.puts %{#[cfg_attr(all(feature = "serde", feature = "alloc"), derive(serde::Serialize, serde::Deserialize), serde(rename_all = "snake_case"))]}
end
if !is_fixed_array_opaque(typedef.type) && !@options[:rust_types_custom_jsonschema_impl].include?(name typedef)
out.puts %{#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]}
end
if !is_fixed_array_opaque(typedef.type)
out.puts "#[derive(Debug)]"
end
Expand Down Expand Up @@ -719,6 +742,43 @@ def render_typedef(out, typedef)
}
EOS
end
if is_fixed_array_opaque(typedef.type) && !@options[:rust_types_custom_jsonschema_impl].include?(name typedef)
out.puts <<-EOS.strip_heredoc
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for #{name typedef} {
fn schema_name() -> String {
"#{name typedef}".to_string()
}
fn is_referenceable() -> bool {
false
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let schema = String::json_schema(gen);
if let schemars::schema::Schema::Object(mut schema) = schema {
schema.extensions.insert(
"contentEncoding".to_owned(),
serde_json::Value::String("hex".to_string()),
);
schema.extensions.insert(
"contentMediaType".to_owned(),
serde_json::Value::String("application/binary".to_string()),
);
let string = *schema.string.unwrap_or_default().clone();
schema.string = Some(Box::new(schemars::schema::StringValidation {
max_length: #{typedef.type.size}_u32.checked_mul(2).map(Some).unwrap_or_default(),
min_length: #{typedef.type.size}_u32.checked_mul(2).map(Some).unwrap_or_default(),
..string
}));
schema.into()
} else {
schema
}
}
}
EOS
end
out.puts <<-EOS.strip_heredoc
impl From<#{name typedef}> for #{reference(typedef, typedef.type)} {
#[must_use]
Expand Down Expand Up @@ -931,6 +991,12 @@ def base_reference(type)
end
end

def array_size(type)
_, size = type.array_size
size = name @top.find_definition(size) if is_named
size
end

def reference(parent, type)
base_ref = base_reference type

Expand Down
92 changes: 92 additions & 0 deletions lib/xdrgen/generators/rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,32 @@ impl<T, const MAX: u32> Default for VecM<T, MAX> {
}
}

#[cfg(feature = "schemars")]
impl<T: schemars::JsonSchema, const MAX: u32> schemars::JsonSchema for VecM<T, MAX> {
fn schema_name() -> String {
format!("VecM<{}, {}>", T::schema_name(), MAX)
}

fn is_referenceable() -> bool {
false
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let schema = Vec::<T>::json_schema(gen);
if let schemars::schema::Schema::Object(mut schema) = schema {
if let Some(array) = schema.array.clone() {
schema.array = Some(Box::new(schemars::schema::ArrayValidation {
max_items: Some(MAX),
..*array
}));
}
schema.into()
} else {
schema
}
}
}

impl<T, const MAX: u32> VecM<T, MAX> {
pub const MAX_LEN: usize = { MAX as usize };

Expand Down Expand Up @@ -1323,6 +1349,40 @@ impl<const MAX: u32> Deref for BytesM<MAX> {
}
}

#[cfg(feature = "schemars")]
impl<const MAX: u32> schemars::JsonSchema for BytesM<MAX> {
fn schema_name() -> String {
format!("BytesM<{MAX}>")
}

fn is_referenceable() -> bool {
false
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let schema = String::json_schema(gen);
if let schemars::schema::Schema::Object(mut schema) = schema {
schema.extensions.insert(
"contentEncoding".to_owned(),
serde_json::Value::String("hex".to_string()),
);
schema.extensions.insert(
"contentMediaType".to_owned(),
serde_json::Value::String("application/binary".to_string()),
);
let string = *schema.string.unwrap_or_default().clone();
schema.string = Some(Box::new(schemars::schema::StringValidation {
max_length: MAX.checked_mul(2).map(Some).unwrap_or_default(),
min_length: None,
..string
}));
schema.into()
} else {
schema
}
}
}

impl<const MAX: u32> Default for BytesM<MAX> {
fn default() -> Self {
Self(Vec::default())
Expand Down Expand Up @@ -1711,6 +1771,27 @@ impl<const MAX: u32> Default for StringM<MAX> {
}
}

#[cfg(feature = "schemars")]
impl<const MAX: u32> schemars::JsonSchema for StringM<MAX> {
fn schema_name() -> String {
format!("StringM<{MAX}>")
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let schema = String::json_schema(gen);
if let schemars::schema::Schema::Object(mut schema) = schema {
let string = *schema.string.unwrap_or_default().clone();
schema.string = Some(Box::new(schemars::schema::StringValidation {
max_length: Some(MAX),
..string
}));
schema.into()
} else {
schema
}
}
}

impl<const MAX: u32> StringM<MAX> {
pub const MAX_LEN: usize = { MAX as usize };

Expand Down Expand Up @@ -2027,6 +2108,17 @@ pub struct Frame<T>(pub T)
where
T: ReadXdr;

#[cfg(feature = "schemars")]
impl<T: schemars::JsonSchema + ReadXdr> schemars::JsonSchema for Frame<T> {
fn schema_name() -> String {
format!("Frame<{}>", T::schema_name())
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
T::json_schema(gen)
}
}

impl<T> ReadXdr for Frame<T>
where
T: ReadXdr,
Expand Down
14 changes: 14 additions & 0 deletions lib/xdrgen/output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ def initialize(source_paths, output_dir)
@files = {}
end

def inputs_hash
Digest::SHA256.hexdigest(
[
Digest::SHA256.hexdigest(relative_source_paths.map { |p| Digest::SHA256.file(p).hexdigest }.join),
Digest::SHA256.hexdigest(relative_source_paths.map { |p| Digest::SHA256.hexdigest(p) }.join),
Digest::SHA256.hexdigest(@output_dir),
].join
)
end

def relative_source_paths
@source_paths.map { |p| Pathname.new(p).expand_path.relative_path_from(Dir.pwd).to_s }.sort
end
Expand All @@ -21,6 +31,10 @@ def relative_source_path_sha256_hashes
relative_source_paths.map { |p| [p, Digest::SHA256.file(p).hexdigest] }.to_h
end

def relative_source_path_sha256_hash
Digest::SHA256.hexdigest(relative_source_paths.map { |p| Digest::SHA256.file(p).hexdigest }.join)
end

def open(child_path)
if @files.has_key?(child_path)
raise Xdrgen::DuplicateFileError, "Cannot open #{child_path} twice"
Expand Down
19 changes: 18 additions & 1 deletion spec/lib/xdrgen/rust_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,28 @@
"MyStruct",
"LotsOfMyStructs",
],
rust_types_custom_jsonschema_impl: [],
}
end

it "can generate #{File.basename path} with custom jsonschema impls" do
c = generate path, "_custom_jsonschema_impls", {
rust_types_custom_str_impl: [],
rust_types_custom_jsonschema_impl: [
"Foo",
"TestArray",
"Color2",
"UnionKey",
"MyUnion",
"HasOptions",
"MyStruct",
"LotsOfMyStructs",
],
}
end
end

def generate(path, output_sub_path, options = {rust_types_custom_str_impl: []})
def generate(path, output_sub_path, options = {rust_types_custom_str_impl: [], rust_types_custom_jsonschema_impl: []})
compilation = Xdrgen::Compilation.new(
[path],
output_dir: "#{SPEC_ROOT}/output/generator_spec_rust#{output_sub_path}/#{File.basename path}",
Expand Down
Loading
Loading