Skip to content

Codegen ‐ Generating API's

Sean Reeise edited this page Nov 12, 2023 · 3 revisions

Generating API Clients - Codegen

This wiki describes how to generate APIs for the graph-rs-sdk client using the OpenApi configs from the Microsoft Graph ms-metadata repository.

You will need to clone the graph-rs-sdk repository if you have not yet. The project is configured to generate code inside of the graph-rs-sdk repository and is not configured for other use cases nor is the graph-codegen crate being exported in the graph-rs-sdk crate.

This is going to change the code in the repositiory without regard to any previous changes you may have made. Consider checking out a new branch or making sure to commit any needed changes.

Quick Example

If you just want to play around and try something fast to see how codegen works then the following example will generate the Teams Api.

Clone the project and create a test.rs file in the examples directory and add the code below. test.rs is already in the .gitignore for testing and codegen use.

use graph_codegen::macros::OpenApiParser;
use graph_codegen::openapi::OpenApi;
use graph_codegen::settings::get_write_configuration;
use graph_core::resource::ResourceIdentity;

fn main() {
    OpenApi::write(get_write_configuration(ResourceIdentity::Teams));
}

Any client that is already generated should generate about the same thing posibly minus or plus a few methods depending on how Microsoft updated the OpenApi configs. There also might be some rearranging of the methods in their order but that really doesn't matter.

Table Of Contents

1. Create A Test File

First, create a file called test.rs in the examples directory.

mkdir examples/test.rs

The examples/test.rs file is already in the .gitignore so you can run codegen from this file.

For a walkthrough we will generate the Teams API from scratch.

The first thing we want to do is look at the paths for the teams api. You can use methods on the codegen OpenApi object to find any paths containing teams. We want to search for teams because the teams API starts every URL path with teams such as /teams/$count.

The OpenApi object has convenience debug methods and generating clients such as debug_path_contains and debug_path_contains_all.

/examples/test.rs

use graph_codegen::openapi::OpenApi;

fn main() {
    let open_api = OpenApi::default();

    open_api.debug_path_contains("teams");
}

When we print the teams APIs you can see we roped in anything that has the word teams. There is many paths printed but we can break it down into categories. First we get paths that start with /teams and of those we can break those down by looking at what is called in codegen as a first second level resource. Think of a resource as the names of the individual APIs themselves such as users, me, groups, sites, teams, etc.

2. First and Second Level Resources

First Level Resource

Or Top Level Resource is any resource that is the start of a path. Think of your main Graph Api resources such as

"/me"
"/users"
"/teams"
"/groups"

Second Level Resources

Any paths, except parts of the path that are ids such as {team-id}, that come immediately after the first level resource in the path assuming that we skip over any id parts of the path.

For example primaryChannel is a second level resource of teams in the path:

"/teams/{team-id}/primaryChannel"

This is an important part to how the codgen is done internally. And there are other factors that go into whether a second level resource should be generated as a separate api client. If you want deep dive into this topic see First and Second Level Resources

In addition, you will see how the codgen deals with method naming conflicts later on in Renaming Methods And Other Attributes.

3. Add Variants To Resource Identity Enum (graph-core)

So for teams we want to find out what second level resources we should use as their own api clients under the teams directory and also tell the codegen to ignore those paths when generating the teams API client.

For this example lets assume schedule and primaryChannel are the only two second level resources we found.

First we need to add a ResourceIdentity for these clients. The ResourceIdentity enum is used to get the path of a client. The name of the enum variant must be exactly matching the path or you must add a line in the ToString method of ResourceIdentity that returns the exact path. For instance, ResourceIdentity::Teams will convert to teams on its own because Teams is the exact path. But things like access review definitions which is a large API on its own and also a third/fourth level resource is named in a way to show that the Definitions api client is a child of AccessReviews. This is for convenience and therefore has the following line in the ToString method:

ResourceIdentity::AccessReviewsDefinitions => "definitions".to_string()

Once the layers start adding up you can generally just do what makes sense or ask in the discussions/issues on GitHub about it. There really is not a hard or fast rule here.

So we now have three new variants for ResourceIdentity:

ResourceIdentity::Teams,
ResourceIdentity::PrimaryChannel,
ResourceIdentity::Schedule

Make sure you place the variants in alphabetical order. The remain crate is used to enforce this rule in order to make things cleaner with such a large enum. Just go down the variants in alphabetical order and find the spot for it, and pay less attention to what remain says about where to put it until you are close to alphabetical order because its recommendations may end up telling you to move it 50 places and compile each time to check before you get to the actual place its suppose to be.

4. Write Configuration

In order to generate these three separate clients: teams, schedule, and primaryChannel we need to have a WriteConfiguration object.

Both First Level Resources and Second Level Resources use a WriteConfiguration in codegen.

For simplicity the WriteConfiguration has builder methods for first level resources (also called top level resources), and second level resources.

Second Level Resource Write Configuration

Lets use the builder for primaryChannel:

// In a match here so resource_identity is ResourceIdentity::PrimaryChannel
ResourceIdentity::PrimaryChannel => WriteConfiguration::second_level_builder(ResourceIdentity::Teams, resource_identity)
        .trim_path_start("/teams/{team-id}".to_string())
        .filter_path(vec!["sharedWithTeams", "messages", "members"])
        .build()
        .unwrap(),
  • .trim_path_start("/teams/{team-id}".to_string()) tells the OpenApi writer to trim paths starting with /teams/{team-id}
    • primaryChannel is all paths that are /teams/{team-id}/primaryChannel.
  • .filter_path(vec!["sharedWithTeams", "messages", "members"]): This is what the actual primaryChannel write configuration has and I left it in here as an example. These are paths, third/fourth+ level resources, that come after primaryChannel that
    • are either too large to not be their own api client
    • are used by multiple different API clients and therefore its easier to implement them once and then link to them
    • are from outer space and for some reason have the exact same method names, like 5 and 6 + method names, as the current api client method names but with completely different paths. However, some api clients have the exact same API request such as I mentioned with $count.
  • IMPORTANT: Last is the .path(&str) method that is abstracted in the second_level_builder call. This sets the path to look for using the ResourceIdentity. So after the trim /teams/{team-id} happens codegen will look for primaryChannel in the path.

Another thing that is abstracted is the filters. The ResourceIdentity enums here are used to filter down the OpenApi config from the 100s of resources available to just the Teams resource and then to just the PrimaryChannel resource.

The Schedule WriteConfiguration is one line simpler because it didnt have any second or third level resources:

ResourceIdentity::Schedule => WriteConfiguration::second_level_builder(ResourceIdentity::Teams, resource_identity)
    .trim_path_start("/teams/{team-id}".to_string())
    .build()
    .unwrap(),

First Level Resource Write Configuration

The Teams configuration:

ResourceIdentity::Teams =>
    WriteConfiguration::builder(resource_identity)
        .children(vec![
            get_write_configuration(ResourceIdentity::PrimaryChannel),
            get_write_configuration(ResourceIdentity::Schedule),
        ]
    )
    .build()
    .unwrap(),
  • Top level resources are special because they can be used to generate multiple other api clients when those api clients are their children. You can see the .children method takes a list of WriteConfiguration

5. Add Links Between Resources

In order for the client to go from teams to primaryChannel and Schedule we need to make sure the codegen adds links between the resources

ResourceSettings stores imports and links between resources.

We will need to create the ResourceSettings for new clients that do not have one yet.

ResourceIdentity::Teams => ResourceSettings::builder(path_name, ri)
    .imports(vec!["crate::users::*", "crate::teams::*"])
    .api_client_links(vec![
        ApiClientLinkSettings(Some("TeamsIdApiClient"),
	   vec![
	        ApiClientLink::Struct("primary_channel", "PrimaryChannelApiClient"),
                ApiClientLink::Struct("schedule", "ScheduleApiClient"),
	     ]
        )
]).build().unwrap(),
  • Some("TeamsIdApiClient"): Tells ApiClientLinkSettings, used by codegen, that these links will be in the impl for TeamsIdApiClient.
  • The macro queue writer is basically where all the macros are written for the project and where the names are created for the api clients.

In the case of Teams its similar to:

let name = ResourceIdentity::Teams.exact_pascal_case(); // "Teams"
let api_client_name = format!("{name}IdApiClient"); // TeamsIdApiClient

Whereas Teams without the id would be:

let name = ResourceIdentity::Teams.exact_pascal_case(); // "Teams"
let api_client_name = format!("{name}ApiClient"); // TeamsApiClient

The ApiClientLink::Struct("primary_channel", "PrimaryChannelApiClient"), will add a macro call to the impl of TeamsIdApiClient

ApiClientLink::Struct("primary_channel", "PrimaryChannelApiClient"), // ApiClientLink::Struct is for non id clients
ApiClientLink::StructId("team", "TeamsIdApiClient"); // ApiClientStructId is for id clients.


impl TeamsIdApiClient {
    api_client_link!(primary_channel, PrimaryChannelApiClient);
}

And this allows us to do calls like so:

client
   .team("TEAM_ID")
   .primary_channel()
   .get_primary_channels();

6. Renaming Methods And Other Attributes

When generating API clients you may find that two methods that were generated, under the same impl block, have the exact same method name and therefore the generated code won't compile. One example of this is that almost all Graph APIs that end their path with $count will have the method name count. This happens because this is the method name provided in the OpenApi document. For instance, in the teams API there are two APIs which relate to counts for channels. One is for all channel counts, and the other is for incoming channel counts. But both have the same method name: count.

The two APIs paths are similar with the one distinction in the path: allChannels and incomingChannels:

"/teams/{{RID}}/allChannels/$count"

"/teams/{{RID}}/incomingChannels/$count"

The {{RID}} specific part of the path is what the codegen replaces the original {teams-id} with in order to properly work in the SDK.

MethodMacroModifier

In these cases, instead of generating a separate API for just two methods, it would be better to just change their method names to something more fitting. This is where a MethodMacroModifier comes into play. A MethodMacroModifier provides the codegen with changes that need to be made to the ouputted macro definitions and one of the most frequent changes is method name changes.

Here is the MethodMacroModifier code for changing the method names of these two channel methods:

MethodMacroModifier {
    matching: vec![GeneratedMacroType::FnName("count"), GeneratedMacroType::Path("/teams/{{RID}}/allChannels/$count")],
    update: GeneratedMacroType::FnName("get_all_channels_count"),
},
MethodMacroModifier {
    matching: vec![
        GeneratedMacroType::FnName("count"),
        GeneratedMacroType::Path("/teams/{{RID}}/incomingChannels/$count")
    ],
    update: GeneratedMacroType::FnName("get_incoming_channels_count"),
},

The MethodMacroModifier takes input for two areas:

  1. matching: a list of types and values used to match the API that you want to change.
  2. update: The type and value of the API that you want to change.

The types are called GeneratedMacroType and in the above code we see two variants:

  1. GeneratedMacroType::FnName: which is the method name of the generated API macro
  2. GeneratedMacroType::Path: which is the path of the API.

Whenver you need to change the generated macro, such as the changing method name, it is always best to use two GeneratedMacroType's in the matching matching vector to prevent unwanted changes to other API methods.

The MethodMacroModifier's you see above are used in a ResourceIdentity match block in graph-codegen/src/settings/method_macro_modifier.rs

Choosing Method Names

In order to update the method name you must choose a new method name. There is no special formula or set of steps for choosing a new method name. What you want to do is just look at the API documentation on Microsoft's website for the Graph API and the API path in order to determine a good name. Here the two method names were changed from count to:

  1. get_all_channels_count
  2. get_incoming_channels_count

Regenerating Code After Updating Attributes

Typically, you will generate the code, which is done in step 8, and find that you have two similar method names and that the code won't compile.

In this situation what you will need to do is to first remove the generate code and then add the MethodMacroModifier types and values to the codegen. After this, you can use codegen to generate the code again.

7. Run The Code Generation

Create a test.rs file in the examples directory and add the code below. test.rs is already in the .gitignore for testing and codegen use.

Run test.rs

cargo run --example test.rs

then run cargo format:

cargo fmt

use graph_codegen::macros::OpenApiParser;
use graph_codegen::openapi::OpenApi;
use graph_codegen::settings::get_write_configuration;
use graph_core::resource::ResourceIdentity;

fn main() {
    OpenApi::write(get_write_configuration(ResourceIdentity::Teams));
}