-
Notifications
You must be signed in to change notification settings - Fork 42
Codegen ‐ Generating API's
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.
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.
- 1. Create A Test File
- 2. First and Second Level Resources
- 3. Add Variants To Resource Identity Enum (graph-core)
- 4. Write Configuration
- 5. Add Links Between Resources
- 6. Renaming Methods And Other Attributes
- 7. Run The Code Generation
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.
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"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.
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::ScheduleMake 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.
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.
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}-
primaryChannelis all paths that are/teams/{team-id}/primaryChannel.
-
-
.filter_path(vec!["sharedWithTeams", "messages", "members"]): This is what the actualprimaryChannelwrite configuration has and I left it in here as an example. These are paths, third/fourth+ level resources, that come afterprimaryChannelthat- 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 thesecond_level_buildercall. This sets the path to look for using theResourceIdentity. So after the trim/teams/{team-id}happens codegen will look forprimaryChannelin 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(),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
.childrenmethod takes a list ofWriteConfiguration
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"): TellsApiClientLinkSettings, used by codegen, that these links will be in the impl forTeamsIdApiClient. - 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"); // TeamsIdApiClientWhereas Teams without the id would be:
let name = ResourceIdentity::Teams.exact_pascal_case(); // "Teams"
let api_client_name = format!("{name}ApiClient"); // TeamsApiClientThe 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();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.
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:
- matching: a list of types and values used to match the API that you want to change.
- 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:
-
GeneratedMacroType::FnName: which is the method name of the generated API macro -
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
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:
get_all_channels_countget_incoming_channels_count
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.
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));
}