diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index c6bba59..fedd2cd 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +.idea \ No newline at end of file diff --git a/main.tsp b/main.tsp deleted file mode 100644 index e728973..0000000 --- a/main.tsp +++ /dev/null @@ -1,1342 +0,0 @@ -import "@typespec/http"; -import "@typespec/openapi"; -import "@typespec/openapi3"; - -using Http; -using OpenAPI; - -/** - * Create beautiful product and API documentation with our developer friendly platform. - */ -@service({ - title: "ReadMe API 🦉", -}) -@info({ - version: "4.355.0", - contact: { - name: "API Support", - url: "https://docs.readme.com/main/docs/need-more-support", - email: "support@readme.io", - }, -}) -namespace ReadMeAPI; - -model baseError { - /** - * An error code unique to the error received. - */ - error?: string; - - /** - * The reason why the error occured. - */ - message?: string; - - /** - * A helpful suggestion for how to alleviate the error. - */ - suggestion?: string; - - /** - * A [ReadMe Metrics](https://readme.com/metrics/) log URL where you can see more information the request that you made. If we have metrics URLs unavailable for your request, this URL will be a URL to our API Reference. - */ - @format("url") docs?: string; - - /** - * Information on where you can receive additional assistance from our wonderful support team. - */ - help?: string; - - /** - * A short poem we wrote you about your error. - */ - poem?: string[]; -} - -model apply { - /** - * Your full name - */ - @minLength(1) name: string = "Your Name"; - - /** - * A valid email we can reach you at. - */ - email: string = "you@example.com"; - - /** - * The job you're looking to apply for (https://readme.com/careers). - */ - job: - | "Front End Engineer" - | "Full Stack Engineer" - | "Head of Product" - | "Head of Solutions Engineering" - | "Product Designer" = "Front End Engineer"; - - /** - * Learn more at https://lgbtlifecenter.org/pronouns/ - */ - pronouns?: string; - - /** - * What have you been up to the past few years? - */ - @format("url") linkedin?: string; - - /** - * Or Bitbucket, Gitlab or anywhere else your code is hosted! - */ - @format("url") github?: string; - - /** - * What should we know about you? - */ - @format("blob") coverLetter?: string; - - /** - * Want to play with the API but not actually apply? Set this to true. - */ - dontReallyApply?: boolean; -} - -model category { - /** - * A short title for the category. This is what will show in the sidebar. - */ - title?: string; - - /** - * A category can be part of your reference or guide documentation, which is determined by this field. - */ - type?: "reference" | "guide" = "guide"; -} - -model changelog { - /** - * Title of the changelog. - */ - title: string; - - type?: "" | "added" | "fixed" | "improved" | "deprecated" | "removed"; - - /** - * Body content of the changelog. - */ - body: string; - - /** - * Visibility of the changelog. - */ - hidden?: boolean = true; -} - -model condensedProjectData { - name?: string; - subdomain?: string; - jwtSecret?: string; - - /** - * The base URL for the project. If the project is not running under a custom domain, it will be `https://projectSubdomain.readme.io`, otherwise it can either be or `https://example.com` or, in the case of an enterprise child project `https://example.com/projectSubdomain`. - */ - @format("url") baseUrl?: string; - - plan?: string; -} - -model customPage { - /** - * Title of the custom page. - */ - title: string; - - /** - * Body formatted in Markdown (displayed by default). - */ - body?: string; - - /** - * Body formatted in HTML (sanitized, only displayed if `htmlmode` is **true**). - */ - html?: string; - - /** - * **true** if `html` should be displayed, **false** if `body` should be displayed. - */ - htmlmode?: boolean; - - /** - * Visibility of the custom page. - */ - hidden?: boolean = true; -} - -@oneOf -union docSchemaPost { - unknown, - unknown, -} - -model docSchemaPut { - /** - * Title of the page. - */ - title?: string; - - /** - * Type of the page. The available types all show up under the /docs/ URL path of your docs project (also known as the "guides" section). Can be "basic" (most common), "error" (page desribing an API error), or "link" (page that redirects to an external link). - */ - type?: "basic" | "error" | "link"; - - /** - * Body content of the page, formatted in [ReadMe-flavored Markdown](https://docs.readme.com/rdmd/docs). - */ - body?: string; - - /** - * Category ID of the page, which you can get through [the **Get all categories** endpoint](https://docs.readme.com/main/reference/getcategories). - */ - category?: string; - - /** - * Visibility of the page. - */ - hidden?: boolean; - - /** - * The position of the page in your project sidebar. - */ - order?: integer; - - /** - * The parent doc's ID, if the page is a subpage. - */ - parentDoc?: string; - - error?: { - code?: string; - }; - - /** - * The slug of the category this page is associated with. You can get this through [the **Get all categories** endpoint](https://docs.readme.com/main/reference/getcategories). This field is an alternative to the `category` field. - */ - categorySlug?: string; - - /** - * If this page is a subpage, this field will be the slug of the parent document. You can get this through https://docs.readme.com/main/reference/docs#getdoc. This field is an alternative to the `parentDoc` field. - */ - parentDocSlug?: string; -} - -model docSchemaResponse { - /** - * Title of the page. - */ - title?: string; - - /** - * Type of the page. The available types all show up under the /docs/ URL path of your docs project (also known as the "guides" section). Can be "basic" (most common), "error" (page desribing an API error), or "link" (page that redirects to an external link). - */ - type?: "basic" | "error" | "link"; - - /** - * Body content of the page, formatted in [ReadMe-flavored Markdown](https://docs.readme.com/rdmd/docs). - */ - body?: string; - - /** - * Category ID of the page, which you can get through [the **Get all categories** endpoint](https://docs.readme.com/main/reference/getcategories). - */ - category?: string; - - /** - * Visibility of the page. - */ - hidden?: boolean; - - /** - * The position of the page in your project sidebar. - */ - order?: integer; - - /** - * The parent doc's ID, if the page is a subpage. - */ - parentDoc?: string; - - error?: { - code?: string; - }; -} - -model version { - /** - * Semantic Version - */ - version: string; - - /** - * Dubbed name of version. - */ - codename?: string; - - /** - * Semantic Version to use as the base fork. - */ - from: string; - - /** - * Should this be the **main** version? - */ - is_stable?: boolean; - - is_beta?: boolean = true; - - /** - * Should this be publically accessible? - */ - is_hidden?: boolean; - - /** - * Should this be deprecated? Only allowed in PUT operations. - */ - is_deprecated?: boolean; -} - -model jobOpening { - /** - * A slugified version of the job opening title. - */ - slug?: string; - - /** - * The job opening position. - */ - title?: string; - - /** - * The description for this open position. This content is formatted as HTML. - */ - description?: string; - - /** - * A short pullquote for the open position. - */ - pullquote?: string; - - /** - * Where this position is located at. - */ - location?: string; - - /** - * The internal organization you'll be working in. - */ - department?: string; - - /** - * The place where you can apply for the position! - */ - @format("url") url?: string; -} - -model error_APIKEY_EMPTY { - ...baseError; - error?: string = "APIKEY_EMPTY"; -} - -model error_APIKEY_MISMATCH { - ...baseError; - error?: string = "APIKEY_MISMATCH"; -} - -model error_APIKEY_NOTFOUND { - ...baseError; - error?: string = "APIKEY_NOTFOUND"; -} - -model error_APPLY_INVALID_EMAIL { - ...baseError; - error?: string = "APPLY_INVALID_EMAIL"; -} - -model error_APPLY_INVALID_JOB { - ...baseError; - error?: string = "APPLY_INVALID_JOB"; -} - -model error_APPLY_INVALID_NAME { - ...baseError; - error?: string = "APPLY_INVALID_NAME"; -} - -model error_CATEGORY_INVALID { - ...baseError; - error?: string = "CATEGORY_INVALID"; -} - -model error_CATEGORY_NOTFOUND { - ...baseError; - error?: string = "CATEGORY_NOTFOUND"; -} - -model error_CHANGELOG_INVALID { - ...baseError; - error?: string = "CHANGELOG_INVALID"; -} - -model error_CHANGELOG_NOTFOUND { - ...baseError; - error?: string = "CHANGELOG_NOTFOUND"; -} - -model error_CUSTOMPAGE_INVALID { - ...baseError; - error?: string = "CUSTOMPAGE_INVALID"; -} - -model error_CUSTOMPAGE_NOTFOUND { - ...baseError; - error?: string = "CUSTOMPAGE_NOTFOUND"; -} - -model error_DOC_INVALID { - ...baseError; - error?: string = "DOC_INVALID"; -} - -model error_DOC_NOTFOUND { - ...baseError; - error?: string = "DOC_NOTFOUND"; -} - -model error_ENDPOINT_NOTFOUND { - ...baseError; - error?: string = "ENDPOINT_NOTFOUND"; -} - -model error_INTERNAL_ERROR { - ...baseError; - error?: string = "INTERNAL_ERROR"; -} - -model error_PROJECT_NEEDSSTAGING { - ...baseError; - error?: string = "PROJECT_NEEDSSTAGING"; -} - -model error_PROJECT_NOTFOUND { - ...baseError; - error?: string = "PROJECT_NOTFOUND"; -} - -model error_RATE_LIMITED { - ...baseError; - error?: string = "RATE_LIMITED"; -} - -model error_REGISTRY_INVALID { - ...baseError; - error?: string = "REGISTRY_INVALID"; -} - -model error_REGISTRY_NOTFOUND { - ...baseError; - error?: string = "REGISTRY_NOTFOUND"; -} - -model error_SPEC_FILE_EMPTY { - ...baseError; - error?: string = "SPEC_FILE_EMPTY"; -} - -model error_SPEC_ID_DUPLICATE { - ...baseError; - error?: string = "SPEC_ID_DUPLICATE"; -} - -model error_SPEC_ID_INVALID { - ...baseError; - error?: string = "SPEC_ID_INVALID"; -} - -model error_SPEC_INVALID { - ...baseError; - error?: string = "SPEC_INVALID"; -} - -model error_SPEC_INVALID_SCHEMA { - ...baseError; - error?: string = "SPEC_INVALID_SCHEMA"; -} - -model error_SPEC_NOTFOUND { - ...baseError; - error?: string = "SPEC_NOTFOUND"; -} - -model error_SPEC_TIMEOUT { - ...baseError; - error?: string = "SPEC_TIMEOUT"; -} - -model error_SPEC_VERSION_NOTFOUND { - ...baseError; - error?: string = "SPEC_VERSION_NOTFOUND"; -} - -model error_UNEXPECTED_ERROR { - ...baseError; - error?: string = "UNEXPECTED_ERROR"; -} - -model error_VERSION_CANT_DEMOTE_STABLE { - ...baseError; - error?: string = "VERSION_CANT_DEMOTE_STABLE"; -} - -model error_VERSION_CANT_REMOVE_STABLE { - ...baseError; - error?: string = "VERSION_CANT_REMOVE_STABLE"; -} - -model error_VERSION_DUPLICATE { - ...baseError; - error?: string = "VERSION_DUPLICATE"; -} - -model error_VERSION_EMPTY { - ...baseError; - error?: string = "VERSION_EMPTY"; -} - -model error_VERSION_FORK_EMPTY { - ...baseError; - error?: string = "VERSION_FORK_EMPTY"; -} - -model error_VERSION_FORK_NOTFOUND { - ...baseError; - error?: string = "VERSION_FORK_NOTFOUND"; -} - -model error_VERSION_INVALID { - ...baseError; - error?: string = "VERSION_INVALID"; -} - -model error_VERSION_NOTFOUND { - ...baseError; - error?: string = "VERSION_NOTFOUND"; -} - -/** - * Successfully retrieved API registry entry. - */ -model getAPIRegistry200ApplicationJsonResponse { - @statusCode statusCode: 200; - @bodyRoot body: {}; -} - -/** - * Successfully retrieved API specification metadata. - */ -model getAPISpecification200Response { - @statusCode statusCode: 200; -} - -/** - * The API specification was successfully uploaded. - */ -model uploadAPISpecification201Response { - @statusCode statusCode: 201; -} - -/** - * There was a validation error during upload. - */ -@error -model uploadAPISpecification400ApplicationJsonResponse { - @statusCode statusCode: 400; - @bodyRoot @oneOf body: - | error_SPEC_FILE_EMPTY - | error_SPEC_INVALID - | error_SPEC_INVALID_SCHEMA - | error_SPEC_VERSION_NOTFOUND; -} - -/** - * The API specification was deleted. - */ -model deleteAPISpecification204Response { - @statusCode statusCode: 204; -} - -/** - * The API specification was updated. - */ -model updateAPISpecification200Response { - @statusCode statusCode: 200; -} - -/** - * There was a validation error during upload. - */ -@error -model updateAPISpecification400ApplicationJsonResponse { - @statusCode statusCode: 400; - @bodyRoot @oneOf body: - | error_SPEC_FILE_EMPTY - | error_SPEC_ID_DUPLICATE - | error_SPEC_ID_INVALID - | error_SPEC_INVALID - | error_SPEC_INVALID_SCHEMA - | error_SPEC_VERSION_NOTFOUND; -} - -/** - * There is no API specification with that ID. - */ -@error -model updateAPISpecification404Response { - @statusCode statusCode: 404; -} - -/** - * All the roles that we're hiring for. - */ -model getOpenRoles200ApplicationJsonResponse { - @statusCode statusCode: 200; - @bodyRoot body: jobOpening[]; -} - -/** - * You did it! - */ -model applyToReadMe200Response { - @statusCode statusCode: 200; -} - -/** - * The list of categories. - */ -model getCategories200Response { - @statusCode statusCode: 200; -} - -/** - * The category has successfully been created. - */ -model createCategory201Response { - @statusCode statusCode: 201; -} - -/** - * The category was deleted. - */ -model deleteCategory204Response { - @statusCode statusCode: 204; -} - -/** - * The category exists and has been returned. - */ -model getCategory200Response { - @statusCode statusCode: 200; -} - -/** - * The category was successfully updated. - */ -model updateCategory200Response { - @statusCode statusCode: 200; -} - -/** - * The category exists and all of the docs have been returned. - */ -model getCategoryDocs200Response { - @statusCode statusCode: 200; -} - -/** - * The list of changelogs. - */ -model getChangelogs200Response { - @statusCode statusCode: 200; -} - -/** - * The changelog was successfully created. - */ -model createChangelog201Response { - @statusCode statusCode: 201; -} - -/** - * There was a validation error during creation. - */ -@error -model createChangelog400Response { - @statusCode statusCode: 400; -} - -/** - * The changelog was successfully updated. - */ -model deleteChangelog204Response { - @statusCode statusCode: 204; -} - -/** - * There is no changelog with that slug. - */ -@error -model deleteChangelog404Response { - @statusCode statusCode: 404; -} - -/** - * The changelog exists and has been returned. - */ -model getChangelog200Response { - @statusCode statusCode: 200; -} - -/** - * There is no changelog with that slug. - */ -@error -model getChangelog404Response { - @statusCode statusCode: 404; -} - -/** - * The changelog was successfully updated. - */ -model updateChangelog200Response { - @statusCode statusCode: 200; -} - -/** - * There was a validation error during update. - */ -@error -model updateChangelog400Response { - @statusCode statusCode: 400; -} - -/** - * There is no changelog with that slug. - */ -@error -model updateChangelog404Response { - @statusCode statusCode: 404; -} - -/** - * The list of custom pages. - */ -model getCustomPages200Response { - @statusCode statusCode: 200; -} - -/** - * The custom page was successfully created. - */ -model createCustomPage201Response { - @statusCode statusCode: 201; -} - -/** - * The custom page was successfully updated. - */ -model deleteCustomPage204Response { - @statusCode statusCode: 204; -} - -/** - * The custom page exists and has been returned. - */ -model getCustomPage200Response { - @statusCode statusCode: 200; -} - -/** - * The custom page was successfully updated. - */ -model updateCustomPage200Response { - @statusCode statusCode: 200; -} - -/** - * The doc was successfully updated. - */ -model deleteDoc204Response { - @statusCode statusCode: 204; -} - -/** - * The doc exists and has been returned. - */ -model getDoc200ApplicationJsonResponse { - @statusCode statusCode: 200; - @bodyRoot body: docSchemaResponse; -} - -/** - * The doc was successfully updated. - */ -model updateDoc200ApplicationJsonResponse { - @statusCode statusCode: 200; - @bodyRoot body: docSchemaResponse; -} - -/** - * The doc exists and has been returned. - */ -model getProductionDoc200ApplicationJsonResponse { - @statusCode statusCode: 200; - @bodyRoot body: docSchemaResponse; -} - -/** - * The doc was successfully created. - */ -model createDoc201ApplicationJsonResponse { - @statusCode statusCode: 201; - @bodyRoot body: docSchemaResponse; -} - -/** - * The search was successful and results were returned. - */ -model searchDocs200Response { - @statusCode statusCode: 200; -} - -/** - * Project data - */ -model getProject200ApplicationJsonResponse { - @statusCode statusCode: 200; - @bodyRoot body: condensedProjectData; -} - -/** - * OpenAPI Definition data - */ -model getAPISchema200ApplicationJsonResponse { - @statusCode statusCode: 200; - @bodyRoot body: {}; -} - -/** - * A list of versions. - */ -model getVersions200Response { - @statusCode statusCode: 200; -} - -/** - * The version was successfully created. - */ -model createVersion200Response { - @statusCode statusCode: 200; -} - -/** - * There was a validation error during creation. - */ -@error -model createVersion400ApplicationJsonResponse { - @statusCode statusCode: 400; - @bodyRoot @oneOf body: error_VERSION_EMPTY | error_VERSION_DUPLICATE | error_VERSION_FORK_EMPTY; -} - -/** - * The version was successfully deleted. - */ -model deleteVersion200Response { - @statusCode statusCode: 200; -} - -/** - * The version exists and has been returned. - */ -model getVersion200Response { - @statusCode statusCode: 200; -} - -/** - * The version was successfully updated. - */ -model updateVersion200Response { - @statusCode statusCode: 200; -} - -/** - * Get an API definition file that's been uploaded to ReadMe. - */ -@tag("API Registry") -@route("/api-registry/{uuid}") -@get -@summary("Retrieve an entry from the API Registry") -op getAPIRegistry( - /** - * An API Registry UUID. This can be found by navigating to your API Reference page and viewing code snippets for Node with the `api` library. - */ - @path uuid: string, -): getAPIRegistry200ApplicationJsonResponse; - -/** - * Get API specification metadata. - */ -@tag("API Specification") -@route("/api-specification") -@get -@summary("Get metadata") -op getAPISpecification( - ...Parameters.perPage, - ...Parameters.page, - ...Parameters.x_readme_version, -): getAPISpecification200Response; - -/** - * Upload an API specification to ReadMe. Or, to use a newer solution see https://docs.readme.com/main/docs/rdme. - */ -@tag("API Specification") -@route("/api-specification") -@post -@summary("Upload specification") -op uploadAPISpecification( - ...Parameters.x_readme_version, - @header contentType: "multipart/form-data", - @bodyRoot body: { - spec?: bytes; - }, -): uploadAPISpecification201Response | uploadAPISpecification400ApplicationJsonResponse; - -/** - * Delete an API specification in ReadMe. - */ -@tag("API Specification") -@route("/api-specification/{id}") -@delete -@summary("Delete specification") -op deleteAPISpecification( - /** - * ID of the API specification. The unique ID for each API can be found by navigating to your **API Definitions** page. - */ - @path id: string, -): deleteAPISpecification204Response; - -/** - * Update an API specification in ReadMe. - */ -@tag("API Specification") -@route("/api-specification/{id}") -@put -@summary("Update specification") -op updateAPISpecification( - /** - * ID of the API specification. The unique ID for each API can be found by navigating to your **API Definitions** page. - */ - @path id: string, - - @header contentType: "multipart/form-data", - @bodyRoot body: { - spec?: bytes; - }, -): updateAPISpecification200Response | updateAPISpecification400ApplicationJsonResponse | updateAPISpecification404Response; - -/** - * Returns all the roles we're hiring for at ReadMe! - */ -@tag("Apply to ReadMe") -@route("/apply") -@get -@summary("Get open roles") -op getOpenRoles(): getOpenRoles200ApplicationJsonResponse; - -/** - * This endpoint will let you apply to a job at ReadMe programatically, without having to go through our UI! - */ -@tag("Apply to ReadMe") -@route("/apply") -@post -@summary("Submit your application!") -op applyToReadMe(@bodyRoot body: apply): applyToReadMe200Response; - -/** - * Returns all the categories for a specified version. - */ -@tag("Categories") -@route("/categories") -@get -@summary("Get all categories") -op getCategories( - ...Parameters.x_readme_version, - ...Parameters.perPage, - ...Parameters.page, -): getCategories200Response; - -/** - * Create a new category inside of this project. - */ -@tag("Categories") -@route("/categories") -@post -@summary("Create category") -op createCategory( - ...Parameters.x_readme_version, - @bodyRoot body: unknown, -): createCategory201Response; - -/** - * Delete the category with this slug. - * >⚠️Heads Up! - * > This will also delete all of the docs within this category. - */ -@tag("Categories") -@route("/categories/{slug}") -@delete -@summary("Delete category") -op deleteCategory( - /** - * A URL-safe representation of the category title. Slugs must be all lowercase, and replace spaces with hyphens. For example, for the category "Getting Started", enter the slug "getting-started". - */ - @path slug: string, - - ...Parameters.x_readme_version, -): deleteCategory204Response; - -/** - * Returns the category with this slug. - */ -@tag("Categories") -@route("/categories/{slug}") -@get -@summary("Get category") -op getCategory( - /** - * A URL-safe representation of the category title. Slugs must be all lowercase, and replace spaces with hyphens. For example, for the category "Getting Started", enter the slug "getting-started". - */ - @path slug: string, - - ...Parameters.x_readme_version, -): getCategory200Response; - -/** - * Change the properties of a category. - */ -@tag("Categories") -@route("/categories/{slug}") -@put -@summary("Update category") -op updateCategory( - /** - * A URL-safe representation of the category title. Slugs must be all lowercase, and replace spaces with hyphens. For example, for the category "Getting Started", enter the slug "getting-started". - */ - @path slug: string, - - ...Parameters.x_readme_version, - @bodyRoot body: category, -): updateCategory200Response; - -/** - * Returns the docs and children docs within this category. - */ -@tag("Categories") -@route("/categories/{slug}/docs") -@get -@summary("Get docs for category") -op getCategoryDocs( - /** - * A URL-safe representation of the category title. Slugs must be all lowercase, and replace spaces with hyphens. For example, for the category "Getting Started", enter the slug "getting-started". - */ - @path slug: string, - - ...Parameters.x_readme_version, -): getCategoryDocs200Response; - -/** - * Returns a list of changelogs. - */ -@tag("Changelog") -@route("/changelogs") -@get -@summary("Get changelogs") -op getChangelogs(...Parameters.perPage, ...Parameters.page): getChangelogs200Response; - -/** - * Create a new changelog entry. - */ -@tag("Changelog") -@route("/changelogs") -@post -@summary("Create changelog") -op createChangelog( - @bodyRoot body: changelog, -): createChangelog201Response | createChangelog400Response; - -/** - * Delete the changelog with this slug. - */ -@tag("Changelog") -@route("/changelogs/{slug}") -@delete -@summary("Delete changelog") -op deleteChangelog( - /** - * A URL-safe representation of the changelog title. Slugs must be all lowercase, and replace spaces with hyphens. For example, for the changelog "Owlet Weekly Update", enter the slug "owlet-weekly-update". - */ - @path slug: string, -): deleteChangelog204Response | deleteChangelog404Response; - -/** - * Returns the changelog with this slug. - */ -@tag("Changelog") -@route("/changelogs/{slug}") -@get -@summary("Get changelog") -op getChangelog( - /** - * A URL-safe representation of the changelog title. Slugs must be all lowercase, and replace spaces with hyphens. For example, for the changelog "Owlet Update", enter the slug "owlet-update". - */ - @path slug: string, -): getChangelog200Response | getChangelog404Response; - -/** - * Update a changelog with this slug. - */ -@tag("Changelog") -@route("/changelogs/{slug}") -@put -@summary("Update changelog") -op updateChangelog( - /** - * A URL-safe representation of the changelog title. Slugs must be all lowercase, and replace spaces with hyphens. For example, for the changelog "Owlet Weekly Update", enter the slug "owlet-weekly-update". - */ - @path slug: string, - - @bodyRoot body: changelog, -): updateChangelog200Response | updateChangelog400Response | updateChangelog404Response; - -/** - * Returns a list of custom pages. - */ -@tag("Custom Pages") -@route("/custompages") -@get -@summary("Get custom pages") -op getCustomPages(...Parameters.perPage, ...Parameters.page): getCustomPages200Response; - -/** - * Create a new custom page inside of this project. - */ -@tag("Custom Pages") -@route("/custompages") -@post -@summary("Create custom page") -op createCustomPage(@bodyRoot body: customPage): createCustomPage201Response; - -/** - * Delete the custom page with this slug. - */ -@tag("Custom Pages") -@route("/custompages/{slug}") -@delete -@summary("Delete custom page") -op deleteCustomPage(...Parameters.slug): deleteCustomPage204Response; - -/** - * Returns the custom page with this slug. - */ -@tag("Custom Pages") -@route("/custompages/{slug}") -@get -@summary("Get custom page") -op getCustomPage(...Parameters.slug): getCustomPage200Response; - -/** - * Update a custom page with this slug. - */ -@tag("Custom Pages") -@route("/custompages/{slug}") -@put -@summary("Update custom page") -op updateCustomPage(...Parameters.slug, @bodyRoot body: customPage): updateCustomPage200Response; - -/** - * Delete the doc with this slug. - */ -@tag("Docs") -@route("/docs/{slug}") -@delete -@summary("Delete doc") -op deleteDoc(...Parameters.slug, ...Parameters.x_readme_version): deleteDoc204Response; - -/** - * Returns the doc with this slug. - */ -@tag("Docs") -@route("/docs/{slug}") -@get -@summary("Get doc") -op getDoc(...Parameters.slug, ...Parameters.x_readme_version): getDoc200ApplicationJsonResponse; - -/** - * Update a doc with this slug. - */ -@tag("Docs") -@route("/docs/{slug}") -@put -@summary("Update doc") -op updateDoc( - ...Parameters.slug, - ...Parameters.x_readme_version, - @bodyRoot body: docSchemaPut, -): updateDoc200ApplicationJsonResponse; - -/** - * This is intended for use by enterprise users with staging enabled. This endpoint will return the live version of your document, whereas the standard endpoint will always return staging. - */ -@tag("Docs") -@route("/docs/{slug}/production") -@get -@summary("Get production doc") -op getProductionDoc( - ...Parameters.slug, - ...Parameters.x_readme_version, -): getProductionDoc200ApplicationJsonResponse; - -/** - * Create a new doc inside of this project. - */ -@tag("Docs") -@route("/docs") -@post -@summary("Create doc") -op createDoc( - ...Parameters.x_readme_version, - @bodyRoot body: docSchemaPost, -): createDoc201ApplicationJsonResponse; - -/** - * Returns all docs that match the search. - */ -@tag("Docs") -@route("/docs/search") -@post -@summary("Search docs") -op searchDocs( - /** - * Search string to look for. - */ - @query(#{ explode: true }) search: string, - - ...Parameters.x_readme_version, -): searchDocs200Response; - -/** - * Returns project data for the API key. - */ -@tag("Projects") -@route("/") -@get -@summary("Get metadata about the current project") -op getProject(): getProject200ApplicationJsonResponse; - -/** - * Returns a copy of our OpenAPI Definition. - */ -@tag("API Specification") -@route("/schema") -@get -@summary("Get our OpenAPI Definition") -op getAPISchema(): getAPISchema200ApplicationJsonResponse; - -/** - * Retrieve a list of versions associated with a project API key. - */ -@tag("Version") -@route("/version") -@get -@summary("Get versions") -op getVersions(): getVersions200Response; - -/** - * Create a new version. - */ -@tag("Version") -@route("/version") -@post -@summary("Create version") -op createVersion( - @bodyRoot body: version, -): createVersion200Response | createVersion400ApplicationJsonResponse; - -/** - * Delete a version - */ -@tag("Version") -@route("/version/{versionId}") -@delete -@summary("Delete version") -op deleteVersion(...Parameters.versionId): deleteVersion200Response; - -/** - * Returns the version with this version ID. - */ -@tag("Version") -@route("/version/{versionId}") -@get -@summary("Get version") -op getVersion(...Parameters.versionId): getVersion200Response; - -/** - * Update an existing version. - */ -@tag("Version") -@route("/version/{versionId}") -@put -@summary("Update version") -op updateVersion(...Parameters.versionId, @bodyRoot body: version): updateVersion200Response; - -namespace Parameters { - model slug { - /** - * A URL-safe representation of the page title. Slugs must be all lowercase, and replace spaces with hyphens. For example, for the title "Getting Started", enter the slug "getting-started". - */ - @path slug: string; - } - model page { - /** - * Used to specify further pages (starts at 1). - */ - @minValue(1) @query(#{ explode: true }) page?: integer = 1; - } - model perPage { - /** - * Number of items to include in pagination (up to 100, defaults to 10). - */ - @minValue(1) - @maxValue(100) - @query(#{ explode: true }) - perPage?: integer = 10; - } - model x_readme_version { - /** - * Version number of your docs project, for example, v3.0. By default the main project version is used. To see all valid versions for your docs project call https://docs.readme.com/main/reference/version#getversions. - */ - @header x_readme_version?: string; - } - model versionId { - /** - * Semver identifier for the project version. For best results, use the formatted `version_clean` value listed in the response from the [Get Versions endpoint](/reference/getversions). - */ - @path versionId: string; - } -} diff --git a/package.json b/package.json index 9f1e690..addc65e 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "workspaces": [ "demo", "packages/generators/*", + "packages/providers/*", "packages/types/*", "packages/*" ], diff --git a/packages/.DS_Store b/packages/.DS_Store new file mode 100644 index 0000000..16bc7dd Binary files /dev/null and b/packages/.DS_Store differ diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 4bd7554..31619f9 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -1,30 +1,38 @@ -import type { GenerateFixturesOptions } from '@contractual/generators.fixtures'; import { Command } from 'commander'; import { generate, generateSpec } from './commands/generate.command.js'; -import type { GenerateClientOptions } from './commands/types.js'; +import type { GenerateContractsOptions } from './commands/types.js'; +import inquirer from 'inquirer'; const program = new Command(); -program.name('contractual').description('A sample CLI tool').version('1.0.0'); +program.name('contractual').description('A sample CLI tool'); const generateCommand = new Command('generate').description('Generate resources'); generateCommand - .command('client') - .description('Generate a client based on the provided OpenAPI file') - .requiredOption('--openapi ', 'Path to the OpenAPI file') + .command('contract') + .description('Generate a contract based on the provided OpenAPI file') + .option('--version ', 'Path to the OpenAPI file') .option('--output ', 'Optional output directory') - .action((options: GenerateClientOptions) => { - return generate('Client', options); + .action((options: GenerateContractsOptions) => { + return generate('Contract', options); }); -generateCommand - .command('fixtures') - .description('Generate fixtures') - .requiredOption('--path ', 'Optional target directory') - .option('--output ', 'Optional output directory') - .action((options: GenerateFixturesOptions) => { - return generate('Fixtures', options); - }); +// generateCommand +// .command('fixtures') +// .description('Generate fixtures') +// .requiredOption('--path ', 'Optional target directory') +// .option('--output ', 'Optional output directory') +// .action((options: GenerateFixturesOptions) => { +// return generate('Fixtures', options); +// }); + +interface InitCommandAnswers { + version: string; + folder: string; + monorepo: boolean; + packageManager: 'npm' | 'pnpm' | 'yarn'; + installDependencies: boolean; +} generateCommand .command('spec') @@ -33,10 +41,45 @@ generateCommand await generateSpec(); }); -program.addCommand(generateCommand); +const initCommand = new Command('init') + .description('Initialize a new Contractual project') + .action(async () => { + const answers: InitCommandAnswers = await inquirer.prompt([ + { + type: 'input', + name: 'version', + message: 'What is the initial version of the API?', + default: 'v1.0.0', + }, + { + type: 'input', + name: 'folder', + message: 'Where to store the contractual folder?', + default: 'root', + }, + { + type: 'confirm', + name: 'monorepo', + message: 'Are you using a monorepo?', + default: false, + }, + { + type: 'list', + name: 'packageManager', + message: 'What package manager do you use?', + choices: ['npm', 'pnpm', 'yarn'], + default: 'npm', + }, + { + type: 'confirm', + name: 'installDependencies', + message: 'Is it okay to install dependencies?', + default: true, + }, + ]); + }); -program.command('snapshot').description('Take a snapshot').action(() => { - console.log('Snapshot taken'); -}); +program.addCommand(generateCommand); +program.addCommand(initCommand); program.parse(process.argv); diff --git a/packages/cli/src/commands/generate.command.ts b/packages/cli/src/commands/generate.command.ts index ccc07c0..9623f78 100644 --- a/packages/cli/src/commands/generate.command.ts +++ b/packages/cli/src/commands/generate.command.ts @@ -1,18 +1,18 @@ -import { transformOpenApiFile } from '@contractual/generators.client'; +import { generateContract } from '@contractual/generators.contract'; import { generateFixtures, type GenerateFixturesOptions } from '@contractual/generators.fixtures'; -import { generateSpecification } from '@contractual/generators.openapi'; -import { type GenerateClientOptions, Target } from './types.js'; +import { generateSpecification } from '@contractual/generators.spec'; +import { type GenerateContractsOptions, Target } from './types.js'; export async function generate( target: TTarget, - options: TTarget extends 'Client' - ? GenerateClientOptions + options: TTarget extends 'Contract' + ? GenerateContractsOptions : TTarget extends 'Fixtures' ? GenerateFixturesOptions : never ) { - if (target === Target.Client) { - return transformOpenApiFile((options as GenerateClientOptions).openapi); + if (target === Target.Contract) { + return generateContract(); } if (target === Target.Fixtures) { diff --git a/packages/cli/src/commands/types.ts b/packages/cli/src/commands/types.ts index ad3d78d..2928308 100644 --- a/packages/cli/src/commands/types.ts +++ b/packages/cli/src/commands/types.ts @@ -1,11 +1,11 @@ export const Target = { - Client: 'Client', + Contract: 'Contracts', Fixtures: 'Fixtures', }; export type Target = (typeof Target)[keyof typeof Target]; -export interface GenerateClientOptions { - openapi: string; +export interface GenerateContractsOptions { + openapi?: string; output?: string; } diff --git a/packages/contract/.DS_Store b/packages/contract/.DS_Store new file mode 100644 index 0000000..2af1a89 Binary files /dev/null and b/packages/contract/.DS_Store differ diff --git a/packages/client/.eslintrc b/packages/contract/.eslintrc similarity index 100% rename from packages/client/.eslintrc rename to packages/contract/.eslintrc diff --git a/packages/client/.gitignore b/packages/contract/.gitignore similarity index 100% rename from packages/client/.gitignore rename to packages/contract/.gitignore diff --git a/packages/client/client/index.d.ts b/packages/contract/contract/index.d.ts similarity index 100% rename from packages/client/client/index.d.ts rename to packages/contract/contract/index.d.ts diff --git a/packages/client/client/index.js b/packages/contract/contract/index.js similarity index 100% rename from packages/client/client/index.js rename to packages/contract/contract/index.js diff --git a/packages/client/client/index.ts b/packages/contract/contract/index.ts similarity index 98% rename from packages/client/client/index.ts rename to packages/contract/contract/index.ts index 2533a5e..5190c88 100644 --- a/packages/client/client/index.ts +++ b/packages/contract/contract/index.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { AppRouter, initContract } from '@ts-rest/core'; -import { initClient } from '@ts-rest/core'; +import type { AppRouter } from '@ts-rest/core'; +import { initContract } from '@ts-rest/core'; export const baseError = z .object({ @@ -133,7 +133,7 @@ export const error_VERSION_CANT_REMOVE_STABLE = baseError.and( z.object({ error: z.string() }).partial() ); -export const appRouter = { +export const apiRouter = { getProject: { method: 'GET' as const, path: '/', @@ -711,9 +711,6 @@ export const ApiOperations = { 'get version': 'getVersion', 'update version': 'updateVersion', 'delete version': 'deleteVersion', -} satisfies Record; - -const clienter = initClient(appRouter, { - baseUrl: '', -}); +} satisfies Record; +export const contract = initContract().router(apiRouter); diff --git a/packages/client/index.ts b/packages/contract/index.ts similarity index 100% rename from packages/client/index.ts rename to packages/contract/index.ts diff --git a/packages/client/jest.config.ts b/packages/contract/jest.config.ts similarity index 100% rename from packages/client/jest.config.ts rename to packages/contract/jest.config.ts diff --git a/packages/client/package.json b/packages/contract/package.json similarity index 89% rename from packages/client/package.json rename to packages/contract/package.json index b56d484..ffa250e 100644 --- a/packages/client/package.json +++ b/packages/contract/package.json @@ -1,5 +1,5 @@ { - "name": "@contractual/client", + "name": "@contractual/contract", "private": false, "version": "0.0.0", "license": "Apache-2.0", @@ -10,9 +10,9 @@ "import": "./dist/src/index.js", "require": "./dist/src/index.js" }, - "./client": { - "import": "./client/index.js", - "require": "./client/index.js" + "./contract": { + "import": "./contract/index.js", + "require": "./contract/index.js" } }, "repository": { @@ -50,7 +50,7 @@ "lint": "pnpm eslint '{src,test}/**/*.ts'" }, "files": [ - "client", + "contract", "generator", "dist", "README.md" diff --git a/packages/client/src/api-client.ts b/packages/contract/src/api-client.ts similarity index 90% rename from packages/client/src/api-client.ts rename to packages/contract/src/api-client.ts index 9128deb..cc93e24 100644 --- a/packages/client/src/api-client.ts +++ b/packages/contract/src/api-client.ts @@ -2,19 +2,19 @@ import type { InitClientReturn, ClientArgs } from '@ts-rest/core'; import { initClient } from '@ts-rest/core'; import type { AxiosError, AxiosResponse, Method } from 'axios'; import { isAxiosError } from 'axios'; -import { appRouter } from '../client/index.js'; +import { apiRouter } from '../contract/index.js'; import { createAxiosClient } from './axios-client.js'; export type ApiClient = ReturnType; -let apiClientInstance: InitClientReturn | null = null; +let apiClientInstance: InitClientReturn | null = null; export function getApiClient(options: { baseUrl: string; baseHeaders: Record | undefined; }) { if (!apiClientInstance) { - apiClientInstance = initClient(appRouter, { + apiClientInstance = initClient(apiRouter, { baseUrl: options.baseUrl, baseHeaders: options.baseHeaders, api: async ({ path, method, headers, body }) => { diff --git a/packages/client/src/axios-client.ts b/packages/contract/src/axios-client.ts similarity index 100% rename from packages/client/src/axios-client.ts rename to packages/contract/src/axios-client.ts diff --git a/packages/client/src/index.ts b/packages/contract/src/index.ts similarity index 100% rename from packages/client/src/index.ts rename to packages/contract/src/index.ts diff --git a/packages/client/src/types.ts b/packages/contract/src/types.ts similarity index 84% rename from packages/client/src/types.ts rename to packages/contract/src/types.ts index 6d22cf1..8e774f4 100644 --- a/packages/client/src/types.ts +++ b/packages/contract/src/types.ts @@ -1,4 +1,4 @@ -import type { ApiOperations } from '../client/index.js'; +import type { ApiOperations } from '../contract/index.js'; import type { ApiClient } from './api-client'; export type ApiOperationToClientMethod = diff --git a/packages/client/tsconfig.build.json b/packages/contract/tsconfig.build.json similarity index 90% rename from packages/client/tsconfig.build.json rename to packages/contract/tsconfig.build.json index 0a44d43..b01b7d6 100644 --- a/packages/client/tsconfig.build.json +++ b/packages/contract/tsconfig.build.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDirs": [ "src", - "client" + "contract" ], "baseUrl": ".", "outDir": "dist", @@ -11,7 +11,7 @@ }, "exclude": [ "index.ts", - "client", + "contract", "dist", "node_modules", "__test__/**/*", diff --git a/packages/client/tsconfig.json b/packages/contract/tsconfig.json similarity index 100% rename from packages/client/tsconfig.json rename to packages/contract/tsconfig.json diff --git a/packages/fixtures/config/output/create-version.fixtures.js b/packages/fixtures/config/output/create-version.fixtures.js new file mode 100644 index 0000000..37ea21c --- /dev/null +++ b/packages/fixtures/config/output/create-version.fixtures.js @@ -0,0 +1,24 @@ +import { registerFixtures } from '@contractual/fixtures'; +export default registerFixtures('create version', { + 'basic version': () => ({ + body: { + version: 'string', + codename: 'string', + from: 'string', + is_stable: true, + is_beta: true, + is_hidden: true, + is_deprecated: true, + }, + }), + 'rc version': ({ extend }) => { + return extend({ + 'with something else': () => ({ + body: { + version: 'string', + from: 'string', + }, + }), + }); + }, +}); diff --git a/packages/fixtures/config/output/delete-version.fixtures.js b/packages/fixtures/config/output/delete-version.fixtures.js new file mode 100644 index 0000000..6eea1db --- /dev/null +++ b/packages/fixtures/config/output/delete-version.fixtures.js @@ -0,0 +1,13 @@ +import { registerFixtures } from '@contractual/fixtures'; +export default registerFixtures('delete version', { + 'basic delete': () => ({ + params: { + versionId: 'string', + } + }), + 'complex': () => ({ + params: { + versionId: 'string', + } + }), +}); diff --git a/packages/fixtures/output/create-version.fixtures.js b/packages/fixtures/output/create-version.fixtures.js new file mode 100644 index 0000000..37ea21c --- /dev/null +++ b/packages/fixtures/output/create-version.fixtures.js @@ -0,0 +1,24 @@ +import { registerFixtures } from '@contractual/fixtures'; +export default registerFixtures('create version', { + 'basic version': () => ({ + body: { + version: 'string', + codename: 'string', + from: 'string', + is_stable: true, + is_beta: true, + is_hidden: true, + is_deprecated: true, + }, + }), + 'rc version': ({ extend }) => { + return extend({ + 'with something else': () => ({ + body: { + version: 'string', + from: 'string', + }, + }), + }); + }, +}); diff --git a/packages/fixtures/output/delete-version.fixtures.js b/packages/fixtures/output/delete-version.fixtures.js new file mode 100644 index 0000000..6eea1db --- /dev/null +++ b/packages/fixtures/output/delete-version.fixtures.js @@ -0,0 +1,13 @@ +import { registerFixtures } from '@contractual/fixtures'; +export default registerFixtures('delete version', { + 'basic delete': () => ({ + params: { + versionId: 'string', + } + }), + 'complex': () => ({ + params: { + versionId: 'string', + } + }), +}); diff --git a/packages/fixtures/src/regsiter-fixtures.ts b/packages/fixtures/src/regsiter-fixtures.ts index 9da9508..d8c2bbc 100644 --- a/packages/fixtures/src/regsiter-fixtures.ts +++ b/packages/fixtures/src/regsiter-fixtures.ts @@ -1,5 +1,5 @@ -import type { ApiClientInput, ApiOperationToClientMethod } from '@contractual/client'; -import type { ApiOperations } from '@contractual/client/client'; +import type { ApiClientInput, ApiOperationToClientMethod } from '@contractual/contract'; +import type { ApiOperations } from '@contractual/contract/contract'; import type { FixtureCallback, FixturesBuilder, diff --git a/packages/generators/client/.eslintrc b/packages/generators/contract/.eslintrc similarity index 100% rename from packages/generators/client/.eslintrc rename to packages/generators/contract/.eslintrc diff --git a/packages/generators/client/client.template.hbs b/packages/generators/contract/contract.template.hbs similarity index 86% rename from packages/generators/client/client.template.hbs rename to packages/generators/contract/contract.template.hbs index 00745a6..3421515 100644 --- a/packages/generators/client/client.template.hbs +++ b/packages/generators/contract/contract.template.hbs @@ -1,5 +1,6 @@ import { z } from 'zod'; import type { AppRouter } from '@ts-rest/core'; +import { initContract } from '@ts-rest/core'; {{#if imports}} {{#each imports}} @@ -13,7 +14,7 @@ import type { AppRouter } from '@ts-rest/core'; {{/each}} {{/if}} -export const appRouter = { +export const ApiContract = { {{#each endpoints}} {{alias}}: { method: '{{toUpperCase method}}' as const, @@ -49,4 +50,6 @@ export const ApiOperations = { {{#each endpoints}} '{{toPlainWords alias}}': '{{alias}}', {{/each}} -} satisfies Record; +} satisfies Record; + +export const contract = initContract().router(ApiContract); diff --git a/packages/generators/client/index.ts b/packages/generators/contract/index.ts similarity index 100% rename from packages/generators/client/index.ts rename to packages/generators/contract/index.ts diff --git a/packages/generators/client/jest.config.ts b/packages/generators/contract/jest.config.ts similarity index 100% rename from packages/generators/client/jest.config.ts rename to packages/generators/contract/jest.config.ts diff --git a/packages/generators/client/package.json b/packages/generators/contract/package.json similarity index 100% rename from packages/generators/client/package.json rename to packages/generators/contract/package.json diff --git a/packages/generators/client/src/generators.ts b/packages/generators/contract/src/generators.ts similarity index 89% rename from packages/generators/client/src/generators.ts rename to packages/generators/contract/src/generators.ts index 77c0c69..54c8cc4 100644 --- a/packages/generators/client/src/generators.ts +++ b/packages/generators/contract/src/generators.ts @@ -3,6 +3,7 @@ import handlebars from 'handlebars'; import * as path from 'node:path'; import { generateZodClientFromOpenAPI } from 'openapi-zod-client'; import type { OpenAPIObject } from 'openapi3-ts/oas30'; +import * as process from 'node:process'; const { create } = handlebars; @@ -53,13 +54,13 @@ function createHandlebars() { return instance; } -export const transformOpenApiFile = async (openapiFilePath: string) => { - const doc = await SwaggerParser.parse(path.resolve(openapiFilePath)); +export const generateContract = async () => { + const doc = await SwaggerParser.parse(path.resolve(process.cwd(), 'contractual', 'specs', 'openapi-v1.1.0.yaml')); const writeToPath = path.resolve( path.dirname(new URL(import.meta.url).pathname), '../../..', - 'client/client', + 'contract/contract', 'index.ts' ); @@ -69,7 +70,7 @@ export const transformOpenApiFile = async (openapiFilePath: string) => { templatePath: path.resolve( path.dirname(new URL(import.meta.url).pathname), '..', - 'client.template.hbs' + 'contract.template.hbs' ), prettierConfig: { tabWidth: 2, diff --git a/packages/generators/client/src/index.ts b/packages/generators/contract/src/index.ts similarity index 100% rename from packages/generators/client/src/index.ts rename to packages/generators/contract/src/index.ts diff --git a/packages/generators/client/tsconfig.build.json b/packages/generators/contract/tsconfig.build.json similarity index 100% rename from packages/generators/client/tsconfig.build.json rename to packages/generators/contract/tsconfig.build.json diff --git a/packages/generators/client/tsconfig.json b/packages/generators/contract/tsconfig.json similarity index 100% rename from packages/generators/client/tsconfig.json rename to packages/generators/contract/tsconfig.json diff --git a/packages/generators/fixtures/package.json b/packages/generators/fixtures/package.json index ba4db0d..9d758c4 100644 --- a/packages/generators/fixtures/package.json +++ b/packages/generators/fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@contractual/generators.fixtures", - "private": false, + "private": true, "version": "0.0.0", "license": "Apache-2.0", "type": "module", diff --git a/packages/generators/openapi/src/generator.ts b/packages/generators/openapi/src/generator.ts deleted file mode 100644 index a0962f8..0000000 --- a/packages/generators/openapi/src/generator.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { compile, NodeHost } from '@typespec/compiler'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import * as process from 'node:process'; - -export async function generateSpecification() { - // console.log( - // path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'tspconfig.yaml') - // ); - - const pathToSpec = path.resolve(process.cwd(), './contractual/spec.tsp'); - - if (!fs.existsSync(pathToSpec)) { - console.error('specification file not found', pathToSpec); - process.exit(1); - } - - console.log(await NodeHost.realpath(pathToSpec)); - - return compile( - { - ...NodeHost, - getExecutionRoot(): string { - return `${process.cwd()}/node_modules/@typespec/compiler`; - }, - }, - pathToSpec, - { - emit: ['@typespec/openapi3'], - additionalImports: ['@typespec/openapi', '@typespec/openapi3', '@typespec/http'], - outputDir: path.resolve(process.cwd(), './contractual/snapshots'), - ignoreDeprecated: true, - } - ) - .then(() => { - // console.log(program.diagnostics); - console.log('Specifi/**/cation generated'); - }) - .catch((error) => { - console.error(error); - }); -} diff --git a/packages/generators/openapi/.eslintrc b/packages/generators/spec/.eslintrc similarity index 100% rename from packages/generators/openapi/.eslintrc rename to packages/generators/spec/.eslintrc diff --git a/packages/generators/openapi/index.ts b/packages/generators/spec/index.ts similarity index 100% rename from packages/generators/openapi/index.ts rename to packages/generators/spec/index.ts diff --git a/packages/generators/openapi/jest.config.ts b/packages/generators/spec/jest.config.ts similarity index 100% rename from packages/generators/openapi/jest.config.ts rename to packages/generators/spec/jest.config.ts diff --git a/packages/generators/openapi/package.json b/packages/generators/spec/package.json similarity index 93% rename from packages/generators/openapi/package.json rename to packages/generators/spec/package.json index de5c4b2..6afee4c 100644 --- a/packages/generators/openapi/package.json +++ b/packages/generators/spec/package.json @@ -1,5 +1,5 @@ { - "name": "@contractual/generators.openapi", + "name": "@contractual/generators.spec", "private": false, "version": "0.0.0", "license": "Apache-2.0", @@ -58,7 +58,9 @@ "@typespec/openapi3": "^0.63.0", "@typespec/rest": "^0.63.1", "@typespec/versioning": "^0.63.0", - "openapi-diff": "^0.23.7" + "openapi-diff": "^0.23.7", + "semver": "^7.6.3", + "yaml": "^2.7.0" }, "peerDependencies": { "typescript": ">=5.x" diff --git a/packages/generators/spec/src/generator.ts b/packages/generators/spec/src/generator.ts new file mode 100644 index 0000000..3ed2784 --- /dev/null +++ b/packages/generators/spec/src/generator.ts @@ -0,0 +1,149 @@ +import { compile, logDiagnostics, NodeHost } from '@typespec/compiler'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as process from 'node:process'; +import openapiDiff from 'openapi-diff'; +import { parse, stringify } from 'yaml'; +import { inc } from 'semver'; + +async function initializePaths() { + const rootPath = path.resolve(process.cwd(), 'contractual'); + const configFilePath = path.resolve(rootPath, 'api-lock.yaml'); + const snapshotsPath = path.resolve(rootPath, 'specs'); + const currentPath = path.resolve(path.dirname(new URL(import.meta.url).pathname)); + const specPath = path.resolve(rootPath, 'api.tsp'); + const tempSpecPath = path.resolve(currentPath, '@typespec', 'openapi3', 'openapi.yaml'); + + return { rootPath, configFilePath, snapshotsPath, currentPath, specPath, tempSpecPath }; +} + +function checkFileExists(filePath: string, errorMessage: string): boolean { + if (!fs.existsSync(filePath)) { + console.error(errorMessage, filePath); + return false; + } + + return true; +} + +async function compileSpecification(specPath: string, outputPath: string) { + const program = await compile(NodeHost, specPath, { + emit: ['@typespec/openapi3'], + additionalImports: ['@typespec/openapi', '@typespec/openapi3', '@typespec/http'], + outputDir: outputPath, + ignoreDeprecated: true, + warningAsError: false, + }); + + if (program.hasError()) { + logDiagnostics( + program.diagnostics.filter(({ severity }) => severity === 'error'), + NodeHost.logSink + ); + + return null; + } + + return program; +} + +async function checkSpecificationDifferences( + tempSpecPath: string, + snapshotsPath: string, + version: string +) { + const diff = await openapiDiff.diffSpecs({ + destinationSpec: { + content: fs.readFileSync(tempSpecPath, 'utf-8'), + location: tempSpecPath, + format: 'openapi3', + }, + sourceSpec: { + content: fs.readFileSync(path.resolve(snapshotsPath, `openapi-v${version}.yaml`), 'utf-8'), + location: path.resolve(snapshotsPath, `openapi-v${version}.yaml`), + format: 'openapi3', + }, + }); + + if (diff.breakingDifferencesFound) { + console.error('Breaking differences found', diff.breakingDifferences); + return false; + } + + if (diff.nonBreakingDifferences.length === 0 && diff.unclassifiedDifferences.length === 0) { + console.log('No differences found'); + return null; + } + + return true; +} + +function updateVersionAndSnapshot( + configPath: string, + snapshotsPath: string, + tempSpecPath: string, + currentVersion: string +) { + const newVersion = inc(currentVersion, 'minor'); + const newConfigContent = stringify({ version: { latest: newVersion } }); + + fs.writeFileSync(configPath, newConfigContent); + fs.copyFileSync(tempSpecPath, path.resolve(snapshotsPath, `openapi-v${newVersion}.yaml`)); + console.log(`Updated to new version: ${newVersion}`); +} + +export async function generateSpecification() { + const paths = await initializePaths(); + + if (!checkFileExists(paths.rootPath, `'contractual' directory not found`)) { + return; + } + + if (!fs.existsSync(paths.snapshotsPath)) { + fs.mkdirSync(paths.snapshotsPath); + } + + if (!checkFileExists(paths.specPath, 'specification file not found')) { + process.exit(1); + } + + const program = await compileSpecification(paths.specPath, paths.currentPath); + + if (!program) { + return; + } + + if (!checkFileExists(paths.tempSpecPath, 'openapi.yaml not found')) { + return; + } + + const configContent = parse(fs.readFileSync(paths.configFilePath, 'utf-8')); + + const { + version: { latest }, + } = configContent; + + if (latest === '0.0.0') { + const destinationPath = path.resolve(paths.snapshotsPath, 'openapi-v1.0.0.yaml'); + fs.copyFileSync(paths.tempSpecPath, destinationPath); + console.log('Initial version created at:', destinationPath); + + const newConfigContent = stringify({ version: { latest: '1.0.0' } }); + + fs.writeFileSync(paths.configFilePath, newConfigContent); + + return; + } + + const differences = await checkSpecificationDifferences( + paths.tempSpecPath, + paths.snapshotsPath, + latest + ); + + if (differences === false || differences === null) { + return; + } + + updateVersionAndSnapshot(paths.configFilePath, paths.snapshotsPath, paths.tempSpecPath, latest); +} diff --git a/packages/generators/openapi/src/index.ts b/packages/generators/spec/src/index.ts similarity index 100% rename from packages/generators/openapi/src/index.ts rename to packages/generators/spec/src/index.ts diff --git a/packages/generators/openapi/tsconfig.build.json b/packages/generators/spec/tsconfig.build.json similarity index 100% rename from packages/generators/openapi/tsconfig.build.json rename to packages/generators/spec/tsconfig.build.json diff --git a/packages/generators/openapi/tsconfig.json b/packages/generators/spec/tsconfig.json similarity index 100% rename from packages/generators/openapi/tsconfig.json rename to packages/generators/spec/tsconfig.json diff --git a/packages/test/.eslintrc b/packages/providers/playwright/.eslintrc similarity index 100% rename from packages/test/.eslintrc rename to packages/providers/playwright/.eslintrc diff --git a/packages/providers/playwright/index.ts b/packages/providers/playwright/index.ts new file mode 100644 index 0000000..4df73b3 --- /dev/null +++ b/packages/providers/playwright/index.ts @@ -0,0 +1 @@ +export * from './src/index.js'; diff --git a/packages/test/jest.config.ts b/packages/providers/playwright/jest.config.ts similarity index 86% rename from packages/test/jest.config.ts rename to packages/providers/playwright/jest.config.ts index 75335b6..6b7ebaf 100644 --- a/packages/test/jest.config.ts +++ b/packages/providers/playwright/jest.config.ts @@ -1,5 +1,5 @@ import type { Config } from 'jest'; -import baseConfig from '../../jest.base.config'; +import baseConfig from '../../../jest.base.config.js'; const config: Config = { ...baseConfig(process.env.COVERAGE_DIR), diff --git a/packages/test/package.json b/packages/providers/playwright/package.json similarity index 100% rename from packages/test/package.json rename to packages/providers/playwright/package.json diff --git a/packages/test/src/index.ts b/packages/providers/playwright/src/index.ts similarity index 86% rename from packages/test/src/index.ts rename to packages/providers/playwright/src/index.ts index 14663c1..453f5d5 100644 --- a/packages/test/src/index.ts +++ b/packages/providers/playwright/src/index.ts @@ -1,7 +1,7 @@ -import type { appRouter } from '@contractual/client/client'; -import { type ApiClient, type ApiClientInput, getApiClient } from '@contractual/client'; -import { ApiOperations } from '@contractual/client/client'; -import { Fixtures } from '@contractual/fixtures/fixtures'; +import type { apiRouter } from '@contractual/contract/contract/index.js'; +import { type ApiClient, type ApiClientInput, getApiClient } from '@contractual/contract'; +import { ApiOperations } from '@contractual/contract/contract/index.js'; +import { Fixtures } from '@contractual/fixtures/fixtures/index.js'; import type { TestFunctionParams } from '@contractual/types.test'; import { test as playwrightTest } from '@playwright/test'; import type { ClientArgs, InitClientReturn } from '@ts-rest/core'; @@ -24,7 +24,7 @@ export function test( // Create the operation function const operationFn = ( clientMethod: (typeof ApiOperations)[TOperation], - client: InitClientReturn + client: InitClientReturn ) => Object.assign( (fixture: TFixture) => { diff --git a/packages/test/tsconfig.build.json b/packages/providers/playwright/tsconfig.build.json similarity index 70% rename from packages/test/tsconfig.build.json rename to packages/providers/playwright/tsconfig.build.json index efb66fa..aee34d4 100644 --- a/packages/test/tsconfig.build.json +++ b/packages/providers/playwright/tsconfig.build.json @@ -1,15 +1,15 @@ { - "extends": "../../tsconfig.build.json", + "extends": "../../../tsconfig.build.json", "compilerOptions": { "rootDir": "src", - "baseUrl": ".", + "baseUrl": "./", "outDir": "dist", "skipLibCheck": true }, "exclude": [ "dist", "index.ts", - "../client/index.ts", + "../contract/index.ts", "node_modules", "__test__", "**/*.spec.ts", diff --git a/packages/providers/playwright/tsconfig.json b/packages/providers/playwright/tsconfig.json new file mode 100644 index 0000000..b027bb4 --- /dev/null +++ b/packages/providers/playwright/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../tsconfig.json" +} \ No newline at end of file diff --git a/packages/test/index.ts b/packages/test/index.ts deleted file mode 100644 index 8420b10..0000000 --- a/packages/test/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './src'; diff --git a/packages/types/fixtures/index.d.ts b/packages/types/fixtures/index.d.ts index 0314033..0abdad8 100644 --- a/packages/types/fixtures/index.d.ts +++ b/packages/types/fixtures/index.d.ts @@ -1,5 +1,5 @@ -import type { ApiClientInput, ApiOperationToClientMethod } from '@contractual/client'; -import type { ApiOperations } from '@contractual/client/client'; +import type { ApiClientInput, ApiOperationToClientMethod } from '@contractual/contract'; +import type { ApiOperations } from '@contractual/contract/contract'; export type FixturesBuilder = Record>; export type FixtureCallback = (params: { extend: ExtendFunction; diff --git a/packages/types/fixtures/index.ts b/packages/types/fixtures/index.ts index 2d12c66..468d823 100644 --- a/packages/types/fixtures/index.ts +++ b/packages/types/fixtures/index.ts @@ -1,5 +1,5 @@ -import type { ApiClientInput, ApiOperationToClientMethod } from '@contractual/client'; -import type { ApiOperations } from '@contractual/client/client'; +import type { ApiClientInput, ApiOperationToClientMethod } from '@contractual/contract'; +import type { ApiOperations } from '@contractual/contract/contract'; export type FixturesBuilder = Record< string, diff --git a/packages/types/test/src/index.ts b/packages/types/test/src/index.ts index b76c58c..dfc76e7 100644 --- a/packages/types/test/src/index.ts +++ b/packages/types/test/src/index.ts @@ -1,6 +1,6 @@ -import type { ApiClient, ApiClientInput, ApiOperationToClientMethod } from '@contractual/client'; +import type { ApiClient, ApiClientInput, ApiOperationToClientMethod } from '@contractual/contract'; import type { Fixtures } from '@contractual/fixtures/fixtures'; -import type { ApiOperations } from '@contractual/client/client'; +import type { ApiOperations } from '@contractual/contract/contract'; export interface TestFunctionParams { operation(