Skip to content

Commit

Permalink
Generate schemars derives in Rust generator (#193)
Browse files Browse the repository at this point in the history
* Generate schemars derives in Rust generator

* update tsets

* move jsonschema behind own feature and add support for custom

* Add manual JsonSchema impls

Copied from 66a4913f1bd0de167ff50edd05c93dd958bec0eb
Copied from stellar/rs-stellar-xdr#349

Co-authored-by: Willem Wyndham <[email protected]>

* fix dead code

* simplified features

* gen for fixed opaque typedefs

* fix cas

* fix

* fix

* add type macro

* add simple methnod for generating json schema

* upd tests

* simplified the fns

* fix tests

* fix

* fix

* test

* fix

* defer adding macro that isn't required by this pr

---------

Co-authored-by: Willem Wyndham <[email protected]>
  • Loading branch information
leighmcculloch and willemneal authored May 16, 2024
1 parent 6cd7f3b commit ac16535
Show file tree
Hide file tree
Showing 31 changed files with 29,766 additions and 2 deletions.
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

0 comments on commit ac16535

Please sign in to comment.