Trait Language

Source
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 your Language struct from configuration which was read from a config file or the command line
  • output_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 call format_type to format the actual types contained in the type definitions, which will in turn dispatch to the relevant format_* 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 by format_type, which is only called if you choose to call it in your write_* 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, and write_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.

  1. The language’s config is loaded from the config file and command line arguments:
let config = Kotlin::Config::deserialize(config_file)?;
  1. 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)?;
  1. 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))
    });
}
  1. 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, the mode argument will include the crate name.
language.begin_file(&mut file, mode)
  1. 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)
  1. For EACE typeshared item in being typeshared, we call write_enum, write_struct, write_type_alias, or write_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.

  1. After all the types are written, we call end_file, with the same arguments that were passed to begin_file.
language.end_file(&mut file, mode)
  1. 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 resembling mod.rs or index.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§

Source

const NAME: &'static str

The lowercase conventional name for this language. This should be a single identifier. It will be used as a prefix for various things; for instance, it will identify this language in the config file, and be used as a prefix when generating CLI parameters

Required Associated Types§

Source

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§

Source

fn new_from_config(config: Self::Config) -> Result<Self>

Create an instance of this language from the loaded configuration.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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§

Source

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.

Source

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)

Source

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).

Source

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).

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Implementors§