WorkOS Hack Week II
Contents
This past week was the second Hack Week at WorkOS.
I wanted to take some time to share what I got up to over the course of the week.
What is Hack Week?
Hack Week is an event we run somewhat regularly at WorkOS. We take a week where we set aside our usual work and work on other WorkOS-adjacent projects.
For this Hack Week I paired up with Sheldon from our Developer Success team to build out the WorkOS Rust SDK.
Rust SDK Background
I've wanted to build a Rust SDK for WorkOS for a while now.
My first attempt was back in December 2020, shortly after first joining WorkOS, but I didn't really have bandwidth to work on it at the time. I picked up the project again in April of this year and was able to get the foundations of the project in place. The plan was to find some volunteers to help me finish the SDK on the side.
However, coming into Hack Week we received an inquiry about a Rust SDK from a developer interested in using WorkOS. I'll use whatever excuse I can to write Rust, so I decided that this would be the opportune time to finish up the SDK in preparation for release.
Design Principles
There are a few guiding design principles that we applied while building out the Rust SDK:
Make illegal states unrepresentable
The phrase "make illegal states unrepresentable" is fairly well-known at this point, but it's still such a powerful principle when designing software interfaces.
In the Rust SDK we do our best to follow this principle and make illegal states unrepresentable, where possible.
A good example of this is when initiating SSO. The "Get Authorization URL" endpoint accepts a parameter known as a "connection selector":
To indicate the connection to use for authentication, use one of the following connection selectors:
connection
,organization
, orprovider
.These connection selectors are mutually exclusive, and exactly one must be provided.
A naive implementation might look something like this:
#[derive(Debug)]
pub struct GetAuthorizationUrlParams<'a> {
// ...
/// Initiate SSO for the connection with the specified ID.
pub connection: Option<&'a ConnectionId>,
/// Initiate SSO for the organization with the specified ID.
pub organization: Option<&'a OrganizationId>,
/// Initiate SSO for the specified OAuth provider.
pub provider: Option<&'a Provider>,
}
However, this design doesn't capture the mutual exclusivity of the options. We could provide none of them, all of them, or some other invalid combination.
We can remedy this by using Rust's enums, which allow us to model the connection selector as a sum type:
/// The selector to use to determine which connection to use for SSO.
#[derive(Debug)]
pub enum ConnectionSelector<'a> {
/// Initiate SSO for the connection with the specified ID.
Connection(&'a ConnectionId),
/// Initiate SSO for the organization with the specified ID.
Organization(&'a OrganizationId),
/// Initiate SSO for the specified OAuth provider.
Provider(&'a Provider),
}
#[derive(Debug)]
pub struct GetAuthorizationUrlParams<'a> {
// ...
/// The connection selector to use to initiate SSO.
pub connection_selector: ConnectionSelector<'a>,
}
The ConnectionSelector
type now has the correct semantics: we can only pass one connection selector at a time.
In practice, this would look something like this:
let authorization_url = workos
.sso()
.get_authorization_url(&GetAuthorizationUrlParams {
client_id: &ClientId::from("client_123456789"),
redirect_uri: "https://your-app.com/callback",
connection_selector: ConnectionSelector::Connection(&ConnectionId::from(
"conn_01E4ZCR3C56J083X43JQXF3JK5",
)),
state: None,
})?;
Gracefully handle new enum variants
The WorkOS API returns enums in a number of spots. In statically-typed languages, like Rust, we model the set of possible values within the type system.
Here's what that looks like for a ConnectionType
:
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConnectionType {
/// AD FS SAML.
///
/// [WorkOS Docs: Integration Guide](https://workos.com/docs/integrations/adfs-saml)
#[serde(rename = "ADFSSAML")]
AdFsSaml,
/// ADP OpenID Connect (OIDC).
///
/// [WorkOS Docs: Integration Guide](https://workos.com/docs/integrations/adp-oidc)
#[serde(rename = "ADPOIDC")]
AdpOidc,
// ...
}
This comes with the benefit of being able to see exactly which types of connections are available, but unfortunately there is a tradeoff: when new connection types are added within WorkOS upstream (which we do regularly) our ConnectionType
enum won't support them.
So what do we do? Make everything String
-ly typed?
Thankfully, there's a better solution; one which can give us the benefits of static types while still allowing us to deal with new variants.
Introducing the KnownOrUnknown
type:
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum KnownOrUnknown<K, U> {
/// A known value.
Known(K),
/// An unknown value.
Unknown(U),
}
This type allows us to represent a value that is either one of two possible types. For example, with a Connection
we can say that the type of the connection is either a ConnectionType
that we know about, or some unknown String
:
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Connection {
// ...
/// The type of the connection.
#[serde(rename = "connection_type")]
pub r#type: KnownOrUnknown<ConnectionType, String>,
}
If we were to get back "ADFSSAML"
from the API, we would end up with a KnownOrUnknown::Known(ConnectionType::AdFsSaml)
. Likewise, if got back some new type like "BrandNewType"
we would end up with a KnownOrUnknown::Unknown("BrandNewType")
.
Closing Thoughts
The first release of the WorkOS Rust SDK is now available on crates.io, with the repository being open-sourced soon.
We're still finishing up the final details for a full release, like adding some example applications and code snippets to the WorkOS Docs.
I'm very pleased with how the SDK has turned out and hope that it makes some fellow Rustaceans feel that extra spark of joy while integrating with WorkOS.
This was just one of the many projects that were completed during this Hack Week and I'm sure we'll be sharing more of them soon.