pub trait Language<'config>:
Sized
+ Sync
+ Debug {
type Config: Deserialize<'config> + Serialize;
const NAME: &'static str;
Show 18 methods
// Required methods
fn new_from_config(config: Self::Config) -> Result<Self>;
fn output_filename_for_crate(&self, crate_name: &CrateName) -> String;
fn format_special_type(
&self,
special_ty: &SpecialRustType,
generic_context: &[TypeName],
) -> Result<String>;
fn write_imports<'a, Crates, Types>(
&self,
writer: &mut impl Write,
crate_name: &CrateName,
imports: Crates,
) -> Result<()>
where Crates: IntoIterator<Item = (&'a CrateName, Types)>,
Types: IntoIterator<Item = &'a TypeName>;
fn write_type_alias(
&self,
w: &mut impl Write,
t: &RustTypeAlias,
) -> Result<()>;
fn write_struct(&self, w: &mut impl Write, rs: &RustStruct) -> Result<()>;
fn write_enum(&self, w: &mut impl Write, e: &RustEnum) -> Result<()>;
fn write_const(&self, w: &mut impl Write, c: &RustConst) -> Result<()>;
// Provided methods
fn mapped_type(&self, type_name: &TypeName) -> Option<Cow<'_, str>> { ... }
fn format_type(
&self,
ty: &RustType,
generic_context: &[TypeName],
) -> Result<String> { ... }
fn format_simple_type(
&self,
base: &TypeName,
generic_context: &[TypeName],
) -> Result<String> { ... }
fn format_generic_type(
&self,
base: &TypeName,
parameters: &[RustType],
generic_context: &[TypeName],
) -> Result<String> { ... }
fn format_generic_parameters(
&self,
parameters: &[RustType],
generic_context: &[TypeName],
) -> Result<String> { ... }
fn begin_file(
&self,
w: &mut impl Write,
mode: FilesMode<&CrateName>,
) -> Result<()> { ... }
fn end_file(
&self,
w: &mut impl Write,
mode: FilesMode<&CrateName>,
) -> Result<()> { ... }
fn write_struct_types_for_enum_variants(
&self,
w: &mut impl Write,
e: &RustEnum,
make_struct_name: &impl Fn(&TypeName) -> String,
) -> Result<()> { ... }
fn exclude_from_import_analysis(&self, name: &TypeName) -> bool { ... }
fn write_additional_files<'a>(
&self,
output_folder: &Path,
output_files: impl IntoIterator<Item = (&'a CrateName, &'a Path)>,
) -> Result<()> { ... }
}
Expand description
The trait you need to implement in order to have your own implementation of typeshare. The whole world revolves around this trait.
In general, to implement this correctly, you must implement:
new_from_config
, which instantiates yourLanguage
struct from configuration which was read from a config file or the command lineoutput_filename_for_crate
, which (in multi-file mode) produces a file name from a crate name. All of the typeshared types from that crate will be written to that file.- The
write_*
methods, which output the actual type definitions. These methods should callformat_type
to format the actual types contained in the type definitions, which will in turn dispatch to the relevantformat_*
method, depending on what kind of type it is. - The
format_special_type
method, which outputs things like integer types, arrays, and other builtin or primitive types. This method is only ever called byformat_type
, which is only called if you choose to call it in yourwrite_*
implementations.
Additionally, you must provide a Config
associated type, which must implement
Serialize + Deserialize
. This type will be used to load configuration from
a config file and from the command line arguments for your language, which will
be passed to new_from_config
. This type should provide defaults for all of
its fields; it should always tolerate being loaded from an empty config file.
When serializing, this type should always output all of its fields, even if
they’re defaulted.
It’s also very common to implement:
mapped_type
, to define certain types as having specialied handling in your lanugage.begin_file
,end_file
, andwrite_additional_files
, to add additional per-file or per-directory content to your output.
If your language spells type names in an unusual way (here, defined as the C++
descended convention, where a type might be spelled Foo<Bar, Baz<Zed>>
),
you’ll want to implement the format_*
methods.
Other methods can be specialized as needed.
§Typeshare execution flow.
This is the detailed flow of how the Language
trait is actually used by
typeshare. It includes references to all of the methods that are called, and
in what order. For these examples, we’re assuming a hypothetical implementation
for Kotlin, which means that there must be impl Language<'_> for Kotlin
somewhere.
- The language’s config is loaded from the config file and command line arguments:
let config = Kotlin::Config::deserialize(config_file)?;
- The language is loaded from the config file via
new_from_config
. This is where the implementation has the opportunity to report any configuration errors that weren’t detected during deserialization.
let language = Kotlin::new_from_config(config)?;
- If we’re in multi-file mode, we call
output_filename_for_crate
for each rust crate being typeshared to determine the filename for the output file that will contain that crate’s types.
let files = crate_names
.iter()
.map(|crate_name| {
let filename = language.output_filename_for_crate(crate_name);
File::create(output_directory.join(filename))
});
}
- We call
begin_file
on the output type to print any headers or preamble appropriate for this language. In multi-file mode,begin_file
is called once for each output file; in this case, themode
argument will include the crate name.
language.begin_file(&mut file, mode)
- In mutli-file mode only, we call
write_imports
with a list of all the types that are being imported from other typeshare’d crates. This allows the language to emit appropriate import statements for its own language.
// Only in multi-file mode
language.write_imports(&mut file, crate_name, computed_imports)
- For EACE typeshared item in being typeshared, we call
write_enum
,write_struct
,write_type_alias
, orwrite_const
, as appropriate.
language.write_struct(&mut file, parsed_struct);
language.write_enum(&mut file, parsed_enum);
6a. In your implementations of these methods, we recommend that you call
format_type
for the fields of these types. format_type
will in turn call
format_simple_type
, format_generic_type
, or format_special_type
, as
appropriate; usually it is only necessary for you to implmenent
format_special_type
yourself, and use the default implementations for the
others. The format_*
methods will otherwise never be called by typeshare.
6b. If your language doesn’t natively support data-containing enums, we
recommand that you call write_types_for_anonymous_structs
in your
implementation of write_enum
; this will call write_struct
for each variant
of the enum.
- After all the types are written, we call
end_file
, with the same arguments that were passed tobegin_file
.
language.end_file(&mut file, mode)
- In multi-file mode only, after ALL files are written, we call
write_additional_files
with the output directory. This gives the language an opportunity to create any files resemblingmod.rs
orindex.js
as it might require.
// Only in multi-file mode
language.write_additional_files(&output_directory, generated_files.iter())
NOTE: at this stage, multi-file output is still work-in-progress, as the algorithms that compute import sets are being rewritten. The API presented here is stable, but output might be buggy while issues with import detection are resolved.
In the future, we hope to make mutli-file mode multithreaded, capable of
writing multiple files concurrently from a shared Language
instance.
Language
therefore has a Sync
bound to keep this possibility available.
Required Associated Constants§
Required Associated Types§
Sourcetype Config: Deserialize<'config> + Serialize
type Config: Deserialize<'config> + Serialize
The configuration for this language. This configuration will be loaded
from a config file and, where possible, from the command line, via
serde
.
It is important that this type include #[serde(default)]
or something
equivelent, so that a config can be loaded with default setting even
if this language isn’t present in the config file.
The serialize
implementation for this type should NOT skip keys, if
possible.
Required Methods§
Sourcefn new_from_config(config: Self::Config) -> Result<Self>
fn new_from_config(config: Self::Config) -> Result<Self>
Create an instance of this language from the loaded configuration.
Sourcefn output_filename_for_crate(&self, crate_name: &CrateName) -> String
fn output_filename_for_crate(&self, crate_name: &CrateName) -> String
In multi-file mode, typeshare will output one separate file with this name for each crate in the input set. These file names should have the appropriate naming convention and extension for this language.
This method isn’t used in single-file mode.
Sourcefn format_special_type(
&self,
special_ty: &SpecialRustType,
generic_context: &[TypeName],
) -> Result<String>
fn format_special_type( &self, special_ty: &SpecialRustType, generic_context: &[TypeName], ) -> Result<String>
Format a special type. This will handle things like arrays, primitives, options, and so on. Every lanugage has different spellings for these types, so this is one of the key methods that a language implementation needs to deal with.
Sourcefn write_imports<'a, Crates, Types>(
&self,
writer: &mut impl Write,
crate_name: &CrateName,
imports: Crates,
) -> Result<()>where
Crates: IntoIterator<Item = (&'a CrateName, Types)>,
Types: IntoIterator<Item = &'a TypeName>,
fn write_imports<'a, Crates, Types>(
&self,
writer: &mut impl Write,
crate_name: &CrateName,
imports: Crates,
) -> Result<()>where
Crates: IntoIterator<Item = (&'a CrateName, Types)>,
Types: IntoIterator<Item = &'a TypeName>,
For generating import statements. This is called only in multi-file
mode, after begin_file
and before any other writes.
imports
includes an ordered list of type names that typeshare
believes are being imported by this file, grouped by the crates they
come from. typeshare
guarantees that these will be passed in some stable
order, so that your output remains consistent.
NOTE: Currently this is bugged and doesn’t receive correct imports. This will be fixed in a future release.
Sourcefn write_type_alias(&self, w: &mut impl Write, t: &RustTypeAlias) -> Result<()>
fn write_type_alias(&self, w: &mut impl Write, t: &RustTypeAlias) -> Result<()>
Write a type alias definition.
Example of a type alias:
type MyTypeAlias = String;
Generally this method will call self.format_type
to produce the
aliased type name in the output definition.
Sourcefn write_struct(&self, w: &mut impl Write, rs: &RustStruct) -> Result<()>
fn write_struct(&self, w: &mut impl Write, rs: &RustStruct) -> Result<()>
Write a struct definition.
Example of a struct:
#[typeshare]
#[derive(Serialize, Deserialize)]
struct Foo {
bar: String
}
Generally this method will call self.format_type
to produce the types
of the individual fields.
Sourcefn write_enum(&self, w: &mut impl Write, e: &RustEnum) -> Result<()>
fn write_enum(&self, w: &mut impl Write, e: &RustEnum) -> Result<()>
Write an enum definition.
Example of an enum:
#[typeshare]
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "content")]
enum Foo {
Fizz,
Buzz { yep_this_works: bool }
}
Generally this will call self.format_type
to produce the types of
the individual fields. If this enum is an algebraic sum type, and this
language doesn’t really support those, it should consider calling
write_struct_types_for_enum_variants
to produce struct types matching
those variants, which can be used for this language’s abstraction for
data like this.
Sourcefn write_const(&self, w: &mut impl Write, c: &RustConst) -> Result<()>
fn write_const(&self, w: &mut impl Write, c: &RustConst) -> Result<()>
Write a constant variable.
Example of a constant variable:
const ANSWER_TO_EVERYTHING: u32 = 42;
If necessary, generally this will call self.format_type
to produce
the type of this constant (though some languages are allowed to omit
it).
Provided Methods§
Sourcefn mapped_type(&self, type_name: &TypeName) -> Option<Cow<'_, str>>
fn mapped_type(&self, type_name: &TypeName) -> Option<Cow<'_, str>>
Most languages provide manual overrides for specific types. When a type is formatted with a name that matches a mapped type, the mapped type name is formatted instead.
By default this returns None
for all types.
Sourcefn format_type(
&self,
ty: &RustType,
generic_context: &[TypeName],
) -> Result<String>
fn format_type( &self, ty: &RustType, generic_context: &[TypeName], ) -> Result<String>
Convert a Rust type into a type from this language. By default this
calls format_simple_type
, format_generic_type
, or
format_special_type
, depending on the type. There should only rarely
be a need to specialize this.
This method should be called by the write_*
methods to write the types
contained by type definitions.
The generic_context
is the set of generic types being provided by
the enclosing type definition; this allows languages that do type
renaming to be able to distinguish concrete type names (like int
)
from generic type names (like T
)
Sourcefn format_simple_type(
&self,
base: &TypeName,
generic_context: &[TypeName],
) -> Result<String>
fn format_simple_type( &self, base: &TypeName, generic_context: &[TypeName], ) -> Result<String>
Format a simple type with no generic parameters.
By default, this first checks self.mapped_type
to see if there’s an
alternative way this type should be formatted, and otherwise prints the
base
verbatim.
The generic_context
is the set of generic types being provided by
the enclosing type definition; this allows languages that do type
renaming to be able to distinguish concrete type names (like int
)
from generic type names (like T
).
Sourcefn format_generic_type(
&self,
base: &TypeName,
parameters: &[RustType],
generic_context: &[TypeName],
) -> Result<String>
fn format_generic_type( &self, base: &TypeName, parameters: &[RustType], generic_context: &[TypeName], ) -> Result<String>
Format a generic type that takes in generic arguments, which may be recursive.
By default, this creates a composite type name by appending
self.format_simple_type
and self.format_generic_parameters
. With
their default implementations, this will print name<parameters>
,
which is a common syntax used by many languages for generics.
The generic_context
is the set of generic types being provided by
the enclosing type definition; this allows languages that do type
renaming to be able to distinguish concrete type names (like int
)
from generic type names (like T
).
Sourcefn format_generic_parameters(
&self,
parameters: &[RustType],
generic_context: &[TypeName],
) -> Result<String>
fn format_generic_parameters( &self, parameters: &[RustType], generic_context: &[TypeName], ) -> Result<String>
Format generic parameters into a syntax used by this language. By
default, this returns <A, B, C, ...>
, since that’s a common syntax
used by most languages.
This method is only used when format_generic_type
calls it.
The generic_context
is the set of generic types being provided by
the enclosing type definition; this allows languages that do type
renaming to be able to distinguish concrete type names (like int
)
from generic type names (like T
).
Sourcefn begin_file(
&self,
w: &mut impl Write,
mode: FilesMode<&CrateName>,
) -> Result<()>
fn begin_file( &self, w: &mut impl Write, mode: FilesMode<&CrateName>, ) -> Result<()>
Write a header for typeshared code. This is called unconditionally at the start of the output file (or at the start of all files, if in multi-file mode).
By default this does nothing.
Sourcefn end_file(
&self,
w: &mut impl Write,
mode: FilesMode<&CrateName>,
) -> Result<()>
fn end_file( &self, w: &mut impl Write, mode: FilesMode<&CrateName>, ) -> Result<()>
Write a header for typeshared code. This is called unconditionally at the end of the output file (or at the end of all files, if in multi-file mode).
By default this does nothing.
Sourcefn write_struct_types_for_enum_variants(
&self,
w: &mut impl Write,
e: &RustEnum,
make_struct_name: &impl Fn(&TypeName) -> String,
) -> Result<()>
fn write_struct_types_for_enum_variants( &self, w: &mut impl Write, e: &RustEnum, make_struct_name: &impl Fn(&TypeName) -> String, ) -> Result<()>
Write out named types to represent anonymous struct enum variants.
Take the following enum as an example:
enum AlgebraicEnum {
AnonymousStruct {
field: String,
another_field: bool,
},
Variant2 {
field: i32,
}
}
This function will write out a pair of struct types resembling:
struct AnonymousStruct {
field: String,
another_field: bool,
}
struct Variant2 {
field: i32,
}
Except that it will use make_struct_name
to compute the names of these
structs based on the names of the variants.
This method isn’t called by default; it is instead provided as a helper
for your implementation of write_enum
, since many languages don’t have
a specific notion of an algebraic sum type, and have to emulate it with
subclasses, tagged unions, or something similar.
Sourcefn exclude_from_import_analysis(&self, name: &TypeName) -> bool
fn exclude_from_import_analysis(&self, name: &TypeName) -> bool
If a type with this name appears in a type definition, it will be
unconditionally excluded from cross-file import analysis. Usually this will
be the types in mapped_types
, since those are types with special behavior
(for instance, a datetime date provided as a standard type by your
langauge).
This is mostly a performance optimization. By default it returns false
for all types.
Sourcefn write_additional_files<'a>(
&self,
output_folder: &Path,
output_files: impl IntoIterator<Item = (&'a CrateName, &'a Path)>,
) -> Result<()>
fn write_additional_files<'a>( &self, output_folder: &Path, output_files: impl IntoIterator<Item = (&'a CrateName, &'a Path)>, ) -> Result<()>
In multi-file mode, this method is called after all of the individual
typeshare files are completely generated. Use it to generate any
additional files your language might need in this directory to
function correctly, such as a mod.rs
, __init__.py
, index.js
, or
anything else like that.
It passed a list of crate names, for each crate that was typeshared, and the associated file paths, indicating all of the files that were generated by typeshare.
By default, this does nothing.
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety", so this trait is not object safe.