Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop OpenAPI Export for API Definition #1178 #1202

Closed
wants to merge 14 commits into from

Conversation

zelosleone
Copy link

@zelosleone zelosleone commented Dec 18, 2024

API Definition OpenAPI Export & Swagger UI Integration

Key Implementations

OpenAPI Export

  • Added conversion logic to transform Golem API Definitions into OpenAPI Spec format
  • Implemented type mapping between Golem types and OpenAPI schemas
  • Preserved request/response types, security schemes, and CORS configurations

Swagger UI Integration

  • Created new SwaggerGenerator to manage Swagger UI assets and configurations
  • Added SwaggerUI binding type for API routes
  • Implemented automatic Swagger UI generation and file serving

Testing

  • Added comprehensive test coverage including:
    • Unit tests for OpenAPI conversion
    • Integration tests validating Swagger UI serving
    • End-to-end tests verifying generated client usage

Technical Details

// New SwaggerUI binding support
impl SwaggerGenerator {
    pub fn new<P: AsRef<Path>>(output_dir: P) -> Self { ... }
    pub async fn generate(&self, api_id: &str, spec_path: &str) -> Result<()> { ... }
    pub async fn clean(&self, api_id: &str) -> Result<()> { ... }
}

// OpenAPI conversion & validation 
#[tokio::test]
async fn test_openapi_export_flow() {
    let api = create_test_api().await;
    let spec = OpenAPIConverter::convert(&api);
    assert!(validate_openapi(&spec).is_ok());
}

Key Features

  • Full OpenAPI 3.0 spec generation preserving all API features
  • Automated Swagger UI integration
  • Support for security schemes and CORS
  • Client library generation compatibility

CI Configuration Updates:

  • This should be ignored, i mistakenly thought i can use the same thing as my local in github without noticing it goes over disk space of github. I deleted the file from commit but please make sure to not to accept that part.

Dependency Updates:

  • Added new dependencies heck and validator and updated the wasmtime dependencies to version 21.0.1 in Cargo.toml. [1] [2]
  • Updated golem-wit dependency version to 1.1.0 in golem-worker-executor-base/Cargo.toml.

Code Refactoring:

  • Moved the ParsedFunctionSite and ParsedFunctionReference enums to a new location in golem-rib/src/function_name.rs and added a new ParsedFunctionName struct. [1] [2] [3] [4] [5] [6]
  • Added a clone method to init_tracing call in golem-cli/src/lib.rs.
  • Added async function permits in various methods of DurableWorkerCtx in golem-worker-executor-base/src/durable_host/blobstore/container.rs. [1] [2] [3] [4] [5] [6] [7]

This implementation provides seamless integration between Golem's API definitions and standard OpenAPI tooling while maintaining all the power of Golem's type system and binding features.

Docs Pull Request: golemcloud/docs#105

/claim #1178

Honestly, all of these already maintaned openapiv3 crates are extremely problematic. Let alone documentation, they dont even have the correct forked customs so its extremely hard to build with just them. It might be better to use our custom openapi3 types and use them as validation only.
@zelosleone
Copy link
Author

zelosleone commented Dec 20, 2024

The code has been updated to incorporate feedback from @jdegoes, utilizing a custom OpenAPIv3 implementation and glademiller's crate for validation. Further review and feedback are appreciated.

- Made sure to use newest component_name
- Added security documentation
- Used TryFrom
- Updated the redis
@zelosleone
Copy link
Author

Pushed the commit according to latest reviews.

params.extend(Self::extract_header_parameters(route));
if params.is_empty() { None } else { Some(params) }
},
request_body: Self::create_request_body(route),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the "main" binding type whose path parameters, query parameters, request body, and response body, are all known and can be encoded precisely through conversion of Rib typing information.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is correct. The BindingType::Default handles comprehensive parameter binding and type conversion. Specially:

  • Extracts and converts path parameters from the route path
  • Handles query parameters from the route definition
  • Processes header parameters
  • Converts request/response bodies using the Rib type system
  • All parameter types are precisely encoded through the OpenAPI converter, maintaining type safety from Golem's internal types to the OpenAPI spec.

Based on jdegoes's comments, could be improved further by adding more error documentation if needed.
@zelosleone
Copy link
Author

Added more detailed documentation.

r#in: ParameterLocation::Path,
description: None,
required: Some(true),
schema: if info.key_name.ends_with("_id") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For path parameters, Rib has inferred types, which we should be using to generate the schema.

In general, we will need, in a separate module (so it's easy to test), a way to convert a Rib type into an OpenAPI schema. Then we will use that for the path parameters, query variables, and request / response headers & bodies.

if route.path.contains("/workers") && route.method == HttpMethod::Get {
params.push(
Parameter {
name: "filter".to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment on Rib type conversion required here.

}
);
}
if route.path.contains("/invoke-and-await") || route.path.contains("/invoke") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of this special-case logic here? Also, I am not sure what this is trying to do because in general, we should be generically converting the user-defined API into OpenAPI (not our own REST API that we expose for workers). Perhaps this is arising from the reuse of the same set of functions across all binding types--if so we should use different functions for different binding types because there's no way to unify them at this level.

}
);
}
if route.path.contains("/api/definitions") && route.method == HttpMethod::Get {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, all this special-case logic doesn't begin here. We want to generate OpenAPI for user-defined APIs (which are defined with file server, swagger, and default / worker binding types), not for Golem REST APIs.


fn extract_header_parameters(route: &Route) -> Vec<Parameter> {
let mut params = Vec::new();
if route.path.contains("/invoke-and-await") || route.path.contains("/invoke") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am convinced all this special-case logic around the Golem REST APIs is not correct and not satisfying the requirements of the ticket, which are to convert the user-defined API into OpenAPI schema.

params
}

fn create_request_body(route: &Route) -> Option<RequestBody> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation should be re-evaluated in light of the above comments.

Essentially, what you need to be doing here is looking at the inferred query, path, request, and response types (which is something you get from the Rib compiler metadata associated with worker binding type), and then generating OpenAPI for them.

That's only for worker binding type. For file server, it's going to be much simpler, and for swagger UI binding type, it will be even simpler. The most complex type is when a user has defined a route with Rib.

@jdegoes
Copy link
Contributor

jdegoes commented Dec 21, 2024

@zelosleone I stopped reviewing for now because I discovered the core of the problem is not solved, which is: when the user builds a custom API, using Rib (worker binding), file server, and of course the new Swagger UI binding types for different routes, an OpenAPI Schema needs to be generated that precisely describes how to interact with this API (including CORS and Security, and having full information on request and response, including headers, bodies, and query and path variables). As far as I can tell, the essential logic to go from a Rib inferred type to some element of OpenAPI schema is not present, which means this solution does not satisfy the requirements. However, you overall have the right architecture and are looking in the right places, so I look forward to taking another look when you have a chance to address these issues. Please re-open when re-architected!

@jdegoes jdegoes closed this Dec 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants