-
Notifications
You must be signed in to change notification settings - Fork 42
Codegen ‐ Generating API's
Generating APIs
This article goes over generating the individual resources of Microsoft Graph as API clients and how to add missing API calls.
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.
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 doesnt matter.
- 1. Create A Test File
- 2. First and Second Level Resources
- 3. Add Variants To Resource Identity Enum (graph-core)
- 4. Second Level Resource Write Configuration
- 5. First Level Resource Write Configuration
- 6. Add Links Between Resources
- 7. Run The Code Generation
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.
If you have not yet, you will want to clone this repository. 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 out a new branch or making sure to commit any needed changes.
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 to know more Read First and Second Level Resources
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 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 discussion/issue 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 there separate clients: teams, schedule, and primaryChannel we need to have
a WriteConfiguration object. 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();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));
}