Skip to content

Commit

Permalink
Generate better schema types (#341)
Browse files Browse the repository at this point in the history
## Type of change
```
- [ ] Bug fix
- [ ] New feature development
- [x] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
```

## Objective
The `quicktype` schema generation seems to have trouble with types in
schema files being referenced from other types in separate schema files,
which created duplicate classes, with a lot of strange names.

Instead of generating one individual file for each response, we create a
big struct with all the options. This seems to solve the issue of
references between schema files, and so no duplicate classes are
created.

Note that the way that the file is loaded in `quicktype` is different.
By default, `quicktype` generates JSON conversion helper functions only
for the root type, which would be the big struct `SchemaTypes`. By
importing the file with `#/definitions/` appended, we only bring the sub
schemas as root types, and ignore `SchemaTypes`, which we don't care
about.

Examples of the schema changes, using C# as an example:
- `ProjectsResponse` previously contained an array of `DatumElement`,
which were identical to `ProjectResponse`. Now it properly contains an
array of `ProjectResponse`. This is also applicable to `SecretsResponse`
which contained an array of `DatumClass`
- Previously it would generate multiple variants for `Argon2ID`, named:
`FluffyArgon2Id` and `PurpleArgon2Id`, now there is only one `Argon2Id`.
This is applicable to the two factor response types, which change in the
same way

This is working correctly with the Java, Go, C++, PHP and Ruby bindings.
  • Loading branch information
dani-garcia authored Dec 4, 2023
1 parent 7fc3b84 commit f684cc9
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 44 deletions.
60 changes: 30 additions & 30 deletions crates/sdk-schemas/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use std::{fs::File, io::Write};

use anyhow::Result;
use itertools::Itertools;
use schemars::{schema::RootSchema, schema_for};
use schemars::{schema::RootSchema, schema_for, JsonSchema};

/// Creates a json schema file for any type passed in using Schemars. The filename and path of the generated
/// schema file is derived from the namespace passed into the macro or supplied as the first argument.
Expand Down Expand Up @@ -45,6 +44,8 @@ use schemars::{schema::RootSchema, schema_for};
/// will generate `Response.json` at `{{pwd}}/path/to/folder/Response.json`
macro_rules! write_schema_for {
($type:ty) => {
use itertools::Itertools;

let schema = schema_for!($type);

let type_name = stringify!($type);
Expand All @@ -65,12 +66,6 @@ macro_rules! write_schema_for {
};
}

macro_rules! write_schema_for_response {
( $($type:ty),+ $(,)? ) => {
$( write_schema_for!("response", bitwarden_json::response::Response<$type>); )+
};
}

fn write_schema(schema: RootSchema, dir_path: String, type_name: String) -> Result<()> {
let file_name = type_name
.split("::")
Expand All @@ -88,34 +83,39 @@ fn write_schema(schema: RootSchema, dir_path: String, type_name: String) -> Resu
Ok(())
}

fn main() -> Result<()> {
use bitwarden_json::response::Response;

#[allow(dead_code)]
#[derive(JsonSchema)]
struct SchemaTypes {
// Input types for new Client
write_schema_for!(bitwarden::client::client_settings::ClientSettings);
client_settings: bitwarden::client::client_settings::ClientSettings,

// Input types for Client::run_command
write_schema_for!(bitwarden_json::command::Command);
input_command: bitwarden_json::command::Command,

// Output types for Client::run_command
// Only add structs which are direct results of SDK commands.
write_schema_for_response! {
bitwarden::auth::login::ApiKeyLoginResponse,
bitwarden::auth::login::PasswordLoginResponse,
bitwarden::auth::login::AccessTokenLoginResponse,
bitwarden::secrets_manager::secrets::SecretIdentifiersResponse,
bitwarden::secrets_manager::secrets::SecretResponse,
bitwarden::secrets_manager::secrets::SecretsResponse,
bitwarden::secrets_manager::secrets::SecretsDeleteResponse,
bitwarden::secrets_manager::projects::ProjectResponse,
bitwarden::secrets_manager::projects::ProjectsResponse,
bitwarden::secrets_manager::projects::ProjectsDeleteResponse,
};
api_key_login: Response<bitwarden::auth::login::ApiKeyLoginResponse>,
password_login: Response<bitwarden::auth::login::PasswordLoginResponse>,
access_token_login: Response<bitwarden::auth::login::AccessTokenLoginResponse>,
secret_identifiers: Response<bitwarden::secrets_manager::secrets::SecretIdentifiersResponse>,
secret: Response<bitwarden::secrets_manager::secrets::SecretResponse>,
secrets: Response<bitwarden::secrets_manager::secrets::SecretsResponse>,
secrets_delete: Response<bitwarden::secrets_manager::secrets::SecretsDeleteResponse>,
project: Response<bitwarden::secrets_manager::projects::ProjectResponse>,
projects: Response<bitwarden::secrets_manager::projects::ProjectsResponse>,
projects_delete: Response<bitwarden::secrets_manager::projects::ProjectsDeleteResponse>,

// Same as above, but for the internal feature
#[cfg(feature = "internal")]
write_schema_for_response! {
bitwarden::platform::FingerprintResponse,
bitwarden::platform::SyncResponse,
bitwarden::platform::UserApiKeyResponse,
};
fingerprint: Response<bitwarden::platform::FingerprintResponse>,
#[cfg(feature = "internal")]
sync: Response<bitwarden::platform::SyncResponse>,
#[cfg(feature = "internal")]
user_api_key: Response<bitwarden::platform::UserApiKeyResponse>,
}

fn main() -> Result<()> {
write_schema_for!("schema_types", SchemaTypes);

#[cfg(feature = "internal")]
write_schema_for!(bitwarden_uniffi::docs::DocRef);
Expand Down
22 changes: 8 additions & 14 deletions support/scripts/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,16 @@ async function* walk(dir: string): AsyncIterable<string> {

async function main() {
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());

const filenames: string[] = [];
for await (const p of walk("./support/schemas")) {
filenames.push(p);
}

filenames.sort();

for (const f of filenames) {
const buffer = fs.readFileSync(f);
const relative = path.relative(path.join(process.cwd(), "support/schemas"), f);
await schemaInput.addSource({ name: relative, schema: buffer.toString() });
}

const inputData = new InputData();
inputData.addInput(schemaInput);
inputData.addSource(
"schema",
{
name: "SchemaTypes",
uris: ["support/schemas/schema_types/SchemaTypes.json#/definitions/"],
},
() => new JSONSchemaInput(new FetchingJSONSchemaStore()),
);

const ts = await quicktype({
inputData,
Expand Down

0 comments on commit f684cc9

Please sign in to comment.