From 978b2ebfb64a5b65a4bad1602b4b0496d48d6214 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Fri, 6 Sep 2024 17:26:41 -0400 Subject: [PATCH] fix(cli): Add deterministic file ordering (#4567) --- packages/cli/cli/versions.yml | 8 + .../mixed-file-directory.json | 6185 +++++++++++++++++ .../src/FernDefinitionDirectory.ts | 53 + .../src/FernDefnitionBuilder.ts | 13 +- .../__test__/FernDefinitionDirectory.test.ts | 71 + .../convertIrToFdrApi.test.ts.snap | 517 ++ pnpm-lock.yaml | 4 - .../.github/workflows/ci.yml | 69 + .../mixed-file-directory/.gitignore | 484 ++ .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 .../SeedMixedFileDirectory.Test.csproj | 26 + .../Core/CollectionItemSerializer.cs | 91 + .../SeedMixedFileDirectory/Core/Constants.cs | 7 + .../Core/DateTimeSerializer.cs | 22 + .../Core/JsonConfiguration.cs | 32 + .../Core/OneOfSerializer.cs | 69 + .../Core/Public/Version.cs | 6 + .../Core/StringEnumSerializer.cs | 53 + .../Organization/CreateOrganizationRequest.cs | 17 + .../Organization/Organization.cs | 23 + .../SeedMixedFileDirectory.csproj | 50 + .../User/Events/Event.cs | 20 + .../User/Events/Metadata/Metadata.cs | 20 + .../src/SeedMixedFileDirectory/User/User.cs | 23 + seed/csharp-model/seed.yml | 1 + .../.github/workflows/ci.yml | 69 + .../mixed-file-directory/.gitignore | 484 ++ .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../csharp-sdk/mixed-file-directory/README.md | 87 + .../mixed-file-directory/reference.md | 220 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 53 + .../Core/RawClientTests.cs | 113 + .../SeedMixedFileDirectory.Test.csproj | 26 + .../SeedMixedFileDirectory.Test/TestClient.cs | 8 + .../Unit/MockServer/BaseMockServerTest.cs | 39 + .../Unit/MockServer/CreateTest.cs | 62 + .../Unit/MockServer/GetMetadataTest.cs | 51 + .../Unit/MockServer/ListEventsTest.cs | 51 + .../Unit/MockServer/ListTest.cs | 52 + .../Core/CollectionItemSerializer.cs | 91 + .../SeedMixedFileDirectory/Core/Constants.cs | 7 + .../Core/DateTimeSerializer.cs | 22 + .../SeedMixedFileDirectory/Core/Extensions.cs | 14 + .../Core/HeaderValue.cs | 17 + .../SeedMixedFileDirectory/Core/Headers.cs | 17 + .../Core/HttpMethodExtensions.cs | 8 + .../Core/JsonConfiguration.cs | 32 + .../Core/OneOfSerializer.cs | 69 + .../Core/Public/ClientOptions.cs | 35 + .../Core/Public/RequestOptions.cs | 35 + .../SeedMixedFileDirectoryApiException.cs | 18 + .../Public/SeedMixedFileDirectoryException.cs | 11 + .../Core/Public/Version.cs | 6 + .../SeedMixedFileDirectory/Core/RawClient.cs | 185 + .../Core/StringEnumSerializer.cs | 53 + .../Organization/OrganizationClient.cs | 63 + .../Types/CreateOrganizationRequest.cs | 17 + .../Organization/Types/Organization.cs | 23 + .../SeedMixedFileDirectory.csproj | 50 + .../SeedMixedFileDirectoryClient.cs | 38 + .../User/Events/EventsClient.cs | 73 + .../User/Events/Metadata/MetadataClient.cs | 66 + .../Requests/GetEventMetadataRequest.cs | 15 + .../User/Events/Metadata/Types/Metadata.cs | 20 + .../Events/Requests/ListUserEventsRequest.cs | 18 + .../User/Events/Types/Event.cs | 20 + .../User/Requests/ListUsersRequest.cs | 18 + .../SeedMixedFileDirectory/User/Types/User.cs | 23 + .../SeedMixedFileDirectory/User/UserClient.cs | 72 + seed/csharp-sdk/seed.yml | 1 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + seed/fastapi/mixed-file-directory/__init__.py | 13 + .../mixed-file-directory/core/__init__.py | 42 + .../core/abstract_fern_service.py | 10 + .../core/datetime_utils.py | 30 + .../core/exceptions/__init__.py | 17 + .../core/exceptions/fern_http_exception.py | 24 + .../core/exceptions/handlers.py | 50 + .../core/exceptions/unauthorized.py | 15 + .../core/pydantic_utilities.py | 259 + .../mixed-file-directory/core/route_args.py | 73 + .../core/security/__init__.py | 5 + .../core/security/bearer.py | 22 + .../core/serialization.py | 258 + seed/fastapi/mixed-file-directory/register.py | 61 + .../resources/__init__.py | 7 + .../resources/organization/__init__.py | 5 + .../organization/service/__init__.py | 5 + .../resources/organization/service/service.py | 81 + .../resources/organization/types/__init__.py | 6 + .../types/create_organization_request.py | 19 + .../organization/types/organization.py | 23 + .../resources/user/__init__.py | 6 + .../resources/user/resources/__init__.py | 6 + .../user/resources/events/__init__.py | 6 + .../resources/events/resources/__init__.py | 6 + .../events/resources/metadata/__init__.py | 5 + .../resources/metadata/service/__init__.py | 5 + .../resources/metadata/service/service.py | 82 + .../resources/metadata/types/__init__.py | 5 + .../resources/metadata/types/metadata.py | 21 + .../user/resources/events/service/__init__.py | 5 + .../user/resources/events/service/service.py | 89 + .../user/resources/events/types/__init__.py | 5 + .../user/resources/events/types/event.py | 21 + .../resources/user/service/__init__.py | 5 + .../resources/user/service/service.py | 87 + .../resources/user/types/__init__.py | 5 + .../resources/user/types/user.py | 22 + .../snippet-templates.json | 0 .../fastapi/mixed-file-directory/snippet.json | 0 .../mixed-file-directory/types/__init__.py | 5 + seed/fastapi/mixed-file-directory/types/id.py | 3 + .../.github/workflows/ci.yml | 27 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../core/extra_properties.go | 141 + .../core/extra_properties_test.go | 228 + .../mixed-file-directory/core/stringer.go | 13 + .../mixed-file-directory/core/time.go | 137 + seed/go-fiber/mixed-file-directory/go.mod | 8 + seed/go-fiber/mixed-file-directory/go.sum | 12 + .../mixed-file-directory/organization.go | 79 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 seed/go-fiber/mixed-file-directory/types.go | 5 + seed/go-fiber/mixed-file-directory/user.go | 50 + .../mixed-file-directory/user/events.go | 50 + .../user/events/metadata.go | 49 + .../.github/workflows/ci.yml | 27 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../core/extra_properties.go | 141 + .../core/extra_properties_test.go | 228 + .../mixed-file-directory/core/stringer.go | 13 + .../mixed-file-directory/core/time.go | 137 + seed/go-model/mixed-file-directory/go.mod | 8 + seed/go-model/mixed-file-directory/go.sum | 12 + .../mixed-file-directory/organization.go | 79 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 seed/go-model/mixed-file-directory/types.go | 5 + seed/go-model/mixed-file-directory/user.go | 45 + .../mixed-file-directory/user/events.go | 45 + .../user/events/metadata.go | 45 + .../.github/workflows/ci.yml | 27 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../mixed-file-directory/client/client.go | 36 + .../client/client_test.go | 45 + seed/go-sdk/mixed-file-directory/core/core.go | 287 + .../mixed-file-directory/core/core_test.go | 303 + .../core/extra_properties.go | 141 + .../core/extra_properties_test.go | 228 + .../go-sdk/mixed-file-directory/core/query.go | 231 + .../mixed-file-directory/core/query_test.go | 187 + .../core/request_option.go | 85 + .../mixed-file-directory/core/retrier.go | 166 + .../mixed-file-directory/core/stringer.go | 13 + seed/go-sdk/mixed-file-directory/core/time.go | 137 + seed/go-sdk/mixed-file-directory/go.mod | 9 + seed/go-sdk/mixed-file-directory/go.sum | 14 + .../option/request_option.go | 41 + .../mixed-file-directory/organization.go | 93 + .../organization/client.go | 68 + seed/go-sdk/mixed-file-directory/pointer.go | 132 + .../snippet-templates.json | 0 seed/go-sdk/mixed-file-directory/snippet.json | 48 + seed/go-sdk/mixed-file-directory/types.go | 5 + seed/go-sdk/mixed-file-directory/user.go | 57 + .../user/client/client.go | 79 + .../mixed-file-directory/user/events.go | 57 + .../user/events/client/client.go | 79 + .../user/events/metadata.go | 56 + .../user/events/metadata/client.go | 75 + .../.github/workflows/ci.yml | 61 + .../mixed-file-directory/.gitignore | 24 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../mixed-file-directory/build.gradle | 67 + .../mixed-file-directory/settings.gradle | 0 .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 .../core/DateTimeDeserializer.java | 55 + .../core/ObjectMappers.java | 36 + .../CreateOrganizationRequest.java | 86 + .../model/organization/Organization.java | 149 + .../mixedFileDirectory/model/user/User.java | 130 + .../model/user/events/Event.java | 108 + .../model/user/events/metadata/Metadata.java | 108 + .../.github/workflows/ci.yml | 61 + seed/java-sdk/mixed-file-directory/.gitignore | 24 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../mixed-file-directory/build.gradle | 70 + .../sample-app/build.gradle | 19 + .../sample-app/src/main/java/sample/App.java | 13 + .../mixed-file-directory/settings.gradle | 1 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 .../SeedMixedFileDirectoryClient.java | 36 + .../SeedMixedFileDirectoryClientBuilder.java | 23 + .../core/ClientOptions.java | 103 + .../core/DateTimeDeserializer.java | 55 + .../mixedFileDirectory/core/Environment.java | 20 + .../mixedFileDirectory/core/MediaTypes.java | 13 + .../core/ObjectMappers.java | 36 + .../core/RequestOptions.java | 58 + .../core/ResponseBodyInputStream.java | 45 + .../core/ResponseBodyReader.java | 44 + .../core/RetryInterceptor.java | 78 + .../SeedMixedFileDirectoryApiException.java | 45 + .../core/SeedMixedFileDirectoryException.java | 17 + .../seed/mixedFileDirectory/core/Stream.java | 97 + .../mixedFileDirectory/core/Suppliers.java | 23 + .../organization/OrganizationClient.java | 77 + .../types/CreateOrganizationRequest.java | 102 + .../organization/types/Organization.java | 165 + .../resources/user/UserClient.java | 88 + .../resources/user/events/EventsClient.java | 88 + .../user/events/metadata/MetadataClient.java | 67 + .../requests/GetEventMetadataRequest.java | 102 + .../user/events/metadata/types/Metadata.java | 124 + .../requests/ListUserEventsRequest.java | 98 + .../resources/user/events/types/Event.java | 124 + .../user/requests/ListUsersRequest.java | 98 + .../resources/user/types/User.java | 146 + .../seed/mixedFileDirectory/TestClient.java | 11 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../core/APIException.java | 10 + .../core/DateTimeDeserializer.java | 56 + .../core/ObjectMappers.java | 41 + .../organization/OrganizationService.java | 23 + .../types/CreateOrganizationRequest.java | 95 + .../organization/types/Organization.java | 162 + .../resources/user/UserService.java | 24 + .../resources/user/events/EventsService.java | 24 + .../user/events/metadata/MetadataService.java | 22 + .../user/events/metadata/types/Metadata.java | 118 + .../resources/user/events/types/Event.java | 118 + .../resources/user/types/User.java | 140 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 .../mixed-file-directory/types/Id.java | 49 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + seed/openapi/mixed-file-directory/openapi.yml | 155 + .../snippet-templates.json | 0 .../openapi/mixed-file-directory/snippet.json | 0 .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../mixed-file-directory/collection.json | 351 + .../snippet-templates.json | 0 .../postman/mixed-file-directory/snippet.json | 0 .../.github/workflows/ci.yml | 61 + seed/pydantic/mixed-file-directory/.gitignore | 5 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + seed/pydantic/mixed-file-directory/README.md | 0 .../mixed-file-directory/pyproject.toml | 59 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 .../src/seed/mixed_file_directory/__init__.py | 6 + .../mixed_file_directory/core/__init__.py | 25 + .../core/datetime_utils.py | 28 + .../core/pydantic_utilities.py | 249 + .../core/serialization.py | 254 + .../src/seed/mixed_file_directory/id.py | 3 + .../src/seed/mixed_file_directory/py.typed | 0 .../resources/__init__.py | 7 + .../resources/organization/__init__.py | 6 + .../create_organization_request.py | 17 + .../resources/organization/organization.py | 21 + .../resources/user/__init__.py | 6 + .../resources/user/resources/__init__.py | 6 + .../user/resources/events/__init__.py | 6 + .../resources/user/resources/events/event.py | 19 + .../resources/events/resources/__init__.py | 6 + .../events/resources/metadata/__init__.py | 5 + .../events/resources/metadata/metadata.py | 19 + .../resources/user/user.py | 20 + .../tests/custom/test_client.py | 7 + .../.github/workflows/ci.yml | 63 + .../mixed-file-directory/.gitignore | 5 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../python-sdk/mixed-file-directory/README.md | 134 + .../mixed-file-directory/pyproject.toml | 61 + .../mixed-file-directory/reference.md | 285 + .../snippet-templates.json | 362 + .../mixed-file-directory/snippet.json | 57 + .../mixed-file-directory/src/seed/__init__.py | 20 + .../mixed-file-directory/src/seed/client.py | 108 + .../src/seed/core/__init__.py | 46 + .../src/seed/core/api_error.py | 15 + .../src/seed/core/client_wrapper.py | 48 + .../src/seed/core/datetime_utils.py | 28 + .../src/seed/core/file.py | 43 + .../src/seed/core/http_client.py | 477 ++ .../src/seed/core/jsonable_encoder.py | 101 + .../src/seed/core/pydantic_utilities.py | 249 + .../src/seed/core/query_encoder.py | 58 + .../src/seed/core/remove_none_from_dict.py | 11 + .../src/seed/core/request_options.py | 32 + .../src/seed/core/serialization.py | 254 + .../src/seed/organization/__init__.py | 5 + .../src/seed/organization/client.py | 129 + .../src/seed/organization/types/__init__.py | 6 + .../types/create_organization_request.py | 19 + .../seed/organization/types/organization.py | 23 + .../mixed-file-directory/src/seed/py.typed | 0 .../src/seed/types/__init__.py | 5 + .../mixed-file-directory/src/seed/types/id.py | 3 + .../src/seed/user/__init__.py | 7 + .../src/seed/user/client.py | 134 + .../src/seed/user/events/__init__.py | 7 + .../src/seed/user/events/client.py | 134 + .../src/seed/user/events/metadata/__init__.py | 5 + .../src/seed/user/events/metadata/client.py | 125 + .../user/events/metadata/types/__init__.py | 5 + .../user/events/metadata/types/metadata.py | 21 + .../src/seed/user/events/types/__init__.py | 5 + .../src/seed/user/events/types/event.py | 21 + .../src/seed/user/types/__init__.py | 5 + .../src/seed/user/types/user.py | 22 + .../mixed-file-directory/src/seed/version.py | 3 + .../mixed-file-directory/tests/__init__.py | 2 + .../mixed-file-directory/tests/conftest.py | 16 + .../tests/custom/test_client.py | 7 + .../tests/test_organization.py | 24 + .../mixed-file-directory/tests/test_user.py | 16 + .../tests/user/__init__.py | 2 + .../tests/user/events/__init__.py | 2 + .../tests/user/events/test_metadata.py | 16 + .../tests/user/test_events.py | 16 + .../mixed-file-directory/tests/utilities.py | 162 + .../tests/utils/__init__.py | 2 + .../tests/utils/assets/models/__init__.py | 21 + .../tests/utils/assets/models/circle.py | 11 + .../tests/utils/assets/models/color.py | 7 + .../assets/models/object_with_defaults.py | 16 + .../models/object_with_optional_field.py | 34 + .../tests/utils/assets/models/shape.py | 26 + .../tests/utils/assets/models/square.py | 11 + .../assets/models/undiscriminated_shape.py | 9 + .../tests/utils/test_http_client.py | 61 + .../tests/utils/test_query_encoding.py | 37 + .../tests/utils/test_serialization.py | 72 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../mixed-file-directory/.rubocop.yml | 36 + seed/ruby-model/mixed-file-directory/Gemfile | 9 + seed/ruby-model/mixed-file-directory/Rakefile | 12 + .../mixed-file-directory/lib/gemconfig.rb | 14 + .../lib/seed_mixed_file_directory_client.rb | 7 + .../types/create_organization_request.rb | 57 + .../organization/types/organization.rb | 78 + .../user/events/metadata/types/metadata.rb | 71 + .../user/events/types/event.rb | 69 + .../user/types/user.rb | 74 + .../seed_mixed_file_directory_client.gemspec | 21 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 .../mixed-file-directory/test/test_helper.rb | 6 + .../test_seed_mixed_file_directory_client.rb | 11 + seed/ruby-sdk/literal/fern_literal.gemspec | 4 + seed/ruby-sdk/literal/lib/fern_literal.rb | 83 +- .../lib/fern_literal/headers/client.rb | 99 + .../lib/fern_literal/inlined/client.rb | 141 + .../literal/lib/fern_literal/path/client.rb | 97 + .../literal/lib/fern_literal/query/client.rb | 105 + .../lib/fern_literal/reference/client.rb | 106 + seed/ruby-sdk/literal/lib/requests.rb | 158 + seed/ruby-sdk/literal/lib/types_export.rb | 8 + seed/ruby-sdk/literal/snippet.json | 115 + .../.github/workflows/publish.yml | 26 + seed/ruby-sdk/mixed-file-directory/.gitignore | 10 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../mixed-file-directory/.rubocop.yml | 36 + seed/ruby-sdk/mixed-file-directory/Gemfile | 9 + seed/ruby-sdk/mixed-file-directory/README.md | 0 seed/ruby-sdk/mixed-file-directory/Rakefile | 12 + .../fern_mixed_file_directory.gemspec | 25 + .../lib/fern_mixed_file_directory.rb | 50 + .../organization/client.rb | 84 + .../types/create_organization_request.rb | 57 + .../organization/types/organization.rb | 78 + .../fern_mixed_file_directory/user/client.rb | 90 + .../user/events/client.rb | 92 + .../user/events/metadata/client.rb | 85 + .../user/events/metadata/types/metadata.rb | 71 + .../user/events/types/event.rb | 69 + .../user/types/user.rb | 74 + .../mixed-file-directory/lib/gemconfig.rb | 14 + .../mixed-file-directory/lib/requests.rb | 140 + .../mixed-file-directory/lib/types_export.rb | 7 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 93 + .../test/test_fern_mixed_file_directory.rb | 11 + .../mixed-file-directory/test/test_helper.rb | 6 + seed/ruby-sdk/seed.yml | 9 +- .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + .../mixed-file-directory/api/index.ts | 2 + .../api/resources/index.ts | 4 + .../api/resources/organization/index.ts | 2 + .../service/OrganizationService.ts | 90 + .../resources/organization/service/index.ts | 1 + .../types/CreateOrganizationRequest.ts | 7 + .../organization/types/Organization.ts | 11 + .../api/resources/organization/types/index.ts | 2 + .../api/resources/user/index.ts | 3 + .../resources/user/resources/events/index.ts | 3 + .../user/resources/events/resources/index.ts | 2 + .../events/resources/metadata/index.ts | 2 + .../metadata/service/MetadataService.ts | 81 + .../resources/metadata/service/index.ts | 1 + .../resources/metadata/types/Metadata.ts | 10 + .../events/resources/metadata/types/index.ts | 1 + .../resources/events/service/EventsService.ts | 81 + .../user/resources/events/service/index.ts | 1 + .../user/resources/events/types/Event.ts | 10 + .../user/resources/events/types/index.ts | 1 + .../api/resources/user/resources/index.ts | 2 + .../api/resources/user/service/UserService.ts | 81 + .../api/resources/user/service/index.ts | 1 + .../api/resources/user/types/User.ts | 11 + .../api/resources/user/types/index.ts | 1 + .../mixed-file-directory/api/types/Id.ts | 5 + .../mixed-file-directory/api/types/index.ts | 1 + .../mixed-file-directory/core/index.ts | 1 + .../core/schemas/Schema.ts | 98 + .../core/schemas/builders/date/date.ts | 65 + .../core/schemas/builders/date/index.ts | 1 + .../core/schemas/builders/enum/enum.ts | 43 + .../core/schemas/builders/enum/index.ts | 1 + .../core/schemas/builders/index.ts | 13 + .../core/schemas/builders/lazy/index.ts | 3 + .../core/schemas/builders/lazy/lazy.ts | 32 + .../core/schemas/builders/lazy/lazyObject.ts | 20 + .../core/schemas/builders/list/index.ts | 1 + .../core/schemas/builders/list/list.ts | 73 + .../builders/literals/booleanLiteral.ts | 29 + .../core/schemas/builders/literals/index.ts | 2 + .../builders/literals/stringLiteral.ts | 29 + .../object-like/getObjectLikeUtils.ts | 79 + .../schemas/builders/object-like/index.ts | 2 + .../schemas/builders/object-like/types.ts | 11 + .../core/schemas/builders/object/index.ts | 22 + .../core/schemas/builders/object/object.ts | 324 + .../object/objectWithoutOptionalProperties.ts | 18 + .../core/schemas/builders/object/property.ts | 23 + .../core/schemas/builders/object/types.ts | 72 + .../core/schemas/builders/primitives/any.ts | 4 + .../schemas/builders/primitives/boolean.ts | 25 + .../core/schemas/builders/primitives/index.ts | 5 + .../schemas/builders/primitives/number.ts | 25 + .../schemas/builders/primitives/string.ts | 25 + .../schemas/builders/primitives/unknown.ts | 4 + .../core/schemas/builders/record/index.ts | 2 + .../core/schemas/builders/record/record.ts | 130 + .../core/schemas/builders/record/types.ts | 17 + .../builders/schema-utils/JsonError.ts | 9 + .../builders/schema-utils/ParseError.ts | 9 + .../builders/schema-utils/getSchemaUtils.ts | 105 + .../schemas/builders/schema-utils/index.ts | 4 + .../schema-utils/stringifyValidationErrors.ts | 8 + .../core/schemas/builders/set/index.ts | 1 + .../core/schemas/builders/set/set.ts | 43 + .../builders/undiscriminated-union/index.ts | 6 + .../builders/undiscriminated-union/types.ts | 10 + .../undiscriminatedUnion.ts | 60 + .../schemas/builders/union/discriminant.ts | 14 + .../core/schemas/builders/union/index.ts | 10 + .../core/schemas/builders/union/types.ts | 26 + .../core/schemas/builders/union/union.ts | 170 + .../core/schemas/index.ts | 2 + .../core/schemas/utils/MaybePromise.ts | 1 + .../addQuestionMarksToNullableProperties.ts | 15 + .../utils/createIdentitySchemaCreator.ts | 21 + .../core/schemas/utils/entries.ts | 3 + .../core/schemas/utils/filterObject.ts | 10 + .../utils/getErrorMessageForIncorrectType.ts | 21 + .../core/schemas/utils/isPlainObject.ts | 17 + .../core/schemas/utils/keys.ts | 3 + .../core/schemas/utils/maybeSkipValidation.ts | 38 + .../core/schemas/utils/partition.ts | 12 + .../errors/SeedMixedFileDirectoryError.ts | 14 + .../mixed-file-directory/errors/index.ts | 1 + seed/ts-express/mixed-file-directory/index.ts | 3 + .../mixed-file-directory/register.ts | 20 + .../serialization/index.ts | 2 + .../serialization/resources/index.ts | 4 + .../resources/organization/index.ts | 1 + .../types/CreateOrganizationRequest.ts | 20 + .../organization/types/Organization.ts | 24 + .../resources/organization/types/index.ts | 2 + .../serialization/resources/user/index.ts | 3 + .../resources/user/resources/events/index.ts | 3 + .../user/resources/events/resources/index.ts | 2 + .../events/resources/metadata/index.ts | 1 + .../resources/metadata/types/Metadata.ts | 22 + .../events/resources/metadata/types/index.ts | 1 + .../user/resources/events/service/index.ts | 1 + .../resources/events/service/listEvents.ts | 16 + .../user/resources/events/types/Event.ts | 20 + .../user/resources/events/types/index.ts | 1 + .../resources/user/resources/index.ts | 2 + .../resources/user/service/index.ts | 1 + .../resources/user/service/list.ts | 14 + .../resources/user/types/User.ts | 22 + .../resources/user/types/index.ts | 1 + .../serialization/types/Id.ts | 13 + .../serialization/types/index.ts | 1 + .../snippet-templates.json | 0 .../mixed-file-directory/snippet.json | 0 .../.github/workflows/ci.yml | 57 + seed/ts-sdk/mixed-file-directory/.gitignore | 3 + .../.mock/definition/__package__.yml | 2 + .../.mock/definition/api.yml | 1 + .../.mock/definition/organization.yml | 26 + .../.mock/definition/user.yml | 26 + .../.mock/definition/user/events.yml | 26 + .../.mock/definition/user/events/metadata.yml | 23 + .../.mock/fern.config.json | 1 + .../mixed-file-directory/.mock/generators.yml | 1 + seed/ts-sdk/mixed-file-directory/.npmignore | 9 + .../mixed-file-directory/.prettierrc.yml | 2 + seed/ts-sdk/mixed-file-directory/README.md | 137 + .../mixed-file-directory/jest.config.js | 5 + seed/ts-sdk/mixed-file-directory/package.json | 43 + seed/ts-sdk/mixed-file-directory/reference.md | 269 + .../snippet-templates.json | 350 + seed/ts-sdk/mixed-file-directory/snippet.json | 49 + .../ts-sdk/mixed-file-directory/src/Client.ts | 38 + .../mixed-file-directory/src/api/index.ts | 2 + .../src/api/resources/index.ts | 5 + .../resources/organization/client/Client.ts | 92 + .../resources/organization/client/index.ts | 1 + .../src/api/resources/organization/index.ts | 2 + .../types/CreateOrganizationRequest.ts | 7 + .../organization/types/Organization.ts | 11 + .../api/resources/organization/types/index.ts | 2 + .../src/api/resources/user/client/Client.ts | 105 + .../src/api/resources/user/client/index.ts | 1 + .../user/client/requests/ListUsersRequest.ts | 16 + .../resources/user/client/requests/index.ts | 1 + .../src/api/resources/user/index.ts | 3 + .../user/resources/events/client/Client.ts | 105 + .../user/resources/events/client/index.ts | 1 + .../client/requests/ListUserEventsRequest.ts | 16 + .../resources/events/client/requests/index.ts | 1 + .../resources/user/resources/events/index.ts | 3 + .../user/resources/events/resources/index.ts | 3 + .../resources/metadata/client/Client.ts | 95 + .../events/resources/metadata/client/index.ts | 1 + .../requests/GetEventMetadataRequest.ts | 15 + .../metadata/client/requests/index.ts | 1 + .../events/resources/metadata/index.ts | 2 + .../resources/metadata/types/Metadata.ts | 10 + .../events/resources/metadata/types/index.ts | 1 + .../user/resources/events/types/Event.ts | 10 + .../user/resources/events/types/index.ts | 1 + .../src/api/resources/user/resources/index.ts | 3 + .../src/api/resources/user/types/User.ts | 11 + .../src/api/resources/user/types/index.ts | 1 + .../mixed-file-directory/src/api/types/Id.ts | 5 + .../src/api/types/index.ts | 1 + .../src/core/fetcher/APIResponse.ts | 12 + .../src/core/fetcher/Fetcher.ts | 143 + .../src/core/fetcher/Supplier.ts | 11 + .../src/core/fetcher/createRequestUrl.ts | 10 + .../src/core/fetcher/getFetchFn.ts | 25 + .../src/core/fetcher/getHeader.ts | 8 + .../src/core/fetcher/getRequestBody.ts | 14 + .../src/core/fetcher/getResponseBody.ts | 32 + .../src/core/fetcher/index.ts | 5 + .../src/core/fetcher/makeRequest.ts | 44 + .../src/core/fetcher/requestWithRetries.ts | 21 + .../src/core/fetcher/signals.ts | 38 + .../Node18UniversalStreamWrapper.ts | 256 + .../stream-wrappers/NodePre18StreamWrapper.ts | 106 + .../stream-wrappers/UndiciStreamWrapper.ts | 243 + .../stream-wrappers/chooseStreamWrapper.ts | 33 + .../mixed-file-directory/src/core/index.ts | 3 + .../src/core/runtime/index.ts | 1 + .../src/core/runtime/runtime.ts | 126 + .../src/core/schemas/Schema.ts | 98 + .../src/core/schemas/builders/date/date.ts | 65 + .../src/core/schemas/builders/date/index.ts | 1 + .../src/core/schemas/builders/enum/enum.ts | 43 + .../src/core/schemas/builders/enum/index.ts | 1 + .../src/core/schemas/builders/index.ts | 13 + .../src/core/schemas/builders/lazy/index.ts | 3 + .../src/core/schemas/builders/lazy/lazy.ts | 32 + .../core/schemas/builders/lazy/lazyObject.ts | 20 + .../src/core/schemas/builders/list/index.ts | 1 + .../src/core/schemas/builders/list/list.ts | 73 + .../builders/literals/booleanLiteral.ts | 29 + .../core/schemas/builders/literals/index.ts | 2 + .../builders/literals/stringLiteral.ts | 29 + .../object-like/getObjectLikeUtils.ts | 79 + .../schemas/builders/object-like/index.ts | 2 + .../schemas/builders/object-like/types.ts | 11 + .../src/core/schemas/builders/object/index.ts | 22 + .../core/schemas/builders/object/object.ts | 324 + .../object/objectWithoutOptionalProperties.ts | 18 + .../core/schemas/builders/object/property.ts | 23 + .../src/core/schemas/builders/object/types.ts | 72 + .../core/schemas/builders/primitives/any.ts | 4 + .../schemas/builders/primitives/boolean.ts | 25 + .../core/schemas/builders/primitives/index.ts | 5 + .../schemas/builders/primitives/number.ts | 25 + .../schemas/builders/primitives/string.ts | 25 + .../schemas/builders/primitives/unknown.ts | 4 + .../src/core/schemas/builders/record/index.ts | 2 + .../core/schemas/builders/record/record.ts | 130 + .../src/core/schemas/builders/record/types.ts | 17 + .../builders/schema-utils/JsonError.ts | 9 + .../builders/schema-utils/ParseError.ts | 9 + .../builders/schema-utils/getSchemaUtils.ts | 105 + .../schemas/builders/schema-utils/index.ts | 4 + .../schema-utils/stringifyValidationErrors.ts | 8 + .../src/core/schemas/builders/set/index.ts | 1 + .../src/core/schemas/builders/set/set.ts | 43 + .../builders/undiscriminated-union/index.ts | 6 + .../builders/undiscriminated-union/types.ts | 10 + .../undiscriminatedUnion.ts | 60 + .../schemas/builders/union/discriminant.ts | 14 + .../src/core/schemas/builders/union/index.ts | 10 + .../src/core/schemas/builders/union/types.ts | 26 + .../src/core/schemas/builders/union/union.ts | 170 + .../src/core/schemas/index.ts | 2 + .../src/core/schemas/utils/MaybePromise.ts | 1 + .../addQuestionMarksToNullableProperties.ts | 15 + .../utils/createIdentitySchemaCreator.ts | 21 + .../src/core/schemas/utils/entries.ts | 3 + .../src/core/schemas/utils/filterObject.ts | 10 + .../utils/getErrorMessageForIncorrectType.ts | 21 + .../src/core/schemas/utils/isPlainObject.ts | 17 + .../src/core/schemas/utils/keys.ts | 3 + .../core/schemas/utils/maybeSkipValidation.ts | 38 + .../src/core/schemas/utils/partition.ts | 12 + .../src/errors/SeedMixedFileDirectoryError.ts | 45 + .../SeedMixedFileDirectoryTimeoutError.ts | 10 + .../mixed-file-directory/src/errors/index.ts | 2 + seed/ts-sdk/mixed-file-directory/src/index.ts | 3 + .../src/serialization/index.ts | 2 + .../src/serialization/resources/index.ts | 4 + .../resources/organization/index.ts | 1 + .../types/CreateOrganizationRequest.ts | 20 + .../organization/types/Organization.ts | 26 + .../resources/organization/types/index.ts | 2 + .../resources/user/client/index.ts | 1 + .../resources/user/client/list.ts | 15 + .../src/serialization/resources/user/index.ts | 3 + .../user/resources/events/client/index.ts | 1 + .../resources/events/client/listEvents.ts | 17 + .../resources/user/resources/events/index.ts | 3 + .../user/resources/events/resources/index.ts | 2 + .../events/resources/metadata/index.ts | 1 + .../resources/metadata/types/Metadata.ts | 23 + .../events/resources/metadata/types/index.ts | 1 + .../user/resources/events/types/Event.ts | 21 + .../user/resources/events/types/index.ts | 1 + .../resources/user/resources/index.ts | 2 + .../resources/user/types/User.ts | 23 + .../resources/user/types/index.ts | 1 + .../src/serialization/types/Id.ts | 13 + .../src/serialization/types/index.ts | 1 + .../mixed-file-directory/tests/custom.test.ts | 13 + .../tests/unit/fetcher/Fetcher.test.ts | 25 + .../unit/fetcher/createRequestUrl.test.ts | 51 + .../tests/unit/fetcher/getFetchFn.test.ts | 22 + .../tests/unit/fetcher/getRequestBody.test.ts | 81 + .../unit/fetcher/getResponseBody.test.ts | 68 + .../tests/unit/fetcher/makeRequest.test.ts | 58 + .../unit/fetcher/requestWithRetries.test.ts | 85 + .../tests/unit/fetcher/signals.test.ts | 69 + .../Node18UniversalStreamWrapper.test.ts | 178 + .../NodePre18StreamWrapper.test.ts | 124 + .../UndiciStreamWrapper.test.ts | 153 + .../chooseStreamWrapper.test.ts | 43 + .../fetcher/stream-wrappers/webpack.test.ts | 35 + .../tests/unit/zurg/date/date.test.ts | 31 + .../tests/unit/zurg/enum/enum.test.ts | 30 + .../tests/unit/zurg/lazy/lazy.test.ts | 57 + .../tests/unit/zurg/lazy/lazyObject.test.ts | 18 + .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 41 + .../unit/zurg/literals/stringLiteral.test.ts | 21 + .../object-like/withParsedProperties.test.ts | 57 + .../tests/unit/zurg/object/extend.test.ts | 89 + .../tests/unit/zurg/object/object.test.ts | 255 + .../objectWithoutOptionalProperties.test.ts | 21 + .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 34 + .../zurg/schema-utils/getSchemaUtils.test.ts | 83 + .../tests/unit/zurg/schema.test.ts | 78 + .../tests/unit/zurg/set/set.test.ts | 48 + .../tests/unit/zurg/skipValidation.test.ts | 45 + .../undiscriminatedUnion.test.ts | 44 + .../tests/unit/zurg/union/union.test.ts | 113 + .../tests/unit/zurg/utils/itSchema.ts | 78 + .../tests/unit/zurg/utils/itValidate.ts | 56 + .../ts-sdk/mixed-file-directory/tsconfig.json | 17 + .../definition/__package__.yml | 2 + .../mixed-file-directory/definition/api.yml | 1 + .../definition/organization.yml | 26 + .../mixed-file-directory/definition/user.yml | 26 + .../definition/user/events.yml | 26 + .../definition/user/events/metadata.yml | 23 + .../apis/mixed-file-directory/generators.yml | 1 + 822 files changed, 38784 insertions(+), 20 deletions(-) create mode 100644 packages/cli/generation/ir-generator/src/__test__/test-definitions/mixed-file-directory.json create mode 100644 packages/cli/openapi-ir-to-fern/src/FernDefinitionDirectory.ts create mode 100644 packages/cli/openapi-ir-to-fern/src/__test__/FernDefinitionDirectory.test.ts create mode 100644 seed/csharp-model/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/csharp-model/mixed-file-directory/.gitignore create mode 100644 seed/csharp-model/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/csharp-model/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/csharp-model/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/csharp-model/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/csharp-model/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/csharp-model/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/csharp-model/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/csharp-model/mixed-file-directory/.mock/generators.yml create mode 100644 seed/csharp-model/mixed-file-directory/snippet-templates.json create mode 100644 seed/csharp-model/mixed-file-directory/snippet.json create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/SeedMixedFileDirectory.Test.csproj create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/CollectionItemSerializer.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/Constants.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/DateTimeSerializer.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/OneOfSerializer.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/Version.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Organization/CreateOrganizationRequest.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Organization.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectory.csproj create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Event.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Metadata.cs create mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/User.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/csharp-sdk/mixed-file-directory/.gitignore create mode 100644 seed/csharp-sdk/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/csharp-sdk/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/csharp-sdk/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/csharp-sdk/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/csharp-sdk/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/csharp-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/csharp-sdk/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/csharp-sdk/mixed-file-directory/.mock/generators.yml create mode 100644 seed/csharp-sdk/mixed-file-directory/README.md create mode 100644 seed/csharp-sdk/mixed-file-directory/reference.md create mode 100644 seed/csharp-sdk/mixed-file-directory/snippet-templates.json create mode 100644 seed/csharp-sdk/mixed-file-directory/snippet.json create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/RawClientTests.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/SeedMixedFileDirectory.Test.csproj create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/TestClient.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/BaseMockServerTest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/CreateTest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/GetMetadataTest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/ListEventsTest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/ListTest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/CollectionItemSerializer.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Constants.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/DateTimeSerializer.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Extensions.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/HeaderValue.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Headers.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/HttpMethodExtensions.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/OneOfSerializer.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/ClientOptions.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/RequestOptions.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/SeedMixedFileDirectoryApiException.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/SeedMixedFileDirectoryException.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/Version.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/RawClient.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/OrganizationClient.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Types/CreateOrganizationRequest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Types/Organization.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectory.csproj create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectoryClient.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/EventsClient.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/MetadataClient.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Requests/GetEventMetadataRequest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Types/Metadata.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Requests/ListUserEventsRequest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Types/Event.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Requests/ListUsersRequest.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Types/User.cs create mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/UserClient.cs create mode 100644 seed/fastapi/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/fastapi/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/fastapi/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/fastapi/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/fastapi/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/fastapi/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/fastapi/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/fastapi/mixed-file-directory/.mock/generators.yml create mode 100644 seed/fastapi/mixed-file-directory/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/core/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/core/abstract_fern_service.py create mode 100644 seed/fastapi/mixed-file-directory/core/datetime_utils.py create mode 100644 seed/fastapi/mixed-file-directory/core/exceptions/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/core/exceptions/fern_http_exception.py create mode 100644 seed/fastapi/mixed-file-directory/core/exceptions/handlers.py create mode 100644 seed/fastapi/mixed-file-directory/core/exceptions/unauthorized.py create mode 100644 seed/fastapi/mixed-file-directory/core/pydantic_utilities.py create mode 100644 seed/fastapi/mixed-file-directory/core/route_args.py create mode 100644 seed/fastapi/mixed-file-directory/core/security/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/core/security/bearer.py create mode 100644 seed/fastapi/mixed-file-directory/core/serialization.py create mode 100644 seed/fastapi/mixed-file-directory/register.py create mode 100644 seed/fastapi/mixed-file-directory/resources/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/organization/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/organization/service/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/organization/service/service.py create mode 100644 seed/fastapi/mixed-file-directory/resources/organization/types/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/organization/types/create_organization_request.py create mode 100644 seed/fastapi/mixed-file-directory/resources/organization/types/organization.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/service/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/service/service.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/types/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/types/metadata.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/service/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/service/service.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/types/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/resources/events/types/event.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/service/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/service/service.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/types/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/resources/user/types/user.py create mode 100644 seed/fastapi/mixed-file-directory/snippet-templates.json create mode 100644 seed/fastapi/mixed-file-directory/snippet.json create mode 100644 seed/fastapi/mixed-file-directory/types/__init__.py create mode 100644 seed/fastapi/mixed-file-directory/types/id.py create mode 100644 seed/go-fiber/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/go-fiber/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/go-fiber/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/go-fiber/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/go-fiber/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/go-fiber/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/go-fiber/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/go-fiber/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/go-fiber/mixed-file-directory/.mock/generators.yml create mode 100644 seed/go-fiber/mixed-file-directory/core/extra_properties.go create mode 100644 seed/go-fiber/mixed-file-directory/core/extra_properties_test.go create mode 100644 seed/go-fiber/mixed-file-directory/core/stringer.go create mode 100644 seed/go-fiber/mixed-file-directory/core/time.go create mode 100644 seed/go-fiber/mixed-file-directory/go.mod create mode 100644 seed/go-fiber/mixed-file-directory/go.sum create mode 100644 seed/go-fiber/mixed-file-directory/organization.go create mode 100644 seed/go-fiber/mixed-file-directory/snippet-templates.json create mode 100644 seed/go-fiber/mixed-file-directory/snippet.json create mode 100644 seed/go-fiber/mixed-file-directory/types.go create mode 100644 seed/go-fiber/mixed-file-directory/user.go create mode 100644 seed/go-fiber/mixed-file-directory/user/events.go create mode 100644 seed/go-fiber/mixed-file-directory/user/events/metadata.go create mode 100644 seed/go-model/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/go-model/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/go-model/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/go-model/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/go-model/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/go-model/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/go-model/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/go-model/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/go-model/mixed-file-directory/.mock/generators.yml create mode 100644 seed/go-model/mixed-file-directory/core/extra_properties.go create mode 100644 seed/go-model/mixed-file-directory/core/extra_properties_test.go create mode 100644 seed/go-model/mixed-file-directory/core/stringer.go create mode 100644 seed/go-model/mixed-file-directory/core/time.go create mode 100644 seed/go-model/mixed-file-directory/go.mod create mode 100644 seed/go-model/mixed-file-directory/go.sum create mode 100644 seed/go-model/mixed-file-directory/organization.go create mode 100644 seed/go-model/mixed-file-directory/snippet-templates.json create mode 100644 seed/go-model/mixed-file-directory/snippet.json create mode 100644 seed/go-model/mixed-file-directory/types.go create mode 100644 seed/go-model/mixed-file-directory/user.go create mode 100644 seed/go-model/mixed-file-directory/user/events.go create mode 100644 seed/go-model/mixed-file-directory/user/events/metadata.go create mode 100644 seed/go-sdk/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/go-sdk/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/go-sdk/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/go-sdk/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/go-sdk/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/go-sdk/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/go-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/go-sdk/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/go-sdk/mixed-file-directory/.mock/generators.yml create mode 100644 seed/go-sdk/mixed-file-directory/client/client.go create mode 100644 seed/go-sdk/mixed-file-directory/client/client_test.go create mode 100644 seed/go-sdk/mixed-file-directory/core/core.go create mode 100644 seed/go-sdk/mixed-file-directory/core/core_test.go create mode 100644 seed/go-sdk/mixed-file-directory/core/extra_properties.go create mode 100644 seed/go-sdk/mixed-file-directory/core/extra_properties_test.go create mode 100644 seed/go-sdk/mixed-file-directory/core/query.go create mode 100644 seed/go-sdk/mixed-file-directory/core/query_test.go create mode 100644 seed/go-sdk/mixed-file-directory/core/request_option.go create mode 100644 seed/go-sdk/mixed-file-directory/core/retrier.go create mode 100644 seed/go-sdk/mixed-file-directory/core/stringer.go create mode 100644 seed/go-sdk/mixed-file-directory/core/time.go create mode 100644 seed/go-sdk/mixed-file-directory/go.mod create mode 100644 seed/go-sdk/mixed-file-directory/go.sum create mode 100644 seed/go-sdk/mixed-file-directory/option/request_option.go create mode 100644 seed/go-sdk/mixed-file-directory/organization.go create mode 100644 seed/go-sdk/mixed-file-directory/organization/client.go create mode 100644 seed/go-sdk/mixed-file-directory/pointer.go create mode 100644 seed/go-sdk/mixed-file-directory/snippet-templates.json create mode 100644 seed/go-sdk/mixed-file-directory/snippet.json create mode 100644 seed/go-sdk/mixed-file-directory/types.go create mode 100644 seed/go-sdk/mixed-file-directory/user.go create mode 100644 seed/go-sdk/mixed-file-directory/user/client/client.go create mode 100644 seed/go-sdk/mixed-file-directory/user/events.go create mode 100644 seed/go-sdk/mixed-file-directory/user/events/client/client.go create mode 100644 seed/go-sdk/mixed-file-directory/user/events/metadata.go create mode 100644 seed/go-sdk/mixed-file-directory/user/events/metadata/client.go create mode 100644 seed/java-model/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/java-model/mixed-file-directory/.gitignore create mode 100644 seed/java-model/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/java-model/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/java-model/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/java-model/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/java-model/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/java-model/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/java-model/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/java-model/mixed-file-directory/.mock/generators.yml create mode 100644 seed/java-model/mixed-file-directory/build.gradle create mode 100644 seed/java-model/mixed-file-directory/settings.gradle create mode 100644 seed/java-model/mixed-file-directory/snippet-templates.json create mode 100644 seed/java-model/mixed-file-directory/snippet.json create mode 100644 seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/DateTimeDeserializer.java create mode 100644 seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ObjectMappers.java create mode 100644 seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/organization/CreateOrganizationRequest.java create mode 100644 seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/organization/Organization.java create mode 100644 seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/User.java create mode 100644 seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/events/Event.java create mode 100644 seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/events/metadata/Metadata.java create mode 100644 seed/java-sdk/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/java-sdk/mixed-file-directory/.gitignore create mode 100644 seed/java-sdk/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/java-sdk/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/java-sdk/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/java-sdk/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/java-sdk/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/java-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/java-sdk/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/java-sdk/mixed-file-directory/.mock/generators.yml create mode 100644 seed/java-sdk/mixed-file-directory/build.gradle create mode 100644 seed/java-sdk/mixed-file-directory/sample-app/build.gradle create mode 100644 seed/java-sdk/mixed-file-directory/sample-app/src/main/java/sample/App.java create mode 100644 seed/java-sdk/mixed-file-directory/settings.gradle create mode 100644 seed/java-sdk/mixed-file-directory/snippet-templates.json create mode 100644 seed/java-sdk/mixed-file-directory/snippet.json create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/SeedMixedFileDirectoryClient.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/SeedMixedFileDirectoryClientBuilder.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ClientOptions.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/DateTimeDeserializer.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Environment.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/MediaTypes.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ObjectMappers.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/RequestOptions.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ResponseBodyInputStream.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ResponseBodyReader.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/RetryInterceptor.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/SeedMixedFileDirectoryApiException.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/SeedMixedFileDirectoryException.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Stream.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Suppliers.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/OrganizationClient.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/types/CreateOrganizationRequest.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/types/Organization.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/UserClient.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/EventsClient.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/MetadataClient.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/requests/GetEventMetadataRequest.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/types/Metadata.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/requests/ListUserEventsRequest.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/types/Event.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/requests/ListUsersRequest.java create mode 100644 seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/types/User.java create mode 100644 seed/java-sdk/mixed-file-directory/src/test/java/com/seed/mixedFileDirectory/TestClient.java create mode 100644 seed/java-spring/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/java-spring/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/java-spring/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/java-spring/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/java-spring/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/java-spring/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/java-spring/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/java-spring/mixed-file-directory/.mock/generators.yml create mode 100644 seed/java-spring/mixed-file-directory/core/APIException.java create mode 100644 seed/java-spring/mixed-file-directory/core/DateTimeDeserializer.java create mode 100644 seed/java-spring/mixed-file-directory/core/ObjectMappers.java create mode 100644 seed/java-spring/mixed-file-directory/resources/organization/OrganizationService.java create mode 100644 seed/java-spring/mixed-file-directory/resources/organization/types/CreateOrganizationRequest.java create mode 100644 seed/java-spring/mixed-file-directory/resources/organization/types/Organization.java create mode 100644 seed/java-spring/mixed-file-directory/resources/user/UserService.java create mode 100644 seed/java-spring/mixed-file-directory/resources/user/events/EventsService.java create mode 100644 seed/java-spring/mixed-file-directory/resources/user/events/metadata/MetadataService.java create mode 100644 seed/java-spring/mixed-file-directory/resources/user/events/metadata/types/Metadata.java create mode 100644 seed/java-spring/mixed-file-directory/resources/user/events/types/Event.java create mode 100644 seed/java-spring/mixed-file-directory/resources/user/types/User.java create mode 100644 seed/java-spring/mixed-file-directory/snippet-templates.json create mode 100644 seed/java-spring/mixed-file-directory/snippet.json create mode 100644 seed/java-spring/mixed-file-directory/types/Id.java create mode 100644 seed/openapi/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/openapi/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/openapi/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/openapi/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/openapi/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/openapi/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/openapi/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/openapi/mixed-file-directory/.mock/generators.yml create mode 100644 seed/openapi/mixed-file-directory/openapi.yml create mode 100644 seed/openapi/mixed-file-directory/snippet-templates.json create mode 100644 seed/openapi/mixed-file-directory/snippet.json create mode 100644 seed/postman/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/postman/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/postman/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/postman/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/postman/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/postman/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/postman/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/postman/mixed-file-directory/.mock/generators.yml create mode 100644 seed/postman/mixed-file-directory/collection.json create mode 100644 seed/postman/mixed-file-directory/snippet-templates.json create mode 100644 seed/postman/mixed-file-directory/snippet.json create mode 100644 seed/pydantic/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/pydantic/mixed-file-directory/.gitignore create mode 100644 seed/pydantic/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/pydantic/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/pydantic/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/pydantic/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/pydantic/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/pydantic/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/pydantic/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/pydantic/mixed-file-directory/.mock/generators.yml create mode 100644 seed/pydantic/mixed-file-directory/README.md create mode 100644 seed/pydantic/mixed-file-directory/pyproject.toml create mode 100644 seed/pydantic/mixed-file-directory/snippet-templates.json create mode 100644 seed/pydantic/mixed-file-directory/snippet.json create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/datetime_utils.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/pydantic_utilities.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/serialization.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/id.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/py.typed create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/create_organization_request.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/organization.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/event.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/metadata/__init__.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/metadata/metadata.py create mode 100644 seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/user.py create mode 100644 seed/pydantic/mixed-file-directory/tests/custom/test_client.py create mode 100644 seed/python-sdk/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/python-sdk/mixed-file-directory/.gitignore create mode 100644 seed/python-sdk/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/python-sdk/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/python-sdk/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/python-sdk/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/python-sdk/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/python-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/python-sdk/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/python-sdk/mixed-file-directory/.mock/generators.yml create mode 100644 seed/python-sdk/mixed-file-directory/README.md create mode 100644 seed/python-sdk/mixed-file-directory/pyproject.toml create mode 100644 seed/python-sdk/mixed-file-directory/reference.md create mode 100644 seed/python-sdk/mixed-file-directory/snippet-templates.json create mode 100644 seed/python-sdk/mixed-file-directory/snippet.json create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/client.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/api_error.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/client_wrapper.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/datetime_utils.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/file.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/http_client.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/jsonable_encoder.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/pydantic_utilities.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/query_encoder.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/remove_none_from_dict.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/request_options.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/core/serialization.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/organization/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/organization/client.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/organization/types/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/organization/types/create_organization_request.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/organization/types/organization.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/py.typed create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/types/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/types/id.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/client.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/events/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/events/client.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/client.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/types/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/types/metadata.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/events/types/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/events/types/event.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/types/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/user/types/user.py create mode 100644 seed/python-sdk/mixed-file-directory/src/seed/version.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/conftest.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/custom/test_client.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/test_organization.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/test_user.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/user/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/user/events/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/user/events/test_metadata.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/user/test_events.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utilities.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/assets/models/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/assets/models/circle.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/assets/models/color.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/assets/models/object_with_defaults.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/assets/models/object_with_optional_field.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/assets/models/shape.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/assets/models/square.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/assets/models/undiscriminated_shape.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/test_http_client.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/test_query_encoding.py create mode 100644 seed/python-sdk/mixed-file-directory/tests/utils/test_serialization.py create mode 100644 seed/ruby-model/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/ruby-model/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/ruby-model/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/ruby-model/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/ruby-model/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/ruby-model/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/ruby-model/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/ruby-model/mixed-file-directory/.mock/generators.yml create mode 100644 seed/ruby-model/mixed-file-directory/.rubocop.yml create mode 100644 seed/ruby-model/mixed-file-directory/Gemfile create mode 100644 seed/ruby-model/mixed-file-directory/Rakefile create mode 100644 seed/ruby-model/mixed-file-directory/lib/gemconfig.rb create mode 100644 seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client.rb create mode 100644 seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/organization/types/create_organization_request.rb create mode 100644 seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/organization/types/organization.rb create mode 100644 seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/events/metadata/types/metadata.rb create mode 100644 seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/events/types/event.rb create mode 100644 seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/types/user.rb create mode 100644 seed/ruby-model/mixed-file-directory/seed_mixed_file_directory_client.gemspec create mode 100644 seed/ruby-model/mixed-file-directory/snippet-templates.json create mode 100644 seed/ruby-model/mixed-file-directory/snippet.json create mode 100644 seed/ruby-model/mixed-file-directory/test/test_helper.rb create mode 100644 seed/ruby-model/mixed-file-directory/test/test_seed_mixed_file_directory_client.rb create mode 100644 seed/ruby-sdk/literal/lib/fern_literal/headers/client.rb create mode 100644 seed/ruby-sdk/literal/lib/fern_literal/inlined/client.rb create mode 100644 seed/ruby-sdk/literal/lib/fern_literal/path/client.rb create mode 100644 seed/ruby-sdk/literal/lib/fern_literal/query/client.rb create mode 100644 seed/ruby-sdk/literal/lib/fern_literal/reference/client.rb create mode 100644 seed/ruby-sdk/literal/lib/requests.rb create mode 100644 seed/ruby-sdk/literal/lib/types_export.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/.github/workflows/publish.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/.gitignore create mode 100644 seed/ruby-sdk/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/ruby-sdk/mixed-file-directory/.mock/generators.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/.rubocop.yml create mode 100644 seed/ruby-sdk/mixed-file-directory/Gemfile create mode 100644 seed/ruby-sdk/mixed-file-directory/README.md create mode 100644 seed/ruby-sdk/mixed-file-directory/Rakefile create mode 100644 seed/ruby-sdk/mixed-file-directory/fern_mixed_file_directory.gemspec create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/client.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/types/create_organization_request.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/types/organization.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/client.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/client.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/metadata/client.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/metadata/types/metadata.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/types/event.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/types/user.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/gemconfig.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/requests.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/lib/types_export.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/snippet-templates.json create mode 100644 seed/ruby-sdk/mixed-file-directory/snippet.json create mode 100644 seed/ruby-sdk/mixed-file-directory/test/test_fern_mixed_file_directory.rb create mode 100644 seed/ruby-sdk/mixed-file-directory/test/test_helper.rb create mode 100644 seed/ts-express/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/ts-express/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/ts-express/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/ts-express/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/ts-express/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/ts-express/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/ts-express/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/ts-express/mixed-file-directory/.mock/generators.yml create mode 100644 seed/ts-express/mixed-file-directory/api/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/organization/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/organization/service/OrganizationService.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/organization/service/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/organization/types/CreateOrganizationRequest.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/organization/types/Organization.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/organization/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/service/MetadataService.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/service/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/types/Metadata.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/service/EventsService.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/service/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/types/Event.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/events/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/resources/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/service/UserService.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/service/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/types/User.ts create mode 100644 seed/ts-express/mixed-file-directory/api/resources/user/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/api/types/Id.ts create mode 100644 seed/ts-express/mixed-file-directory/api/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/Schema.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/date/date.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/date/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/enum/enum.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/enum/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/lazy.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/lazyObject.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/list/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/literals/booleanLiteral.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/literals/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/literals/stringLiteral.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/types.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/object/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/object/objectWithoutOptionalProperties.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/object/property.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/object/types.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/any.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/boolean.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/number.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/string.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/unknown.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/record/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/record/types.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/JsonError.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/ParseError.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/getSchemaUtils.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/stringifyValidationErrors.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/set/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/set/set.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/types.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/union/discriminant.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/union/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/union/types.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/index.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/MaybePromise.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/addQuestionMarksToNullableProperties.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/createIdentitySchemaCreator.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/entries.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/filterObject.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/getErrorMessageForIncorrectType.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/keys.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/maybeSkipValidation.ts create mode 100644 seed/ts-express/mixed-file-directory/core/schemas/utils/partition.ts create mode 100644 seed/ts-express/mixed-file-directory/errors/SeedMixedFileDirectoryError.ts create mode 100644 seed/ts-express/mixed-file-directory/errors/index.ts create mode 100644 seed/ts-express/mixed-file-directory/index.ts create mode 100644 seed/ts-express/mixed-file-directory/register.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/organization/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/organization/types/CreateOrganizationRequest.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/organization/types/Organization.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/organization/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/types/Metadata.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/service/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/service/listEvents.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/types/Event.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/resources/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/service/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/service/list.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/types/User.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/resources/user/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/types/Id.ts create mode 100644 seed/ts-express/mixed-file-directory/serialization/types/index.ts create mode 100644 seed/ts-express/mixed-file-directory/snippet-templates.json create mode 100644 seed/ts-express/mixed-file-directory/snippet.json create mode 100644 seed/ts-sdk/mixed-file-directory/.github/workflows/ci.yml create mode 100644 seed/ts-sdk/mixed-file-directory/.gitignore create mode 100644 seed/ts-sdk/mixed-file-directory/.mock/definition/__package__.yml create mode 100644 seed/ts-sdk/mixed-file-directory/.mock/definition/api.yml create mode 100644 seed/ts-sdk/mixed-file-directory/.mock/definition/organization.yml create mode 100644 seed/ts-sdk/mixed-file-directory/.mock/definition/user.yml create mode 100644 seed/ts-sdk/mixed-file-directory/.mock/definition/user/events.yml create mode 100644 seed/ts-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml create mode 100644 seed/ts-sdk/mixed-file-directory/.mock/fern.config.json create mode 100644 seed/ts-sdk/mixed-file-directory/.mock/generators.yml create mode 100644 seed/ts-sdk/mixed-file-directory/.npmignore create mode 100644 seed/ts-sdk/mixed-file-directory/.prettierrc.yml create mode 100644 seed/ts-sdk/mixed-file-directory/README.md create mode 100644 seed/ts-sdk/mixed-file-directory/jest.config.js create mode 100644 seed/ts-sdk/mixed-file-directory/package.json create mode 100644 seed/ts-sdk/mixed-file-directory/reference.md create mode 100644 seed/ts-sdk/mixed-file-directory/snippet-templates.json create mode 100644 seed/ts-sdk/mixed-file-directory/snippet.json create mode 100644 seed/ts-sdk/mixed-file-directory/src/Client.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/organization/client/Client.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/organization/client/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/organization/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/CreateOrganizationRequest.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/Organization.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/Client.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/requests/ListUsersRequest.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/requests/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/Client.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/requests/ListUserEventsRequest.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/requests/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/Client.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/requests/GetEventMetadataRequest.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/requests/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/types/Metadata.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/types/Event.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/types/User.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/resources/user/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/types/Id.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/api/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/APIResponse.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/Fetcher.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/Supplier.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/createRequestUrl.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/getFetchFn.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/getHeader.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/getRequestBody.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/getResponseBody.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/makeRequest.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/requestWithRetries.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/signals.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/NodePre18StreamWrapper.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/UndiciStreamWrapper.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/chooseStreamWrapper.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/runtime/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/runtime/runtime.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/Schema.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/date/date.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/date/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/enum/enum.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/enum/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/lazy.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/lazyObject.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/list/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/list/list.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/booleanLiteral.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/stringLiteral.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/getObjectLikeUtils.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/types.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/object.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/objectWithoutOptionalProperties.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/property.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/types.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/any.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/boolean.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/number.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/string.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/unknown.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/record.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/types.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/JsonError.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/ParseError.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/getSchemaUtils.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/set/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/set/set.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/types.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/discriminant.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/types.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/union.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/MaybePromise.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/createIdentitySchemaCreator.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/entries.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/filterObject.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/getErrorMessageForIncorrectType.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/isPlainObject.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/keys.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/maybeSkipValidation.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/partition.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/errors/SeedMixedFileDirectoryError.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/errors/SeedMixedFileDirectoryTimeoutError.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/errors/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/CreateOrganizationRequest.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/Organization.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/client/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/client/list.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/client/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/client/listEvents.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/types/Metadata.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/types/Event.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/types/User.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/types/Id.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/serialization/types/index.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/custom.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/Fetcher.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/createRequestUrl.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getFetchFn.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getRequestBody.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getResponseBody.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/makeRequest.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/requestWithRetries.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/signals.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/Node18UniversalStreamWrapper.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/NodePre18StreamWrapper.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/UndiciStreamWrapper.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/chooseStreamWrapper.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/webpack.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tsconfig.json create mode 100644 test-definitions/fern/apis/mixed-file-directory/definition/__package__.yml create mode 100644 test-definitions/fern/apis/mixed-file-directory/definition/api.yml create mode 100644 test-definitions/fern/apis/mixed-file-directory/definition/organization.yml create mode 100644 test-definitions/fern/apis/mixed-file-directory/definition/user.yml create mode 100644 test-definitions/fern/apis/mixed-file-directory/definition/user/events.yml create mode 100644 test-definitions/fern/apis/mixed-file-directory/definition/user/events/metadata.yml create mode 100644 test-definitions/fern/apis/mixed-file-directory/generators.yml diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index 94740f288bb..fa43c54a4b2 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,3 +1,11 @@ +- changelogEntry: + - summary: | + Fix an issue with non-deterministic file ordering when OpenAPI is used as input. + type: fix + createdAt: '2024-09-06' + irVersion: 53 + version: 0.41.5 + - changelogEntry: - summary: | The Fern OpenAPI importer now handles importing an array for the `type` key. diff --git a/packages/cli/generation/ir-generator/src/__test__/test-definitions/mixed-file-directory.json b/packages/cli/generation/ir-generator/src/__test__/test-definitions/mixed-file-directory.json new file mode 100644 index 00000000000..f399215921f --- /dev/null +++ b/packages/cli/generation/ir-generator/src/__test__/test-definitions/mixed-file-directory.json @@ -0,0 +1,6185 @@ +{ + "fdrApiDefinitionId": null, + "apiVersion": null, + "apiName": { + "originalName": "mixed-file-directory", + "camelCase": { + "unsafeName": "mixedFileDirectory", + "safeName": "mixedFileDirectory" + }, + "snakeCase": { + "unsafeName": "mixed_file_directory", + "safeName": "mixed_file_directory" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_FILE_DIRECTORY", + "safeName": "MIXED_FILE_DIRECTORY" + }, + "pascalCase": { + "unsafeName": "MixedFileDirectory", + "safeName": "MixedFileDirectory" + } + }, + "apiDisplayName": null, + "apiDocs": null, + "auth": { + "requirement": "ALL", + "schemes": [], + "docs": null + }, + "headers": [], + "idempotencyHeaders": [], + "types": { + "type_:Id": { + "name": { + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id" + }, + "shape": { + "_type": "alias", + "aliasOf": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "resolvedType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "availability": null, + "docs": null + }, + "type_organization:Organization": { + "name": { + "name": { + "originalName": "Organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:Organization" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id", + "default": null, + "inline": null + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + }, + "wireValue": "users" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User", + "default": null, + "inline": null + } + } + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_:Id", + "type_user:User" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "availability": null, + "docs": null + }, + "type_organization:CreateOrganizationRequest": { + "name": { + "name": { + "originalName": "CreateOrganizationRequest", + "camelCase": { + "unsafeName": "createOrganizationRequest", + "safeName": "createOrganizationRequest" + }, + "snakeCase": { + "unsafeName": "create_organization_request", + "safeName": "create_organization_request" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_ORGANIZATION_REQUEST", + "safeName": "CREATE_ORGANIZATION_REQUEST" + }, + "pascalCase": { + "unsafeName": "CreateOrganizationRequest", + "safeName": "CreateOrganizationRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:CreateOrganizationRequest" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "availability": null, + "docs": null + }, + "type_user:User": { + "name": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id", + "default": null, + "inline": null + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "age", + "camelCase": { + "unsafeName": "age", + "safeName": "age" + }, + "snakeCase": { + "unsafeName": "age", + "safeName": "age" + }, + "screamingSnakeCase": { + "unsafeName": "AGE", + "safeName": "AGE" + }, + "pascalCase": { + "unsafeName": "Age", + "safeName": "Age" + } + }, + "wireValue": "age" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_:Id" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "availability": null, + "docs": null + }, + "type_user/events:Event": { + "name": { + "name": { + "originalName": "Event", + "camelCase": { + "unsafeName": "event", + "safeName": "event" + }, + "snakeCase": { + "unsafeName": "event", + "safeName": "event" + }, + "screamingSnakeCase": { + "unsafeName": "EVENT", + "safeName": "EVENT" + }, + "pascalCase": { + "unsafeName": "Event", + "safeName": "Event" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "file": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + }, + "typeId": "type_user/events:Event" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id", + "default": null, + "inline": null + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_:Id" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "availability": null, + "docs": null + }, + "type_user/events/metadata:Metadata": { + "name": { + "name": { + "originalName": "Metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + }, + { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "file": { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + }, + "typeId": "type_user/events/metadata:Metadata" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id", + "default": null, + "inline": null + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "value", + "camelCase": { + "unsafeName": "value", + "safeName": "value" + }, + "snakeCase": { + "unsafeName": "value", + "safeName": "value" + }, + "screamingSnakeCase": { + "unsafeName": "VALUE", + "safeName": "VALUE" + }, + "pascalCase": { + "unsafeName": "Value", + "safeName": "Value" + } + }, + "wireValue": "value" + }, + "valueType": { + "_type": "unknown" + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_:Id" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "availability": null, + "docs": null + } + }, + "errors": {}, + "services": { + "service_organization": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/organizations", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_organization.create", + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "displayName": null, + "auth": false, + "idempotent": false, + "baseUrl": null, + "method": "POST", + "path": { + "head": "/", + "parts": [] + }, + "fullPath": { + "head": "/organizations/", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "CreateOrganizationRequest", + "camelCase": { + "unsafeName": "createOrganizationRequest", + "safeName": "createOrganizationRequest" + }, + "snakeCase": { + "unsafeName": "create_organization_request", + "safeName": "create_organization_request" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_ORGANIZATION_REQUEST", + "safeName": "CREATE_ORGANIZATION_REQUEST" + }, + "pascalCase": { + "unsafeName": "CreateOrganizationRequest", + "safeName": "CreateOrganizationRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:CreateOrganizationRequest", + "default": null, + "inline": null + }, + "contentType": null, + "docs": null + }, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "CreateOrganizationRequest", + "camelCase": { + "unsafeName": "createOrganizationRequest", + "safeName": "createOrganizationRequest" + }, + "snakeCase": { + "unsafeName": "create_organization_request", + "safeName": "create_organization_request" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_ORGANIZATION_REQUEST", + "safeName": "CREATE_ORGANIZATION_REQUEST" + }, + "pascalCase": { + "unsafeName": "CreateOrganizationRequest", + "safeName": "CreateOrganizationRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:CreateOrganizationRequest", + "default": null, + "inline": null + }, + "contentType": null, + "docs": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "Organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:Organization", + "default": null, + "inline": null + }, + "docs": null + } + }, + "status-code": null + }, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "url": "/organizations/", + "rootPathParameters": [], + "servicePathParameters": [], + "endpointPathParameters": [], + "serviceHeaders": [], + "endpointHeaders": [], + "queryParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "CreateOrganizationRequest", + "camelCase": { + "unsafeName": "createOrganizationRequest", + "safeName": "createOrganizationRequest" + }, + "snakeCase": { + "unsafeName": "create_organization_request", + "safeName": "create_organization_request" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_ORGANIZATION_REQUEST", + "safeName": "CREATE_ORGANIZATION_REQUEST" + }, + "pascalCase": { + "unsafeName": "CreateOrganizationRequest", + "safeName": "CreateOrganizationRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:CreateOrganizationRequest" + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "CreateOrganizationRequest", + "camelCase": { + "unsafeName": "createOrganizationRequest", + "safeName": "createOrganizationRequest" + }, + "snakeCase": { + "unsafeName": "create_organization_request", + "safeName": "create_organization_request" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_ORGANIZATION_REQUEST", + "safeName": "CREATE_ORGANIZATION_REQUEST" + }, + "pascalCase": { + "unsafeName": "CreateOrganizationRequest", + "safeName": "CreateOrganizationRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:CreateOrganizationRequest" + } + } + ] + } + }, + "jsonExample": { + "name": "string" + } + }, + "name": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:Organization" + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "value": { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id" + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:Organization" + } + }, + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:Organization" + } + }, + { + "name": { + "name": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + }, + "wireValue": "users" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "value": { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id" + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + } + }, + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + } + }, + { + "name": { + "name": { + "originalName": "age", + "camelCase": { + "unsafeName": "age", + "safeName": "age" + }, + "snakeCase": { + "unsafeName": "age", + "safeName": "age" + }, + "screamingSnakeCase": { + "unsafeName": "AGE", + "safeName": "AGE" + }, + "pascalCase": { + "unsafeName": "Age", + "safeName": "Age" + } + }, + "wireValue": "age" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "originalTypeDeclaration": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + } + } + ] + } + }, + "jsonExample": { + "id": "string", + "name": "string", + "age": 1 + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "id": "string", + "name": "string", + "age": 1 + } + ] + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "typeId": "type_organization:Organization" + } + } + ] + } + }, + "jsonExample": { + "id": "string", + "name": "string", + "users": [ + { + "id": "string", + "name": "string", + "age": 1 + } + ] + } + } + } + }, + "id": "455a09a2292a243e99e1189cbc61d209bd194f41", + "docs": null + } + } + ], + "pagination": null, + "availability": null, + "docs": "Create a new organization." + } + ] + }, + "service_user": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/users", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_user.list", + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "displayName": null, + "auth": false, + "idempotent": false, + "baseUrl": null, + "method": "GET", + "path": { + "head": "/", + "parts": [] + }, + "fullPath": { + "head": "/users/", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "availability": null, + "docs": "The maximum number of results to return." + } + ], + "headers": [], + "requestBody": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "ListUsersRequest", + "camelCase": { + "unsafeName": "listUsersRequest", + "safeName": "listUsersRequest" + }, + "snakeCase": { + "unsafeName": "list_users_request", + "safeName": "list_users_request" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_USERS_REQUEST", + "safeName": "LIST_USERS_REQUEST" + }, + "pascalCase": { + "unsafeName": "ListUsersRequest", + "safeName": "ListUsersRequest" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User", + "default": null, + "inline": null + } + } + }, + "docs": null + } + }, + "status-code": null + }, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "url": "/users/", + "rootPathParameters": [], + "servicePathParameters": [], + "endpointPathParameters": [], + "serviceHeaders": [], + "endpointHeaders": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "shape": { + "type": "single" + } + } + ], + "request": null, + "name": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "value": { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id" + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + } + }, + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + } + }, + { + "name": { + "name": { + "originalName": "age", + "camelCase": { + "unsafeName": "age", + "safeName": "age" + }, + "snakeCase": { + "unsafeName": "age", + "safeName": "age" + }, + "screamingSnakeCase": { + "unsafeName": "AGE", + "safeName": "AGE" + }, + "pascalCase": { + "unsafeName": "Age", + "safeName": "Age" + } + }, + "wireValue": "age" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "originalTypeDeclaration": { + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + } + } + ] + } + }, + "jsonExample": { + "id": "string", + "name": "string", + "age": 1 + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "id": "string", + "name": "string", + "age": 1 + } + ] + } + } + }, + "id": "dc75a53bc505b5327a42531505cd5d21a02f6901", + "docs": null + } + } + ], + "pagination": null, + "availability": null, + "docs": "List all users." + } + ] + }, + "service_user/events": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "file": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/users/events", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_user/events.listEvents", + "name": { + "originalName": "listEvents", + "camelCase": { + "unsafeName": "listEvents", + "safeName": "listEvents" + }, + "snakeCase": { + "unsafeName": "list_events", + "safeName": "list_events" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_EVENTS", + "safeName": "LIST_EVENTS" + }, + "pascalCase": { + "unsafeName": "ListEvents", + "safeName": "ListEvents" + } + }, + "displayName": null, + "auth": false, + "idempotent": false, + "baseUrl": null, + "method": "GET", + "path": { + "head": "/", + "parts": [] + }, + "fullPath": { + "head": "/users/events/", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "availability": null, + "docs": "The maximum number of results to return." + } + ], + "headers": [], + "requestBody": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "ListUserEventsRequest", + "camelCase": { + "unsafeName": "listUserEventsRequest", + "safeName": "listUserEventsRequest" + }, + "snakeCase": { + "unsafeName": "list_user_events_request", + "safeName": "list_user_events_request" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_USER_EVENTS_REQUEST", + "safeName": "LIST_USER_EVENTS_REQUEST" + }, + "pascalCase": { + "unsafeName": "ListUserEventsRequest", + "safeName": "ListUserEventsRequest" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "Event", + "camelCase": { + "unsafeName": "event", + "safeName": "event" + }, + "snakeCase": { + "unsafeName": "event", + "safeName": "event" + }, + "screamingSnakeCase": { + "unsafeName": "EVENT", + "safeName": "EVENT" + }, + "pascalCase": { + "unsafeName": "Event", + "safeName": "Event" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "file": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + }, + "typeId": "type_user/events:Event", + "default": null, + "inline": null + } + } + }, + "docs": null + } + }, + "status-code": null + }, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "url": "/users/events/", + "rootPathParameters": [], + "servicePathParameters": [], + "endpointPathParameters": [], + "serviceHeaders": [], + "endpointHeaders": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "shape": { + "type": "single" + } + } + ], + "request": null, + "name": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Event", + "camelCase": { + "unsafeName": "event", + "safeName": "event" + }, + "snakeCase": { + "unsafeName": "event", + "safeName": "event" + }, + "screamingSnakeCase": { + "unsafeName": "EVENT", + "safeName": "EVENT" + }, + "pascalCase": { + "unsafeName": "Event", + "safeName": "Event" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "file": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + }, + "typeId": "type_user/events:Event" + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "value": { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id" + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Event", + "camelCase": { + "unsafeName": "event", + "safeName": "event" + }, + "snakeCase": { + "unsafeName": "event", + "safeName": "event" + }, + "screamingSnakeCase": { + "unsafeName": "EVENT", + "safeName": "EVENT" + }, + "pascalCase": { + "unsafeName": "Event", + "safeName": "Event" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "file": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + }, + "typeId": "type_user/events:Event" + } + }, + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Event", + "camelCase": { + "unsafeName": "event", + "safeName": "event" + }, + "snakeCase": { + "unsafeName": "event", + "safeName": "event" + }, + "screamingSnakeCase": { + "unsafeName": "EVENT", + "safeName": "EVENT" + }, + "pascalCase": { + "unsafeName": "Event", + "safeName": "Event" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "file": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + }, + "typeId": "type_user/events:Event" + } + } + ] + } + }, + "jsonExample": { + "id": "string", + "name": "string" + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "Event", + "camelCase": { + "unsafeName": "event", + "safeName": "event" + }, + "snakeCase": { + "unsafeName": "event", + "safeName": "event" + }, + "screamingSnakeCase": { + "unsafeName": "EVENT", + "safeName": "EVENT" + }, + "pascalCase": { + "unsafeName": "Event", + "safeName": "Event" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "file": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + }, + "typeId": "type_user/events:Event", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "id": "string", + "name": "string" + } + ] + } + } + }, + "id": "bce0197b7031fd2abae4069f7dc90597b5eaa8b4", + "docs": null + } + } + ], + "pagination": null, + "availability": null, + "docs": "List all user events." + } + ] + }, + "service_user/events/metadata": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + }, + { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "file": { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/users/events/metadata", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_user/events/metadata.getMetadata", + "name": { + "originalName": "getMetadata", + "camelCase": { + "unsafeName": "getMetadata", + "safeName": "getMetadata" + }, + "snakeCase": { + "unsafeName": "get_metadata", + "safeName": "get_metadata" + }, + "screamingSnakeCase": { + "unsafeName": "GET_METADATA", + "safeName": "GET_METADATA" + }, + "pascalCase": { + "unsafeName": "GetMetadata", + "safeName": "GetMetadata" + } + }, + "displayName": null, + "auth": false, + "idempotent": false, + "baseUrl": null, + "method": "GET", + "path": { + "head": "/", + "parts": [] + }, + "fullPath": { + "head": "/users/events/metadata/", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id", + "default": null, + "inline": null + }, + "allowMultiple": false, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "GetEventMetadataRequest", + "camelCase": { + "unsafeName": "getEventMetadataRequest", + "safeName": "getEventMetadataRequest" + }, + "snakeCase": { + "unsafeName": "get_event_metadata_request", + "safeName": "get_event_metadata_request" + }, + "screamingSnakeCase": { + "unsafeName": "GET_EVENT_METADATA_REQUEST", + "safeName": "GET_EVENT_METADATA_REQUEST" + }, + "pascalCase": { + "unsafeName": "GetEventMetadataRequest", + "safeName": "GetEventMetadataRequest" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "Metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + }, + { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "file": { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + }, + "typeId": "type_user/events/metadata:Metadata", + "default": null, + "inline": null + }, + "docs": null + } + }, + "status-code": null + }, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "url": "/users/events/metadata/", + "rootPathParameters": [], + "servicePathParameters": [], + "endpointPathParameters": [], + "serviceHeaders": [], + "endpointHeaders": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "value": { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id" + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "jsonExample": "string" + }, + "shape": { + "type": "single" + } + } + ], + "request": null, + "name": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + }, + { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "file": { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + }, + "typeId": "type_user/events/metadata:Metadata" + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "value": { + "shape": { + "type": "named", + "typeName": { + "name": { + "originalName": "Id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "typeId": "type_:Id" + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "jsonExample": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + }, + { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "file": { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + }, + "typeId": "type_user/events/metadata:Metadata" + } + }, + { + "name": { + "name": { + "originalName": "value", + "camelCase": { + "unsafeName": "value", + "safeName": "value" + }, + "snakeCase": { + "unsafeName": "value", + "safeName": "value" + }, + "screamingSnakeCase": { + "unsafeName": "VALUE", + "safeName": "VALUE" + }, + "pascalCase": { + "unsafeName": "Value", + "safeName": "Value" + } + }, + "wireValue": "value" + }, + "value": { + "shape": { + "type": "unknown", + "unknown": { + "key": "value" + } + }, + "jsonExample": { + "key": "value" + } + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + }, + { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "file": { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + }, + "typeId": "type_user/events/metadata:Metadata" + } + } + ] + } + }, + "jsonExample": { + "id": "string", + "value": { + "key": "value" + } + } + } + } + }, + "id": "554cbaab66fc783caeee42a0166684d038ff9d38", + "docs": null + } + } + ], + "pagination": null, + "availability": null, + "docs": "Get event metadata." + } + ] + } + }, + "constants": { + "errorInstanceIdKey": { + "name": { + "originalName": "errorInstanceId", + "camelCase": { + "unsafeName": "errorInstanceID", + "safeName": "errorInstanceID" + }, + "snakeCase": { + "unsafeName": "error_instance_id", + "safeName": "error_instance_id" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_INSTANCE_ID", + "safeName": "ERROR_INSTANCE_ID" + }, + "pascalCase": { + "unsafeName": "ErrorInstanceID", + "safeName": "ErrorInstanceID" + } + }, + "wireValue": "errorInstanceId" + } + }, + "environments": null, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": { + "service_user/events/metadata": [ + "type_:Id", + "type_user/events/metadata:Metadata" + ], + "service_organization": [ + "type_organization:Organization", + "type_organization:CreateOrganizationRequest" + ], + "service_user": [ + "type_user:User" + ], + "service_user/events": [ + "type_user/events:Event" + ] + }, + "sharedTypes": [] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "subpackages": { + "subpackage_organization": { + "name": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + ], + "packagePath": [], + "file": { + "originalName": "organization", + "camelCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "snakeCase": { + "unsafeName": "organization", + "safeName": "organization" + }, + "screamingSnakeCase": { + "unsafeName": "ORGANIZATION", + "safeName": "ORGANIZATION" + }, + "pascalCase": { + "unsafeName": "Organization", + "safeName": "Organization" + } + } + }, + "service": "service_organization", + "types": [ + "type_organization:Organization", + "type_organization:CreateOrganizationRequest" + ], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_user": { + "name": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "service": "service_user", + "types": [ + "type_user:User" + ], + "errors": [], + "subpackages": [ + "subpackage_user/events" + ], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_user/events": { + "name": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "file": { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + }, + "service": "service_user/events", + "types": [ + "type_user/events:Event" + ], + "errors": [], + "subpackages": [ + "subpackage_user/events/metadata" + ], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_user/events/metadata": { + "name": { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + }, + { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + ], + "packagePath": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + { + "originalName": "events", + "camelCase": { + "unsafeName": "events", + "safeName": "events" + }, + "snakeCase": { + "unsafeName": "events", + "safeName": "events" + }, + "screamingSnakeCase": { + "unsafeName": "EVENTS", + "safeName": "EVENTS" + }, + "pascalCase": { + "unsafeName": "Events", + "safeName": "Events" + } + } + ], + "file": { + "originalName": "metadata", + "camelCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "snakeCase": { + "unsafeName": "metadata", + "safeName": "metadata" + }, + "screamingSnakeCase": { + "unsafeName": "METADATA", + "safeName": "METADATA" + }, + "pascalCase": { + "unsafeName": "Metadata", + "safeName": "Metadata" + } + } + }, + "service": "service_user/events/metadata", + "types": [ + "type_user/events/metadata:Metadata" + ], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + } + }, + "rootPackage": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "websocket": null, + "service": null, + "types": [ + "type_:Id" + ], + "errors": [], + "subpackages": [ + "subpackage_organization", + "subpackage_user" + ], + "webhooks": null, + "navigationConfig": null, + "hasEndpointsInTree": true, + "docs": null + }, + "sdkConfig": { + "isAuthMandatory": false, + "hasStreamingEndpoints": false, + "hasPaginatedEndpoints": false, + "hasFileDownloadEndpoints": false, + "platformHeaders": { + "language": "X-Fern-Language", + "sdkName": "X-Fern-SDK-Name", + "sdkVersion": "X-Fern-SDK-Version", + "userAgent": null + } + } +} \ No newline at end of file diff --git a/packages/cli/openapi-ir-to-fern/src/FernDefinitionDirectory.ts b/packages/cli/openapi-ir-to-fern/src/FernDefinitionDirectory.ts new file mode 100644 index 00000000000..7dbfa26e875 --- /dev/null +++ b/packages/cli/openapi-ir-to-fern/src/FernDefinitionDirectory.ts @@ -0,0 +1,53 @@ +import { RelativeFilePath } from "@fern-api/fs-utils"; +import { RawSchemas } from "@fern-api/fern-definition-schema"; +import path from "path"; + +export class FernDefinitionDirectory { + private files: Record = {}; + private directories: Record = {}; + + public getAllFiles(): Record { + const files: Record = {}; + + const walk = (root: FernDefinitionDirectory, currentPath?: string) => { + for (const [relativeFilePath, definition] of Object.entries(root.files)) { + const fullRelativeFilePath = + currentPath != null + ? RelativeFilePath.of(`${currentPath}${path.sep}${relativeFilePath}`) + : RelativeFilePath.of(relativeFilePath); + files[fullRelativeFilePath] = definition; + } + const sortedDirectories = Object.keys(root.directories).sort(); + for (const directory of sortedDirectories) { + const nextPath = currentPath != null ? `${currentPath}${path.sep}${directory}` : directory; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const nextDirectory = root.directories[directory]!; + walk(nextDirectory, nextPath); + } + }; + + walk(this); + + return files; + } + + public getOrCreateFile(relativeFilePath: RelativeFilePath): RawSchemas.DefinitionFileSchema { + return this.getOrCreateFileRecursive(relativeFilePath.split(path.sep)); + } + + private getOrCreateFileRecursive(pathParts: string[]): RawSchemas.DefinitionFileSchema { + if (pathParts.length === 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return (this.files[RelativeFilePath.of(pathParts[0]!)] ??= {}); + } + const [directory, ...remainingPath] = pathParts; + if (directory == null) { + throw new Error(`Internal error; cannot add file with path: ${pathParts}`); + } + if (!this.directories[directory]) { + this.directories[directory] = new FernDefinitionDirectory(); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.directories[directory]!.getOrCreateFileRecursive(remainingPath); + } +} diff --git a/packages/cli/openapi-ir-to-fern/src/FernDefnitionBuilder.ts b/packages/cli/openapi-ir-to-fern/src/FernDefnitionBuilder.ts index 3c710c2ebde..c4fdbd93d3c 100644 --- a/packages/cli/openapi-ir-to-fern/src/FernDefnitionBuilder.ts +++ b/packages/cli/openapi-ir-to-fern/src/FernDefnitionBuilder.ts @@ -5,6 +5,7 @@ import { RawSchemas, RootApiFileSchema, visitRawEnvironmentDeclaration } from "@ import { camelCase, isEqual } from "lodash-es"; import path, { basename, extname } from "path"; import { convertToSourceSchema } from "./utils/convertToSourceSchema"; +import { FernDefinitionDirectory } from "./FernDefinitionDirectory"; export interface FernDefinitionBuilder { addNavigation({ navigation }: { navigation: string[] }): void; @@ -86,9 +87,9 @@ export interface FernDefinition { } export class FernDefinitionBuilderImpl implements FernDefinitionBuilder { + private root: FernDefinitionDirectory; private rootApiFile: RawSchemas.RootApiFileSchema; private packageMarkerFile: RawSchemas.PackageMarkerFileSchema = {}; - private definitionFiles: Record = {}; private basePath: string | undefined = undefined; public constructor( @@ -96,6 +97,7 @@ export class FernDefinitionBuilderImpl implements FernDefinitionBuilder { private readonly modifyBasePaths: boolean, public readonly enableUniqueErrorsPerEndpoint: boolean ) { + this.root = new FernDefinitionDirectory(); this.rootApiFile = { name: "api", "error-discrimination": { @@ -404,6 +406,7 @@ export class FernDefinitionBuilderImpl implements FernDefinitionBuilder { } public build(): FernDefinition { + const definitionFiles = this.root.getAllFiles(); if (this.modifyBasePaths) { const basePath = getSharedEnvironmentBasePath(this.rootApiFile); @@ -426,7 +429,7 @@ export class FernDefinitionBuilderImpl implements FernDefinitionBuilder { } // subsitute definition files - for (const [_, file] of Object.entries(this.definitionFiles)) { + for (const file of Object.values(definitionFiles)) { if (file.service != null) { file.service = { ...file.service, @@ -497,7 +500,7 @@ export class FernDefinitionBuilderImpl implements FernDefinitionBuilder { } // subsitute definition files - for (const [_, file] of Object.entries(this.definitionFiles)) { + for (const file of Object.values(definitionFiles)) { if (file.service != null) { file.service = { ...file.service, @@ -520,7 +523,7 @@ export class FernDefinitionBuilderImpl implements FernDefinitionBuilder { const definition: FernDefinition = { rootApiFile: this.rootApiFile, packageMarkerFile: this.packageMarkerFile, - definitionFiles: this.definitionFiles + definitionFiles }; return definition; } @@ -531,7 +534,7 @@ export class FernDefinitionBuilderImpl implements FernDefinitionBuilder { if (file === FERN_PACKAGE_MARKER_FILENAME) { return this.packageMarkerFile; } else { - return (this.definitionFiles[file] ??= {}); + return this.root.getOrCreateFile(file); } } diff --git a/packages/cli/openapi-ir-to-fern/src/__test__/FernDefinitionDirectory.test.ts b/packages/cli/openapi-ir-to-fern/src/__test__/FernDefinitionDirectory.test.ts new file mode 100644 index 00000000000..a5dbffb84ec --- /dev/null +++ b/packages/cli/openapi-ir-to-fern/src/__test__/FernDefinitionDirectory.test.ts @@ -0,0 +1,71 @@ +import { RawSchemas } from "@fern-api/fern-definition-schema"; +import { RelativeFilePath } from "@fern-api/fs-utils"; +import { FernDefinitionDirectory } from "../FernDefinitionDirectory"; + +interface TestCase { + description: string; + giveFilepaths: string[]; + wantFiles: Record; +} + +describe("FernDefinitionDirectory", () => { + const testCases: TestCase[] = [ + { + description: "empty", + giveFilepaths: [], + wantFiles: {} + }, + { + description: "single file", + giveFilepaths: ["example.yml"], + wantFiles: { + "example.yml": {} + } + }, + { + description: "single directory", + giveFilepaths: ["one/a.yml"], + wantFiles: { + "one/a.yml": {} + } + }, + { + description: "single directory, multiple files", + giveFilepaths: ["one/b.yml", "one/a.yml"], + wantFiles: { + "one/a.yml": {}, + "one/b.yml": {} + } + }, + { + description: "multiple directory, multiple files", + giveFilepaths: ["one/b.yml", "two/foo/d.yml", "two/foo/c.yml", "one/a.yml"], + wantFiles: { + "one/a.yml": {}, + "one/b.yml": {}, + "two/foo/c.yml": {}, + "two/foo/d.yml": {} + } + }, + { + description: "file/directory match", + giveFilepaths: ["user/events/metadata.yml", "user/events.yml", "user.yml", "events.yml"], + wantFiles: { + "events.yml": {}, + "user.yml": {}, + "user/events.yml": {}, + "user/events/metadata.yml": {} + } + } + ]; + + testCases.forEach((testCase) => { + it(`"${testCase.description}"`, async () => { + const root = new FernDefinitionDirectory(); + for (const filepath of testCase.giveFilepaths) { + root.getOrCreateFile(RelativeFilePath.of(filepath)); + } + expect(root.getAllFiles()).toEqual(testCase.wantFiles); + }); + }); +}); diff --git a/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/convertIrToFdrApi.test.ts.snap b/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/convertIrToFdrApi.test.ts.snap index 9ba1dafdad7..9c94998b7ca 100644 --- a/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/convertIrToFdrApi.test.ts.snap +++ b/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/convertIrToFdrApi.test.ts.snap @@ -15587,6 +15587,523 @@ exports[`fdr test definitions > mixed-case 1`] = ` } `; +exports[`fdr test definitions > mixed-file-directory 1`] = ` +{ + "auth": undefined, + "globalHeaders": [], + "rootPackage": { + "endpoints": [], + "pointsTo": undefined, + "subpackages": [ + "subpackage_organization", + "subpackage_user", + ], + "types": [ + "type_:Id", + ], + "webhooks": [], + "websockets": [], + }, + "snippetsConfiguration": {}, + "subpackages": { + "subpackage_organization": { + "description": undefined, + "displayName": undefined, + "endpoints": [ + { + "auth": false, + "availability": undefined, + "defaultEnvironment": undefined, + "description": "Create a new organization.", + "environments": undefined, + "errors": undefined, + "errorsV2": [], + "examples": [], + "headers": [], + "id": "create", + "method": "POST", + "name": "Create", + "originalEndpointId": "endpoint_organization.create", + "path": { + "parts": [ + { + "type": "literal", + "value": "/organizations", + }, + { + "type": "literal", + "value": "/", + }, + ], + "pathParameters": [], + }, + "queryParameters": [], + "request": { + "type": { + "contentType": "application/json", + "shape": { + "type": "reference", + "value": { + "type": "id", + "value": "type_organization:CreateOrganizationRequest", + }, + }, + "type": "json", + }, + }, + "response": { + "statusCode": undefined, + "type": { + "type": "reference", + "value": { + "type": "id", + "value": "type_organization:Organization", + }, + }, + }, + }, + ], + "name": "organization", + "pointsTo": undefined, + "subpackageId": "subpackage_organization", + "subpackages": [], + "types": [ + "type_organization:Organization", + "type_organization:CreateOrganizationRequest", + ], + "webhooks": [], + "websockets": [], + }, + "subpackage_user": { + "description": undefined, + "displayName": undefined, + "endpoints": [ + { + "auth": false, + "availability": undefined, + "defaultEnvironment": undefined, + "description": "List all users.", + "environments": undefined, + "errors": undefined, + "errorsV2": [], + "examples": [], + "headers": [], + "id": "list", + "method": "GET", + "name": "List", + "originalEndpointId": "endpoint_user.list", + "path": { + "parts": [ + { + "type": "literal", + "value": "/users", + }, + { + "type": "literal", + "value": "/", + }, + ], + "pathParameters": [], + }, + "queryParameters": [ + { + "availability": undefined, + "description": "The maximum number of results to return.", + "key": "limit", + "type": { + "itemType": { + "type": "primitive", + "value": { + "default": undefined, + "maximum": undefined, + "minimum": undefined, + "type": "integer", + }, + }, + "type": "optional", + }, + }, + ], + "request": undefined, + "response": { + "statusCode": undefined, + "type": { + "type": "reference", + "value": { + "itemType": { + "type": "id", + "value": "type_user:User", + }, + "type": "list", + }, + }, + }, + }, + ], + "name": "user", + "pointsTo": undefined, + "subpackageId": "subpackage_user", + "subpackages": [ + "subpackage_user/events", + ], + "types": [ + "type_user:User", + ], + "webhooks": [], + "websockets": [], + }, + "subpackage_user/events": { + "description": undefined, + "displayName": undefined, + "endpoints": [ + { + "auth": false, + "availability": undefined, + "defaultEnvironment": undefined, + "description": "List all user events.", + "environments": undefined, + "errors": undefined, + "errorsV2": [], + "examples": [], + "headers": [], + "id": "listEvents", + "method": "GET", + "name": "List Events", + "originalEndpointId": "endpoint_user/events.listEvents", + "path": { + "parts": [ + { + "type": "literal", + "value": "/users/events", + }, + { + "type": "literal", + "value": "/", + }, + ], + "pathParameters": [], + }, + "queryParameters": [ + { + "availability": undefined, + "description": "The maximum number of results to return.", + "key": "limit", + "type": { + "itemType": { + "type": "primitive", + "value": { + "default": undefined, + "maximum": undefined, + "minimum": undefined, + "type": "integer", + }, + }, + "type": "optional", + }, + }, + ], + "request": undefined, + "response": { + "statusCode": undefined, + "type": { + "type": "reference", + "value": { + "itemType": { + "type": "id", + "value": "type_user/events:Event", + }, + "type": "list", + }, + }, + }, + }, + ], + "name": "events", + "pointsTo": undefined, + "subpackageId": "subpackage_user/events", + "subpackages": [ + "subpackage_user/events/metadata", + ], + "types": [ + "type_user/events:Event", + ], + "webhooks": [], + "websockets": [], + }, + "subpackage_user/events/metadata": { + "description": undefined, + "displayName": undefined, + "endpoints": [ + { + "auth": false, + "availability": undefined, + "defaultEnvironment": undefined, + "description": "Get event metadata.", + "environments": undefined, + "errors": undefined, + "errorsV2": [], + "examples": [], + "headers": [], + "id": "getMetadata", + "method": "GET", + "name": "Get Metadata", + "originalEndpointId": "endpoint_user/events/metadata.getMetadata", + "path": { + "parts": [ + { + "type": "literal", + "value": "/users/events/metadata", + }, + { + "type": "literal", + "value": "/", + }, + ], + "pathParameters": [], + }, + "queryParameters": [ + { + "availability": undefined, + "description": undefined, + "key": "id", + "type": { + "type": "id", + "value": "type_:Id", + }, + }, + ], + "request": undefined, + "response": { + "statusCode": undefined, + "type": { + "type": "reference", + "value": { + "type": "id", + "value": "type_user/events/metadata:Metadata", + }, + }, + }, + }, + ], + "name": "metadata", + "pointsTo": undefined, + "subpackageId": "subpackage_user/events/metadata", + "subpackages": [], + "types": [ + "type_user/events/metadata:Metadata", + ], + "webhooks": [], + "websockets": [], + }, + }, + "types": { + "type_:Id": { + "availability": undefined, + "description": undefined, + "name": "Id", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "default": undefined, + "maxLength": undefined, + "minLength": undefined, + "regex": undefined, + "type": "string", + }, + }, + }, + }, + "type_organization:CreateOrganizationRequest": { + "availability": undefined, + "description": undefined, + "name": "CreateOrganizationRequest", + "shape": { + "extends": [], + "properties": [ + { + "availability": undefined, + "description": undefined, + "key": "name", + "valueType": { + "type": "primitive", + "value": { + "default": undefined, + "maxLength": undefined, + "minLength": undefined, + "regex": undefined, + "type": "string", + }, + }, + }, + ], + "type": "object", + }, + }, + "type_organization:Organization": { + "availability": undefined, + "description": undefined, + "name": "Organization", + "shape": { + "extends": [], + "properties": [ + { + "availability": undefined, + "description": undefined, + "key": "id", + "valueType": { + "type": "id", + "value": "type_:Id", + }, + }, + { + "availability": undefined, + "description": undefined, + "key": "name", + "valueType": { + "type": "primitive", + "value": { + "default": undefined, + "maxLength": undefined, + "minLength": undefined, + "regex": undefined, + "type": "string", + }, + }, + }, + { + "availability": undefined, + "description": undefined, + "key": "users", + "valueType": { + "itemType": { + "type": "id", + "value": "type_user:User", + }, + "type": "list", + }, + }, + ], + "type": "object", + }, + }, + "type_user/events/metadata:Metadata": { + "availability": undefined, + "description": undefined, + "name": "Metadata", + "shape": { + "extends": [], + "properties": [ + { + "availability": undefined, + "description": undefined, + "key": "id", + "valueType": { + "type": "id", + "value": "type_:Id", + }, + }, + { + "availability": undefined, + "description": undefined, + "key": "value", + "valueType": { + "type": "unknown", + }, + }, + ], + "type": "object", + }, + }, + "type_user/events:Event": { + "availability": undefined, + "description": undefined, + "name": "Event", + "shape": { + "extends": [], + "properties": [ + { + "availability": undefined, + "description": undefined, + "key": "id", + "valueType": { + "type": "id", + "value": "type_:Id", + }, + }, + { + "availability": undefined, + "description": undefined, + "key": "name", + "valueType": { + "type": "primitive", + "value": { + "default": undefined, + "maxLength": undefined, + "minLength": undefined, + "regex": undefined, + "type": "string", + }, + }, + }, + ], + "type": "object", + }, + }, + "type_user:User": { + "availability": undefined, + "description": undefined, + "name": "User", + "shape": { + "extends": [], + "properties": [ + { + "availability": undefined, + "description": undefined, + "key": "id", + "valueType": { + "type": "id", + "value": "type_:Id", + }, + }, + { + "availability": undefined, + "description": undefined, + "key": "name", + "valueType": { + "type": "primitive", + "value": { + "default": undefined, + "maxLength": undefined, + "minLength": undefined, + "regex": undefined, + "type": "string", + }, + }, + }, + { + "availability": undefined, + "description": undefined, + "key": "age", + "valueType": { + "type": "primitive", + "value": { + "default": undefined, + "maximum": undefined, + "minimum": undefined, + "type": "integer", + }, + }, + }, + ], + "type": "object", + }, + }, + }, +} +`; + exports[`fdr test definitions > multi-line-docs 1`] = ` { "auth": undefined, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df13cb41897..5f0a67ffb86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3102,10 +3102,6 @@ importers: specifier: ^2.0.5 version: 2.0.5(@types/node@18.7.18)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5) - packages/cli/cli/dist/dev: {} - - packages/cli/cli/dist/prod: {} - packages/cli/configuration: dependencies: '@fern-api/core-utils': diff --git a/seed/csharp-model/mixed-file-directory/.github/workflows/ci.yml b/seed/csharp-model/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..38549d48d01 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - uses: actions/checkout@master + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Install tools + run: | + dotnet tool restore + + - name: Build Release + run: dotnet build src -c Release /p:ContinuousIntegrationBuild=true + + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - uses: actions/checkout@master + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Install tools + run: | + dotnet tool restore + + - name: Run Tests + run: | + dotnet test src + + + publish: + needs: [compile] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Publish + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} + run: | + dotnet pack src -c Release + dotnet nuget push src/SeedMixedFileDirectory/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" diff --git a/seed/csharp-model/mixed-file-directory/.gitignore b/seed/csharp-model/mixed-file-directory/.gitignore new file mode 100644 index 00000000000..5e57f18055d --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/seed/csharp-model/mixed-file-directory/.mock/definition/__package__.yml b/seed/csharp-model/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/csharp-model/mixed-file-directory/.mock/definition/api.yml b/seed/csharp-model/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/csharp-model/mixed-file-directory/.mock/definition/organization.yml b/seed/csharp-model/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/csharp-model/mixed-file-directory/.mock/definition/user.yml b/seed/csharp-model/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/csharp-model/mixed-file-directory/.mock/definition/user/events.yml b/seed/csharp-model/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/csharp-model/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/csharp-model/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/csharp-model/mixed-file-directory/.mock/fern.config.json b/seed/csharp-model/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/csharp-model/mixed-file-directory/.mock/generators.yml b/seed/csharp-model/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/csharp-model/mixed-file-directory/snippet-templates.json b/seed/csharp-model/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/csharp-model/mixed-file-directory/snippet.json b/seed/csharp-model/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/SeedMixedFileDirectory.Test.csproj b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/SeedMixedFileDirectory.Test.csproj new file mode 100644 index 00000000000..165484bd569 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/SeedMixedFileDirectory.Test.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/CollectionItemSerializer.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/CollectionItemSerializer.cs new file mode 100644 index 00000000000..87d4d84d1dc --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/CollectionItemSerializer.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMixedFileDirectory.Core; + +/// +/// Json collection converter. +/// +/// Type of item to convert. +/// Converter to use for individual items. +internal class CollectionItemSerializer + : JsonConverter> + where TConverterType : JsonConverter +{ + /// + /// Reads a json string and deserializes it into an object. + /// + /// Json reader. + /// Type to convert. + /// Serializer options. + /// Created object. + public override IEnumerable? Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + + var returnValue = new List(); + + while (reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + var item = (TDatatype)( + JsonSerializer.Deserialize(ref reader, typeof(TDatatype), jsonSerializerOptions) + ?? throw new Exception( + $"Failed to deserialize collection item of type {typeof(TDatatype)}" + ) + ); + returnValue.Add(item); + } + + reader.Read(); + } + + return returnValue; + } + + /// + /// Writes a json string. + /// + /// Json writer. + /// Value to write. + /// Serializer options. + public override void Write( + Utf8JsonWriter writer, + IEnumerable? value, + JsonSerializerOptions options + ) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + + writer.WriteStartArray(); + + foreach (var data in value) + { + JsonSerializer.Serialize(writer, data, jsonSerializerOptions); + } + + writer.WriteEndArray(); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/Constants.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/Constants.cs new file mode 100644 index 00000000000..5c0a541c4fd --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/Constants.cs @@ -0,0 +1,7 @@ +namespace SeedMixedFileDirectory.Core; + +internal static class Constants +{ + public const string DateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffK"; + public const string DateFormat = "yyyy-MM-dd"; +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/DateTimeSerializer.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/DateTimeSerializer.cs new file mode 100644 index 00000000000..4f0a2c657b3 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/DateTimeSerializer.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMixedFileDirectory.Core; + +internal class DateTimeSerializer : JsonConverter +{ + public override DateTime Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + return DateTime.Parse(reader.GetString()!, null, DateTimeStyles.RoundtripKind); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(Constants.DateTimeFormat)); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs new file mode 100644 index 00000000000..aa412ddce4d --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMixedFileDirectory.Core; + +internal static class JsonOptions +{ + public static readonly JsonSerializerOptions JsonSerializerOptions; + + static JsonOptions() + { + JsonSerializerOptions = new JsonSerializerOptions + { + Converters = { new DateTimeSerializer(), new OneOfSerializer() }, + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + } +} + +internal static class JsonUtils +{ + public static string Serialize(T obj) + { + return JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions); + } + + public static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, JsonOptions.JsonSerializerOptions)!; + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/OneOfSerializer.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/OneOfSerializer.cs new file mode 100644 index 00000000000..083cb8178f7 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/OneOfSerializer.cs @@ -0,0 +1,69 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; + +namespace SeedMixedFileDirectory.Core; + +internal class OneOfSerializer : JsonConverter +{ + public override IOneOf? Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType is JsonTokenType.Null) + return default; + + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) + { + try + { + var readerCopy = reader; + var result = JsonSerializer.Deserialize(ref readerCopy, type, options); + reader.Skip(); + return (IOneOf)cast.Invoke(null, [result])!; + } + catch (JsonException) { } + } + + throw new JsonException( + $"Cannot deserialize into one of the supported types for {typeToConvert}" + ); + } + + public override void Write(Utf8JsonWriter writer, IOneOf value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + + private static (System.Type type, MethodInfo cast)[] GetOneOfTypes(System.Type typeToConvert) + { + var casts = typeToConvert + .GetRuntimeMethods() + .Where(m => m.IsSpecialName && m.Name == "op_Implicit") + .ToArray(); + var type = typeToConvert; + while (type != null) + { + if ( + type.IsGenericType + && (type.Name.StartsWith("OneOf`") || type.Name.StartsWith("OneOfBase`")) + ) + { + return type.GetGenericArguments() + .Select(t => (t, casts.First(c => c.GetParameters()[0].ParameterType == t))) + .ToArray(); + } + + type = type.BaseType; + } + throw new InvalidOperationException($"{type} isn't OneOf or OneOfBase"); + } + + public override bool CanConvert(System.Type typeToConvert) + { + return typeof(IOneOf).IsAssignableFrom(typeToConvert); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/Version.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/Version.cs new file mode 100644 index 00000000000..6bf8ea8e377 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/Version.cs @@ -0,0 +1,6 @@ +namespace SeedMixedFileDirectory; + +internal class Version +{ + public const string Current = "0.0.1"; +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs new file mode 100644 index 00000000000..3f86e6d408b --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs @@ -0,0 +1,53 @@ +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMixedFileDirectory.Core; + +internal class StringEnumSerializer : JsonConverter + where TEnum : struct, System.Enum +{ + private readonly Dictionary _enumToString = new(); + private readonly Dictionary _stringToEnum = new(); + + public StringEnumSerializer() + { + var type = typeof(TEnum); + var values = Enum.GetValues(type); + + foreach (var value in values) + { + var enumValue = (TEnum)value; + var enumMember = type.GetMember(enumValue.ToString())[0]; + var attr = enumMember + .GetCustomAttributes(typeof(EnumMemberAttribute), false) + .Cast() + .FirstOrDefault(); + + var stringValue = + attr?.Value + ?? value.ToString() + ?? throw new Exception("Unexpected null enum toString value"); + + _enumToString.Add(enumValue, stringValue); + _stringToEnum.Add(stringValue, enumValue); + } + } + + public override TEnum Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(_enumToString[value]); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Organization/CreateOrganizationRequest.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Organization/CreateOrganizationRequest.cs new file mode 100644 index 00000000000..4fcaa5582cd --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Organization/CreateOrganizationRequest.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public record CreateOrganizationRequest +{ + [JsonPropertyName("name")] + public required string Name { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Organization.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Organization.cs new file mode 100644 index 00000000000..0c84627921c --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Organization.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public record Organization +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("users")] + public IEnumerable Users { get; set; } = new List(); + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectory.csproj b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectory.csproj new file mode 100644 index 00000000000..7fac8fd09df --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectory.csproj @@ -0,0 +1,50 @@ + + + + + net462;net8.0;net7.0;net6.0;netstandard2.0 + enable + false + 12 + enable + 0.0.1 + README.md + https://github.com/mixed-file-directory/fern + + + + true + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + <_Parameter1>SeedMixedFileDirectory.Test + + + + diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Event.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Event.cs new file mode 100644 index 00000000000..d66c4e6434a --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Event.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.User; + +public record Event +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Metadata.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Metadata.cs new file mode 100644 index 00000000000..4529e495f9c --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Metadata.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.User.Events; + +public record Metadata +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("value")] + public required object Value { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/User.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/User.cs new file mode 100644 index 00000000000..fa1e2e3a9b5 --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/User/User.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public record User +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("age")] + public required int Age { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/seed.yml b/seed/csharp-model/seed.yml index ea5b14c450c..b5e0c55caa4 100644 --- a/seed/csharp-model/seed.yml +++ b/seed/csharp-model/seed.yml @@ -34,4 +34,5 @@ scripts: commands: - dotnet build src -c Release /p:ContinuousIntegrationBuild=true allowedFailures: + - mixed-file-directory - objects-with-imports diff --git a/seed/csharp-sdk/mixed-file-directory/.github/workflows/ci.yml b/seed/csharp-sdk/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..38549d48d01 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - uses: actions/checkout@master + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Install tools + run: | + dotnet tool restore + + - name: Build Release + run: dotnet build src -c Release /p:ContinuousIntegrationBuild=true + + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - uses: actions/checkout@master + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Install tools + run: | + dotnet tool restore + + - name: Run Tests + run: | + dotnet test src + + + publish: + needs: [compile] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Publish + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} + run: | + dotnet pack src -c Release + dotnet nuget push src/SeedMixedFileDirectory/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" diff --git a/seed/csharp-sdk/mixed-file-directory/.gitignore b/seed/csharp-sdk/mixed-file-directory/.gitignore new file mode 100644 index 00000000000..5e57f18055d --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/seed/csharp-sdk/mixed-file-directory/.mock/definition/__package__.yml b/seed/csharp-sdk/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/csharp-sdk/mixed-file-directory/.mock/definition/api.yml b/seed/csharp-sdk/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/csharp-sdk/mixed-file-directory/.mock/definition/organization.yml b/seed/csharp-sdk/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/csharp-sdk/mixed-file-directory/.mock/definition/user.yml b/seed/csharp-sdk/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/csharp-sdk/mixed-file-directory/.mock/definition/user/events.yml b/seed/csharp-sdk/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/csharp-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/csharp-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/csharp-sdk/mixed-file-directory/.mock/fern.config.json b/seed/csharp-sdk/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/csharp-sdk/mixed-file-directory/.mock/generators.yml b/seed/csharp-sdk/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/csharp-sdk/mixed-file-directory/README.md b/seed/csharp-sdk/mixed-file-directory/README.md new file mode 100644 index 00000000000..26ebaef952b --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/README.md @@ -0,0 +1,87 @@ +# Seed C# Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-SDK%20generated%20by%20Fern-brightgreen)](https://github.com/fern-api/fern) +[![nuget shield](https://img.shields.io/nuget/v/SeedMixedFileDirectory)](https://nuget.org/packages/SeedMixedFileDirectory) + +The Seed C# library provides convenient access to the Seed API from C#. + +## Installation + +```sh +nuget install SeedMixedFileDirectory +``` + +## Usage + +Instantiate and use the client with the following: + +```csharp +using SeedMixedFileDirectory; + +var client = new SeedMixedFileDirectoryClient(); +await client.Organization.CreateAsync(new CreateOrganizationRequest { Name = "string" }); +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```csharp +using SeedMixedFileDirectory; + +try { + var response = await client.Organization.CreateAsync(...); +} catch (SeedMixedFileDirectoryApiException e) { + System.Console.WriteLine(e.Body); + System.Console.WriteLine(e.StatusCode); +} +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retriable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retriable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `MaxRetries` request option to configure this behavior. + +```csharp +var response = await client.Organization.CreateAsync( + ..., + new RequestOptions { + MaxRetries: 0 // Override MaxRetries at the request level + } +); +``` + +### Timeouts + +The SDK defaults to a 30 second timeout. Use the `Timeout` option to configure this behavior. + +```csharp +var response = await client.Organization.CreateAsync( + ..., + new RequestOptions { + Timeout: TimeSpan.FromSeconds(3) // Override timeout to 3s + } +); +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! \ No newline at end of file diff --git a/seed/csharp-sdk/mixed-file-directory/reference.md b/seed/csharp-sdk/mixed-file-directory/reference.md new file mode 100644 index 00000000000..4c43fcdfa12 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/reference.md @@ -0,0 +1,220 @@ +# Reference +## Organization +
client.Organization.CreateAsync(CreateOrganizationRequest { ... }) -> Organization +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a new organization. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Organization.CreateAsync(new CreateOrganizationRequest { Name = "string" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `CreateOrganizationRequest` + +
+
+
+
+ + +
+
+
+ +## User +
client.User.ListAsync(ListUsersRequest { ... }) -> IEnumerable +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List all users. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.User.ListAsync(new ListUsersRequest { Limit = 1 }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `ListUsersRequest` + +
+
+
+
+ + +
+
+
+ +## User Events +
client.User.Events.ListEventsAsync(ListUserEventsRequest { ... }) -> IEnumerable +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List all user events. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.User.Events.ListEventsAsync(new ListUserEventsRequest { Limit = 1 }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `ListUserEventsRequest` + +
+
+
+
+ + +
+
+
+ +## User Events Metadata +
client.User.Events.Metadata.GetMetadataAsync(GetEventMetadataRequest { ... }) -> Metadata +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get event metadata. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.User.Events.Metadata.GetMetadataAsync(new GetEventMetadataRequest { Id = "string" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `GetEventMetadataRequest` + +
+
+
+
+ + +
+
+
diff --git a/seed/csharp-sdk/mixed-file-directory/snippet-templates.json b/seed/csharp-sdk/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/csharp-sdk/mixed-file-directory/snippet.json b/seed/csharp-sdk/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e1864585a4a --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/snippet.json @@ -0,0 +1,53 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": null, + "id": { + "path": "/organizations/", + "method": "POST", + "identifier_override": "endpoint_organization.create" + }, + "snippet": { + "type": "typescript", + "client": "using SeedMixedFileDirectory;\n\nvar client = new SeedMixedFileDirectoryClient();\nawait client.Organization.CreateAsync(new CreateOrganizationRequest { Name = \"string\" });\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/users/", + "method": "GET", + "identifier_override": "endpoint_user.list" + }, + "snippet": { + "type": "typescript", + "client": "using SeedMixedFileDirectory;\n\nvar client = new SeedMixedFileDirectoryClient();\nawait client.User.ListAsync(new ListUsersRequest { Limit = 1 });\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/users/events/", + "method": "GET", + "identifier_override": "endpoint_user/events.listEvents" + }, + "snippet": { + "type": "typescript", + "client": "using SeedMixedFileDirectory.User;\nusing SeedMixedFileDirectory;\n\nvar client = new SeedMixedFileDirectoryClient();\nawait client.User.Events.ListEventsAsync(new ListUserEventsRequest { Limit = 1 });\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/users/events/metadata/", + "method": "GET", + "identifier_override": "endpoint_user/events/metadata.getMetadata" + }, + "snippet": { + "type": "typescript", + "client": "using SeedMixedFileDirectory.User.Events;\nusing SeedMixedFileDirectory;\n\nvar client = new SeedMixedFileDirectoryClient();\nawait client.User.Events.Metadata.GetMetadataAsync(new GetEventMetadataRequest { Id = \"string\" });\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/RawClientTests.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/RawClientTests.cs new file mode 100644 index 00000000000..baae38a585b --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/RawClientTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Net.Http; +using FluentAssertions; +using NUnit.Framework; +using SeedMixedFileDirectory.Core; +using WireMock.Server; +using SystemTask = System.Threading.Tasks.Task; +using WireMockRequest = WireMock.RequestBuilders.Request; +using WireMockResponse = WireMock.ResponseBuilders.Response; + +namespace SeedMixedFileDirectory.Test.Core +{ + [TestFixture] + public class RawClientTests + { + private WireMockServer _server; + private HttpClient _httpClient; + private RawClient _rawClient; + private string _baseUrl; + private const int _maxRetries = 3; + + [SetUp] + public void SetUp() + { + _server = WireMockServer.Start(); + _baseUrl = _server.Url ?? ""; + _httpClient = new HttpClient { BaseAddress = new Uri(_baseUrl) }; + _rawClient = new RawClient( + new ClientOptions() { HttpClient = _httpClient, MaxRetries = _maxRetries } + ); + } + + [Test] + [TestCase(408)] + [TestCase(429)] + [TestCase(500)] + [TestCase(504)] + public async SystemTask MakeRequestAsync_ShouldRetry_OnRetryableStatusCodes(int statusCode) + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WhenStateIs("Server Error") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new RawClient.BaseApiRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.MakeRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.That(content, Is.EqualTo("Success")); + + Assert.That(_server.LogEntries.Count, Is.EqualTo(_maxRetries)); + } + + [Test] + [TestCase(400)] + [TestCase(409)] + public async SystemTask MakeRequestAsync_ShouldRetry_OnNonRetryableStatusCodes( + int statusCode + ) + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith( + WireMockResponse.Create().WithStatusCode(statusCode).WithBody("Failure") + ); + + var request = new RawClient.BaseApiRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.MakeRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(statusCode)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.That(content, Is.EqualTo("Failure")); + + Assert.That(_server.LogEntries.Count, Is.EqualTo(1)); + } + + [TearDown] + public void TearDown() + { + _server.Dispose(); + _httpClient.Dispose(); + } + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/SeedMixedFileDirectory.Test.csproj b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/SeedMixedFileDirectory.Test.csproj new file mode 100644 index 00000000000..165484bd569 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/SeedMixedFileDirectory.Test.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/TestClient.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/TestClient.cs new file mode 100644 index 00000000000..e0dc80aad9f --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/TestClient.cs @@ -0,0 +1,8 @@ +using NUnit.Framework; + +#nullable enable + +namespace SeedMixedFileDirectory.Test; + +[TestFixture] +public class TestClient { } diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/BaseMockServerTest.cs new file mode 100644 index 00000000000..6ac994265f8 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/BaseMockServerTest.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using SeedMixedFileDirectory; +using WireMock.Logging; +using WireMock.Server; +using WireMock.Settings; + +#nullable enable + +namespace SeedMixedFileDirectory.Test.Unit.MockServer; + +[SetUpFixture] +public class BaseMockServerTest +{ + protected static WireMockServer Server { get; set; } = null!; + + protected static SeedMixedFileDirectoryClient Client { get; set; } = null!; + + protected static RequestOptions RequestOptions { get; set; } = null!; + + [OneTimeSetUp] + public void GlobalSetup() + { + // Start the WireMock server + Server = WireMockServer.Start( + new WireMockServerSettings { Logger = new WireMockConsoleLogger() } + ); + + // Initialize the Client + Client = new SeedMixedFileDirectoryClient(); + + RequestOptions = new RequestOptions { BaseUrl = Server.Urls[0] }; + } + + [OneTimeTearDown] + public void GlobalTeardown() + { + Server.Stop(); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/CreateTest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/CreateTest.cs new file mode 100644 index 00000000000..60347c89a39 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/CreateTest.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using FluentAssertions.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using SeedMixedFileDirectory; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.Test.Unit.MockServer; + +[TestFixture] +public class CreateTest : BaseMockServerTest +{ + [Test] + public async Task MockServerTest() + { + const string requestJson = """ + { + "name": "string" + } + """; + + const string mockResponse = """ + { + "id": "string", + "name": "string", + "users": [ + { + "id": "string", + "name": "string", + "age": 1 + } + ] + } + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/organizations/") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Organization.CreateAsync( + new CreateOrganizationRequest { Name = "string" }, + RequestOptions + ); + JToken + .Parse(mockResponse) + .Should() + .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/GetMetadataTest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/GetMetadataTest.cs new file mode 100644 index 00000000000..eb0d350147f --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/GetMetadataTest.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using FluentAssertions.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using SeedMixedFileDirectory.Core; +using SeedMixedFileDirectory.User.Events; + +#nullable enable + +namespace SeedMixedFileDirectory.Test.Unit.MockServer; + +[TestFixture] +public class GetMetadataTest : BaseMockServerTest +{ + [Test] + public async Task MockServerTest() + { + const string mockResponse = """ + { + "id": "string", + "value": { + "key": "value" + } + } + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/users/events/metadata/") + .WithParam("id", "string") + .UsingGet() + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.User.Events.Metadata.GetMetadataAsync( + new GetEventMetadataRequest { Id = "string" }, + RequestOptions + ); + JToken + .Parse(mockResponse) + .Should() + .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/ListEventsTest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/ListEventsTest.cs new file mode 100644 index 00000000000..384fdd28b10 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/ListEventsTest.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using FluentAssertions.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using SeedMixedFileDirectory.Core; +using SeedMixedFileDirectory.User; + +#nullable enable + +namespace SeedMixedFileDirectory.Test.Unit.MockServer; + +[TestFixture] +public class ListEventsTest : BaseMockServerTest +{ + [Test] + public async Task MockServerTest() + { + const string mockResponse = """ + [ + { + "id": "string", + "name": "string" + } + ] + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/users/events/") + .WithParam("limit", "1") + .UsingGet() + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.User.Events.ListEventsAsync( + new ListUserEventsRequest { Limit = 1 }, + RequestOptions + ); + JToken + .Parse(mockResponse) + .Should() + .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/ListTest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/ListTest.cs new file mode 100644 index 00000000000..06099347e9e --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Unit/MockServer/ListTest.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using FluentAssertions.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using SeedMixedFileDirectory; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.Test.Unit.MockServer; + +[TestFixture] +public class ListTest : BaseMockServerTest +{ + [Test] + public async Task MockServerTest() + { + const string mockResponse = """ + [ + { + "id": "string", + "name": "string", + "age": 1 + } + ] + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/users/") + .WithParam("limit", "1") + .UsingGet() + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.User.ListAsync( + new ListUsersRequest { Limit = 1 }, + RequestOptions + ); + JToken + .Parse(mockResponse) + .Should() + .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/CollectionItemSerializer.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/CollectionItemSerializer.cs new file mode 100644 index 00000000000..87d4d84d1dc --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/CollectionItemSerializer.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMixedFileDirectory.Core; + +/// +/// Json collection converter. +/// +/// Type of item to convert. +/// Converter to use for individual items. +internal class CollectionItemSerializer + : JsonConverter> + where TConverterType : JsonConverter +{ + /// + /// Reads a json string and deserializes it into an object. + /// + /// Json reader. + /// Type to convert. + /// Serializer options. + /// Created object. + public override IEnumerable? Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + + var returnValue = new List(); + + while (reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + var item = (TDatatype)( + JsonSerializer.Deserialize(ref reader, typeof(TDatatype), jsonSerializerOptions) + ?? throw new Exception( + $"Failed to deserialize collection item of type {typeof(TDatatype)}" + ) + ); + returnValue.Add(item); + } + + reader.Read(); + } + + return returnValue; + } + + /// + /// Writes a json string. + /// + /// Json writer. + /// Value to write. + /// Serializer options. + public override void Write( + Utf8JsonWriter writer, + IEnumerable? value, + JsonSerializerOptions options + ) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + + writer.WriteStartArray(); + + foreach (var data in value) + { + JsonSerializer.Serialize(writer, data, jsonSerializerOptions); + } + + writer.WriteEndArray(); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Constants.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Constants.cs new file mode 100644 index 00000000000..5c0a541c4fd --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Constants.cs @@ -0,0 +1,7 @@ +namespace SeedMixedFileDirectory.Core; + +internal static class Constants +{ + public const string DateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffK"; + public const string DateFormat = "yyyy-MM-dd"; +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/DateTimeSerializer.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/DateTimeSerializer.cs new file mode 100644 index 00000000000..4f0a2c657b3 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/DateTimeSerializer.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMixedFileDirectory.Core; + +internal class DateTimeSerializer : JsonConverter +{ + public override DateTime Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + return DateTime.Parse(reader.GetString()!, null, DateTimeStyles.RoundtripKind); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(Constants.DateTimeFormat)); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Extensions.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Extensions.cs new file mode 100644 index 00000000000..161287fdbb6 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Extensions.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace SeedMixedFileDirectory.Core; + +internal static class Extensions +{ + public static string Stringify(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + var attribute = (EnumMemberAttribute) + Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)); + return attribute?.Value ?? value.ToString(); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/HeaderValue.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/HeaderValue.cs new file mode 100644 index 00000000000..2e178ad9561 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/HeaderValue.cs @@ -0,0 +1,17 @@ +using OneOf; + +namespace SeedMixedFileDirectory.Core; + +internal sealed class HeaderValue(OneOf> value) + : OneOfBase>(value) +{ + public static implicit operator HeaderValue(string value) + { + return new HeaderValue(value); + } + + public static implicit operator HeaderValue(Func value) + { + return new HeaderValue(value); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Headers.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Headers.cs new file mode 100644 index 00000000000..16b5ec8bea6 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Headers.cs @@ -0,0 +1,17 @@ +namespace SeedMixedFileDirectory.Core; + +internal sealed class Headers : Dictionary +{ + public Headers() { } + + public Headers(Dictionary value) + { + foreach (var kvp in value) + { + this[kvp.Key] = new HeaderValue(kvp.Value); + } + } + + public Headers(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/HttpMethodExtensions.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/HttpMethodExtensions.cs new file mode 100644 index 00000000000..a184ae4910e --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/HttpMethodExtensions.cs @@ -0,0 +1,8 @@ +using System.Net.Http; + +namespace SeedMixedFileDirectory.Core; + +internal static class HttpMethodExtensions +{ + public static readonly HttpMethod Patch = new("PATCH"); +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs new file mode 100644 index 00000000000..aa412ddce4d --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMixedFileDirectory.Core; + +internal static class JsonOptions +{ + public static readonly JsonSerializerOptions JsonSerializerOptions; + + static JsonOptions() + { + JsonSerializerOptions = new JsonSerializerOptions + { + Converters = { new DateTimeSerializer(), new OneOfSerializer() }, + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + } +} + +internal static class JsonUtils +{ + public static string Serialize(T obj) + { + return JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions); + } + + public static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, JsonOptions.JsonSerializerOptions)!; + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/OneOfSerializer.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/OneOfSerializer.cs new file mode 100644 index 00000000000..083cb8178f7 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/OneOfSerializer.cs @@ -0,0 +1,69 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; + +namespace SeedMixedFileDirectory.Core; + +internal class OneOfSerializer : JsonConverter +{ + public override IOneOf? Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType is JsonTokenType.Null) + return default; + + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) + { + try + { + var readerCopy = reader; + var result = JsonSerializer.Deserialize(ref readerCopy, type, options); + reader.Skip(); + return (IOneOf)cast.Invoke(null, [result])!; + } + catch (JsonException) { } + } + + throw new JsonException( + $"Cannot deserialize into one of the supported types for {typeToConvert}" + ); + } + + public override void Write(Utf8JsonWriter writer, IOneOf value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + + private static (System.Type type, MethodInfo cast)[] GetOneOfTypes(System.Type typeToConvert) + { + var casts = typeToConvert + .GetRuntimeMethods() + .Where(m => m.IsSpecialName && m.Name == "op_Implicit") + .ToArray(); + var type = typeToConvert; + while (type != null) + { + if ( + type.IsGenericType + && (type.Name.StartsWith("OneOf`") || type.Name.StartsWith("OneOfBase`")) + ) + { + return type.GetGenericArguments() + .Select(t => (t, casts.First(c => c.GetParameters()[0].ParameterType == t))) + .ToArray(); + } + + type = type.BaseType; + } + throw new InvalidOperationException($"{type} isn't OneOf or OneOfBase"); + } + + public override bool CanConvert(System.Type typeToConvert) + { + return typeof(IOneOf).IsAssignableFrom(typeToConvert); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/ClientOptions.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/ClientOptions.cs new file mode 100644 index 00000000000..e97d3d264b1 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/ClientOptions.cs @@ -0,0 +1,35 @@ +using System; +using System.Net.Http; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public partial class ClientOptions +{ + /// + /// The Base URL for the API. + /// + public string BaseUrl { get; init; } = ""; + + /// + /// The http client used to make requests. + /// + public HttpClient HttpClient { get; init; } = new HttpClient(); + + /// + /// The http client used to make requests. + /// + public int MaxRetries { get; init; } = 2; + + /// + /// The timeout for the request. + /// + public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30); + + /// + /// The http headers sent with the request. + /// + internal Headers Headers { get; init; } = new(); +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/RequestOptions.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/RequestOptions.cs new file mode 100644 index 00000000000..252f7b1cc85 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/RequestOptions.cs @@ -0,0 +1,35 @@ +using System; +using System.Net.Http; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public partial class RequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; init; } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; init; } + + /// + /// The http client used to make requests. + /// + public int? MaxRetries { get; init; } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; init; } + + /// + /// The http headers sent with the request. + /// + internal Headers Headers { get; init; } = new(); +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/SeedMixedFileDirectoryApiException.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/SeedMixedFileDirectoryApiException.cs new file mode 100644 index 00000000000..368cf3c23c8 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/SeedMixedFileDirectoryApiException.cs @@ -0,0 +1,18 @@ +namespace SeedMixedFileDirectory; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +public class SeedMixedFileDirectoryApiException(string message, int statusCode, object body) + : SeedMixedFileDirectoryException(message) +{ + /// + /// The error code of the response that triggered the exception. + /// + public int StatusCode => statusCode; + + /// + /// The body of the response that triggered the exception. + /// + public object Body => body; +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/SeedMixedFileDirectoryException.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/SeedMixedFileDirectoryException.cs new file mode 100644 index 00000000000..8125dc04369 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/SeedMixedFileDirectoryException.cs @@ -0,0 +1,11 @@ +using System; + +#nullable enable + +namespace SeedMixedFileDirectory; + +/// +/// Base exception class for all exceptions thrown by the SDK. +/// +public class SeedMixedFileDirectoryException(string message, Exception? innerException = null) + : Exception(message, innerException) { } diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/Version.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/Version.cs new file mode 100644 index 00000000000..6bf8ea8e377 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/Public/Version.cs @@ -0,0 +1,6 @@ +namespace SeedMixedFileDirectory; + +internal class Version +{ + public const string Current = "0.0.1"; +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/RawClient.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/RawClient.cs new file mode 100644 index 00000000000..a11f6009621 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/RawClient.cs @@ -0,0 +1,185 @@ +using System.Net.Http; +using System.Text; +using System.Threading; + +namespace SeedMixedFileDirectory.Core; + +#nullable enable + +/// +/// Utility class for making raw HTTP requests to the API. +/// +internal class RawClient(ClientOptions clientOptions) +{ + private const int InitialRetryDelayMs = 1000; + private const int MaxRetryDelayMs = 60000; + + /// + /// The client options applied on every request. + /// + public readonly ClientOptions Options = clientOptions; + + public async Task MakeRequestAsync( + BaseApiRequest request, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeout = request.Options?.Timeout ?? Options.Timeout; + cts.CancelAfter(timeout); + + // Send the request. + return await SendWithRetriesAsync(request, cts.Token); + } + + public record BaseApiRequest + { + public required string BaseUrl { get; init; } + + public required HttpMethod Method { get; init; } + + public required string Path { get; init; } + + public string? ContentType { get; init; } + + public Dictionary Query { get; init; } = new(); + + public Headers Headers { get; init; } = new(); + + public RequestOptions? Options { get; init; } + } + + /// + /// The request object to be sent for streaming uploads. + /// + public record StreamApiRequest : BaseApiRequest + { + public Stream? Body { get; init; } + } + + /// + /// The request object to be sent for JSON APIs. + /// + public record JsonApiRequest : BaseApiRequest + { + public object? Body { get; init; } + } + + /// + /// The response object returned from the API. + /// + public record ApiResponse + { + public required int StatusCode { get; init; } + + public required HttpResponseMessage Raw { get; init; } + } + + private async Task SendWithRetriesAsync( + BaseApiRequest request, + CancellationToken cancellationToken + ) + { + var httpClient = request.Options?.HttpClient ?? Options.HttpClient; + var maxRetries = request.Options?.MaxRetries ?? Options.MaxRetries; + var response = await httpClient.SendAsync(BuildHttpRequest(request), cancellationToken); + for (var i = 0; i < maxRetries; i++) + { + if (!ShouldRetry(response)) + { + break; + } + var delayMs = Math.Min(InitialRetryDelayMs * (int)Math.Pow(2, i), MaxRetryDelayMs); + await System.Threading.Tasks.Task.Delay(delayMs, cancellationToken); + response = await httpClient.SendAsync(BuildHttpRequest(request), cancellationToken); + } + return new ApiResponse { StatusCode = (int)response.StatusCode, Raw = response }; + } + + private static bool ShouldRetry(HttpResponseMessage response) + { + var statusCode = (int)response.StatusCode; + return statusCode is 408 or 429 or >= 500; + } + + private HttpRequestMessage BuildHttpRequest(BaseApiRequest request) + { + var url = BuildUrl(request); + var httpRequest = new HttpRequestMessage(request.Method, url); + switch (request) + { + // Add the request body to the request. + case JsonApiRequest jsonRequest: + { + if (jsonRequest.Body != null) + { + httpRequest.Content = new StringContent( + JsonUtils.Serialize(jsonRequest.Body), + Encoding.UTF8, + "application/json" + ); + } + break; + } + case StreamApiRequest { Body: not null } streamRequest: + httpRequest.Content = new StreamContent(streamRequest.Body); + break; + } + if (request.ContentType != null) + { + request.Headers.Add("Content-Type", request.ContentType); + } + SetHeaders(httpRequest, Options.Headers); + SetHeaders(httpRequest, request.Headers); + SetHeaders(httpRequest, request.Options?.Headers ?? new Headers()); + return httpRequest; + } + + private static string BuildUrl(BaseApiRequest request) + { + var baseUrl = request.Options?.BaseUrl ?? request.BaseUrl; + var trimmedBaseUrl = baseUrl.TrimEnd('/'); + var trimmedBasePath = request.Path.TrimStart('/'); + var url = $"{trimmedBaseUrl}/{trimmedBasePath}"; + if (request.Query.Count <= 0) + return url; + url += "?"; + url = request.Query.Aggregate( + url, + (current, queryItem) => + { + if (queryItem.Value is System.Collections.IEnumerable collection and not string) + { + var items = collection + .Cast() + .Select(value => $"{queryItem.Key}={value}") + .ToList(); + if (items.Any()) + { + current += string.Join("&", items) + "&"; + } + } + else + { + current += $"{queryItem.Key}={queryItem.Value}&"; + } + return current; + } + ); + url = url[..^1]; + return url; + } + + private static void SetHeaders(HttpRequestMessage httpRequest, Headers headers) + { + foreach (var header in headers) + { + var value = header.Value?.Match(str => str, func => func.Invoke()); + if (value != null) + { + httpRequest.Headers.TryAddWithoutValidation(header.Key, value); + } + } + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs new file mode 100644 index 00000000000..3f86e6d408b --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs @@ -0,0 +1,53 @@ +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMixedFileDirectory.Core; + +internal class StringEnumSerializer : JsonConverter + where TEnum : struct, System.Enum +{ + private readonly Dictionary _enumToString = new(); + private readonly Dictionary _stringToEnum = new(); + + public StringEnumSerializer() + { + var type = typeof(TEnum); + var values = Enum.GetValues(type); + + foreach (var value in values) + { + var enumValue = (TEnum)value; + var enumMember = type.GetMember(enumValue.ToString())[0]; + var attr = enumMember + .GetCustomAttributes(typeof(EnumMemberAttribute), false) + .Cast() + .FirstOrDefault(); + + var stringValue = + attr?.Value + ?? value.ToString() + ?? throw new Exception("Unexpected null enum toString value"); + + _enumToString.Add(enumValue, stringValue); + _stringToEnum.Add(stringValue, enumValue); + } + } + + public override TEnum Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(_enumToString[value]); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/OrganizationClient.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/OrganizationClient.cs new file mode 100644 index 00000000000..5e267b54da0 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/OrganizationClient.cs @@ -0,0 +1,63 @@ +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public partial class OrganizationClient +{ + private RawClient _client; + + internal OrganizationClient(RawClient client) + { + _client = client; + } + + /// + /// Create a new organization. + /// + /// + /// + /// await client.Organization.CreateAsync(new CreateOrganizationRequest { Name = "string" }); + /// + /// + public async Task CreateAsync( + CreateOrganizationRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var response = await _client.MakeRequestAsync( + new RawClient.JsonApiRequest + { + BaseUrl = _client.Options.BaseUrl, + Method = HttpMethod.Post, + Path = "/organizations/", + Body = request, + Options = options, + }, + cancellationToken + ); + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + if (response.StatusCode is >= 200 and < 400) + { + try + { + return JsonUtils.Deserialize(responseBody)!; + } + catch (JsonException e) + { + throw new SeedMixedFileDirectoryException("Failed to deserialize response", e); + } + } + + throw new SeedMixedFileDirectoryApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Types/CreateOrganizationRequest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Types/CreateOrganizationRequest.cs new file mode 100644 index 00000000000..4fcaa5582cd --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Types/CreateOrganizationRequest.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public record CreateOrganizationRequest +{ + [JsonPropertyName("name")] + public required string Name { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Types/Organization.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Types/Organization.cs new file mode 100644 index 00000000000..0c84627921c --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Organization/Types/Organization.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public record Organization +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("users")] + public IEnumerable Users { get; set; } = new List(); + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectory.csproj b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectory.csproj new file mode 100644 index 00000000000..7fac8fd09df --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectory.csproj @@ -0,0 +1,50 @@ + + + + + net462;net8.0;net7.0;net6.0;netstandard2.0 + enable + false + 12 + enable + 0.0.1 + README.md + https://github.com/mixed-file-directory/fern + + + + true + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + <_Parameter1>SeedMixedFileDirectory.Test + + + + diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectoryClient.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectoryClient.cs new file mode 100644 index 00000000000..8f365b0064e --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectoryClient.cs @@ -0,0 +1,38 @@ +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public partial class SeedMixedFileDirectoryClient +{ + private RawClient _client; + + public SeedMixedFileDirectoryClient(ClientOptions? clientOptions = null) + { + var defaultHeaders = new Headers( + new Dictionary() + { + { "X-Fern-Language", "C#" }, + { "X-Fern-SDK-Name", "SeedMixedFileDirectory" }, + { "X-Fern-SDK-Version", Version.Current }, + { "User-Agent", "Fernmixed-file-directory/0.0.1" }, + } + ); + clientOptions ??= new ClientOptions(); + foreach (var header in defaultHeaders) + { + if (!clientOptions.Headers.ContainsKey(header.Key)) + { + clientOptions.Headers[header.Key] = header.Value; + } + } + _client = new RawClient(clientOptions); + Organization = new OrganizationClient(_client); + User = new UserClient(_client); + } + + public OrganizationClient Organization { get; init; } + + public UserClient User { get; init; } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/EventsClient.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/EventsClient.cs new file mode 100644 index 00000000000..8730001c7fa --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/EventsClient.cs @@ -0,0 +1,73 @@ +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using SeedMixedFileDirectory; +using SeedMixedFileDirectory.Core; +using SeedMixedFileDirectory.User.Events; + +#nullable enable + +namespace SeedMixedFileDirectory.User; + +public partial class EventsClient +{ + private RawClient _client; + + internal EventsClient(RawClient client) + { + _client = client; + Metadata = new MetadataClient(_client); + } + + public MetadataClient Metadata { get; } + + /// + /// List all user events. + /// + /// + /// + /// await client.User.Events.ListEventsAsync(new ListUserEventsRequest { Limit = 1 }); + /// + /// + public async Task> ListEventsAsync( + ListUserEventsRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var _query = new Dictionary(); + if (request.Limit != null) + { + _query["limit"] = request.Limit.ToString(); + } + var response = await _client.MakeRequestAsync( + new RawClient.JsonApiRequest + { + BaseUrl = _client.Options.BaseUrl, + Method = HttpMethod.Get, + Path = "/users/events/", + Query = _query, + Options = options, + }, + cancellationToken + ); + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + if (response.StatusCode is >= 200 and < 400) + { + try + { + return JsonUtils.Deserialize>(responseBody)!; + } + catch (JsonException e) + { + throw new SeedMixedFileDirectoryException("Failed to deserialize response", e); + } + } + + throw new SeedMixedFileDirectoryApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/MetadataClient.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/MetadataClient.cs new file mode 100644 index 00000000000..607f3c0333c --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/MetadataClient.cs @@ -0,0 +1,66 @@ +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using SeedMixedFileDirectory; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.User.Events; + +public partial class MetadataClient +{ + private RawClient _client; + + internal MetadataClient(RawClient client) + { + _client = client; + } + + /// + /// Get event metadata. + /// + /// + /// + /// await client.User.Events.Metadata.GetMetadataAsync(new GetEventMetadataRequest { Id = "string" }); + /// + /// + public async Task GetMetadataAsync( + GetEventMetadataRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var _query = new Dictionary(); + _query["id"] = request.Id; + var response = await _client.MakeRequestAsync( + new RawClient.JsonApiRequest + { + BaseUrl = _client.Options.BaseUrl, + Method = HttpMethod.Get, + Path = "/users/events/metadata/", + Query = _query, + Options = options, + }, + cancellationToken + ); + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + if (response.StatusCode is >= 200 and < 400) + { + try + { + return JsonUtils.Deserialize(responseBody)!; + } + catch (JsonException e) + { + throw new SeedMixedFileDirectoryException("Failed to deserialize response", e); + } + } + + throw new SeedMixedFileDirectoryApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Requests/GetEventMetadataRequest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Requests/GetEventMetadataRequest.cs new file mode 100644 index 00000000000..99b53cb4d15 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Requests/GetEventMetadataRequest.cs @@ -0,0 +1,15 @@ +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.User.Events; + +public record GetEventMetadataRequest +{ + public required string Id { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Types/Metadata.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Types/Metadata.cs new file mode 100644 index 00000000000..4529e495f9c --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Metadata/Types/Metadata.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.User.Events; + +public record Metadata +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("value")] + public required object Value { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Requests/ListUserEventsRequest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Requests/ListUserEventsRequest.cs new file mode 100644 index 00000000000..53ce12c4576 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Requests/ListUserEventsRequest.cs @@ -0,0 +1,18 @@ +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.User; + +public record ListUserEventsRequest +{ + /// + /// The maximum number of results to return. + /// + public int? Limit { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Types/Event.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Types/Event.cs new file mode 100644 index 00000000000..d66c4e6434a --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Events/Types/Event.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory.User; + +public record Event +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Requests/ListUsersRequest.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Requests/ListUsersRequest.cs new file mode 100644 index 00000000000..e517db0f7c4 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Requests/ListUsersRequest.cs @@ -0,0 +1,18 @@ +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public record ListUsersRequest +{ + /// + /// The maximum number of results to return. + /// + public int? Limit { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Types/User.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Types/User.cs new file mode 100644 index 00000000000..fa1e2e3a9b5 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/Types/User.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using SeedMixedFileDirectory.Core; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public record User +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("age")] + public required int Age { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/UserClient.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/UserClient.cs new file mode 100644 index 00000000000..c4866333fb5 --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/User/UserClient.cs @@ -0,0 +1,72 @@ +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using SeedMixedFileDirectory.Core; +using SeedMixedFileDirectory.User; + +#nullable enable + +namespace SeedMixedFileDirectory; + +public partial class UserClient +{ + private RawClient _client; + + internal UserClient(RawClient client) + { + _client = client; + Events = new EventsClient(_client); + } + + public EventsClient Events { get; } + + /// + /// List all users. + /// + /// + /// + /// await client.User.ListAsync(new ListUsersRequest { Limit = 1 }); + /// + /// + public async Task> ListAsync( + ListUsersRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var _query = new Dictionary(); + if (request.Limit != null) + { + _query["limit"] = request.Limit.ToString(); + } + var response = await _client.MakeRequestAsync( + new RawClient.JsonApiRequest + { + BaseUrl = _client.Options.BaseUrl, + Method = HttpMethod.Get, + Path = "/users/", + Query = _query, + Options = options, + }, + cancellationToken + ); + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + if (response.StatusCode is >= 200 and < 400) + { + try + { + return JsonUtils.Deserialize>(responseBody)!; + } + catch (JsonException e) + { + throw new SeedMixedFileDirectoryException("Failed to deserialize response", e); + } + } + + throw new SeedMixedFileDirectoryApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } +} diff --git a/seed/csharp-sdk/seed.yml b/seed/csharp-sdk/seed.yml index 505c0b7ddd2..ccf904e6d51 100644 --- a/seed/csharp-sdk/seed.yml +++ b/seed/csharp-sdk/seed.yml @@ -90,6 +90,7 @@ allowedFailures: - objects-with-imports - examples:no-custom-config - examples:readme-config + - mixed-file-directory ## WIRE TEST FAILURES - unknown # issue with example object # - package-yml # user-provided example path not coming in properly diff --git a/seed/fastapi/mixed-file-directory/.mock/definition/__package__.yml b/seed/fastapi/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/fastapi/mixed-file-directory/.mock/definition/api.yml b/seed/fastapi/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/fastapi/mixed-file-directory/.mock/definition/organization.yml b/seed/fastapi/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/fastapi/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/fastapi/mixed-file-directory/.mock/definition/user.yml b/seed/fastapi/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/fastapi/mixed-file-directory/.mock/definition/user/events.yml b/seed/fastapi/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/fastapi/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/fastapi/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/fastapi/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/fastapi/mixed-file-directory/.mock/fern.config.json b/seed/fastapi/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/fastapi/mixed-file-directory/.mock/generators.yml b/seed/fastapi/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/fastapi/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/fastapi/mixed-file-directory/__init__.py b/seed/fastapi/mixed-file-directory/__init__.py new file mode 100644 index 00000000000..7963345e06e --- /dev/null +++ b/seed/fastapi/mixed-file-directory/__init__.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +from .resources import CreateOrganizationRequest, Organization, User, organization, user +from .types import Id + +__all__ = [ + "CreateOrganizationRequest", + "Id", + "Organization", + "User", + "organization", + "user", +] diff --git a/seed/fastapi/mixed-file-directory/core/__init__.py b/seed/fastapi/mixed-file-directory/core/__init__.py new file mode 100644 index 00000000000..f9c8e44aea0 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +from .datetime_utils import serialize_datetime +from .exceptions import ( + FernHTTPException, + UnauthorizedException, + default_exception_handler, + fern_http_exception_handler, + http_exception_handler, +) +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, +) +from .route_args import route_args +from .security import BearerToken +from .serialization import FieldMetadata, convert_and_respect_annotation_metadata + +__all__ = [ + "BearerToken", + "FernHTTPException", + "FieldMetadata", + "IS_PYDANTIC_V2", + "UnauthorizedException", + "UniversalBaseModel", + "UniversalRootModel", + "convert_and_respect_annotation_metadata", + "default_exception_handler", + "fern_http_exception_handler", + "http_exception_handler", + "parse_obj_as", + "route_args", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", +] diff --git a/seed/fastapi/mixed-file-directory/core/abstract_fern_service.py b/seed/fastapi/mixed-file-directory/core/abstract_fern_service.py new file mode 100644 index 00000000000..9966b4876da --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/abstract_fern_service.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import abc + +import fastapi + + +class AbstractFernService(abc.ABC): + @classmethod + def _init_fern(cls, router: fastapi.APIRouter) -> None: ... diff --git a/seed/fastapi/mixed-file-directory/core/datetime_utils.py b/seed/fastapi/mixed-file-directory/core/datetime_utils.py new file mode 100644 index 00000000000..47344e9d9cc --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/datetime_utils.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname( + None + ): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/seed/fastapi/mixed-file-directory/core/exceptions/__init__.py b/seed/fastapi/mixed-file-directory/core/exceptions/__init__.py new file mode 100644 index 00000000000..dae4b8980c1 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/exceptions/__init__.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +from .fern_http_exception import FernHTTPException +from .handlers import ( + default_exception_handler, + fern_http_exception_handler, + http_exception_handler, +) +from .unauthorized import UnauthorizedException + +__all__ = [ + "FernHTTPException", + "UnauthorizedException", + "default_exception_handler", + "fern_http_exception_handler", + "http_exception_handler", +] diff --git a/seed/fastapi/mixed-file-directory/core/exceptions/fern_http_exception.py b/seed/fastapi/mixed-file-directory/core/exceptions/fern_http_exception.py new file mode 100644 index 00000000000..81610359a7f --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/exceptions/fern_http_exception.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import abc +import fastapi +import typing + + +class FernHTTPException(abc.ABC, fastapi.HTTPException): + def __init__( + self, + status_code: int, + name: typing.Optional[str] = None, + content: typing.Optional[typing.Any] = None, + ): + super().__init__(status_code=status_code) + self.name = name + self.status_code = status_code + self.content = content + + def to_json_response(self) -> fastapi.responses.JSONResponse: + content = fastapi.encoders.jsonable_encoder(self.content, exclude_none=True) + return fastapi.responses.JSONResponse( + content=content, status_code=self.status_code + ) diff --git a/seed/fastapi/mixed-file-directory/core/exceptions/handlers.py b/seed/fastapi/mixed-file-directory/core/exceptions/handlers.py new file mode 100644 index 00000000000..ae1c2741f06 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/exceptions/handlers.py @@ -0,0 +1,50 @@ +# This file was auto-generated by Fern from our API Definition. + +import logging + +import starlette +import starlette.exceptions + +import fastapi + +from .fern_http_exception import FernHTTPException + + +def fern_http_exception_handler( + request: fastapi.requests.Request, + exc: FernHTTPException, + skip_log: bool = False, +) -> fastapi.responses.JSONResponse: + if not skip_log: + logging.getLogger(__name__).error( + f"{exc.__class__.__name__} in {request.url.path}", exc_info=exc + ) + return exc.to_json_response() + + +def http_exception_handler( + request: fastapi.requests.Request, + exc: starlette.exceptions.HTTPException, + skip_log: bool = False, +) -> fastapi.responses.JSONResponse: + if not skip_log: + logging.getLogger(__name__).error( + f"{exc.__class__.__name__} in {request.url.path}", exc_info=exc + ) + return FernHTTPException( + status_code=exc.status_code, content=exc.detail + ).to_json_response() + + +def default_exception_handler( + request: fastapi.requests.Request, + exc: Exception, + skip_log: bool = False, +) -> fastapi.responses.JSONResponse: + if not skip_log: + logging.getLogger(__name__).error( + f"{exc.__class__.__name__} in {request.url.path}", exc_info=exc + ) + return FernHTTPException( + status_code=500, content="Internal Server Error" + ).to_json_response() diff --git a/seed/fastapi/mixed-file-directory/core/exceptions/unauthorized.py b/seed/fastapi/mixed-file-directory/core/exceptions/unauthorized.py new file mode 100644 index 00000000000..32d532e5ef2 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/exceptions/unauthorized.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .fern_http_exception import FernHTTPException + + +class UnauthorizedException(FernHTTPException): + """ + This is the exception that is thrown by Fern when auth is not present on a + request. + """ + + def __init__(self, content: typing.Optional[str] = None) -> None: + super().__init__(status_code=401, content=content) diff --git a/seed/fastapi/mixed-file-directory/core/pydantic_utilities.py b/seed/fastapi/mixed-file-directory/core/pydantic_utilities.py new file mode 100644 index 00000000000..ac22d16a187 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/pydantic_utilities.py @@ -0,0 +1,259 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +import typing +from collections import defaultdict + +import typing_extensions + +import pydantic + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + # isort will try to reformat the comments on these imports, which breaks mypy + # isort: off + from pydantic.v1.datetime_parse import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + parse_date as parse_date, + ) + from pydantic.v1.datetime_parse import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + parse_datetime as parse_datetime, + ) + from pydantic.v1.json import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + ENCODERS_BY_TYPE as encoders_by_type, + ) + from pydantic.v1.typing import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + get_args as get_args, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + get_origin as get_origin, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + is_literal_type as is_literal_type, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + is_union as is_union, + ) + from pydantic.v1.fields import ModelField as ModelField # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore # Pydantic v1 + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore # Pydantic v1 + from pydantic.fields import ModelField as ModelField # type: ignore # Pydantic v1 + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore # Pydantic v1 + from pydantic.typing import get_args as get_args # type: ignore # Pydantic v1 + from pydantic.typing import get_origin as get_origin # type: ignore # Pydantic v1 + from pydantic.typing import is_literal_type as is_literal_type # type: ignore # Pydantic v1 + from pydantic.typing import is_union as is_union # type: ignore # Pydantic v1 + + # isort: on + + +T = typing.TypeVar("T") +Model = typing.TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T: + dealiased_object = convert_and_respect_annotation_metadata( + object_=object_, annotation=type_, direction="read" + ) + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore # Pydantic v2 + return adapter.validate_python(dealiased_object) + else: + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback( + obj: typing.Any, fallback_serializer: typing.Callable[[typing.Any], typing.Any] +) -> typing.Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + else: + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + protected_namespaces=(), + json_encoders={dt.datetime: serialize_datetime}, + ) # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore # Pydantic v2 + else: + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multi-plexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset: typing.Any = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none: typing.Any = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore # Pydantic v2 + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore # Pydantic v2 + ) + + else: + _fields_set = self.__fields_set__ + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default != None: + _fields_set.add(name) + + kwargs_with_defaults_exclude_unset_include_fields: typing.Any = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict( + **kwargs_with_defaults_exclude_unset_include_fields + ) + + return convert_and_respect_annotation_metadata( + object_=dict_dump, annotation=self.__class__, direction="write" + ) + + +def deep_union_pydantic_dicts( + source: typing.Dict[str, typing.Any], destination: typing.Dict[str, typing.Any] +) -> typing.Dict[str, typing.Any]: + for key, value in source.items(): + if isinstance(value, dict): + node = destination.setdefault(key, {}) + deep_union_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore # Pydantic v2 + pass + + UniversalRootModel: typing_extensions.TypeAlias = V2RootModel # type: ignore +else: + UniversalRootModel: typing_extensions.TypeAlias = UniversalBaseModel # type: ignore + + +def encode_by_type(o: typing.Any) -> typing.Any: + encoders_by_class_tuples: typing.Dict[ + typing.Callable[[typing.Any], typing.Any], typing.Tuple[typing.Any, ...] + ] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: typing.Type["Model"]) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore # Pydantic v2 + else: + model.update_forward_refs() + + +# Mirrors Pydantic's internal typing +AnyCallable = typing.Callable[..., typing.Any] + + +def universal_root_validator( + pre: bool = False, +) -> typing.Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return pydantic.model_validator(mode="before" if pre else "after")(func) # type: ignore # Pydantic v2 + else: + return pydantic.root_validator(pre=pre)(func) # type: ignore # Pydantic v1 + + return decorator + + +def universal_field_validator( + field_name: str, pre: bool = False +) -> typing.Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return pydantic.field_validator( + field_name, mode="before" if pre else "after" + )(func) # type: ignore # Pydantic v2 + else: + return pydantic.validator(field_name, pre=pre)(func) # type: ignore # Pydantic v1 + + return decorator + + +PydanticField = typing.Union[ModelField, pydantic.fields.FieldInfo] + + +def _get_model_fields( + model: typing.Type["Model"], +) -> typing.Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return model.model_fields # type: ignore # Pydantic v2 + else: + return model.__fields__ # type: ignore # Pydantic v1 + + +def _get_field_default(field: PydanticField) -> typing.Any: + try: + value = field.get_default() # type: ignore # Pydantic < v1.10.15 + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/seed/fastapi/mixed-file-directory/core/route_args.py b/seed/fastapi/mixed-file-directory/core/route_args.py new file mode 100644 index 00000000000..bd940bf4ddd --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/route_args.py @@ -0,0 +1,73 @@ +# This file was auto-generated by Fern from our API Definition. + +import enum +import inspect +import typing + +import typing_extensions + +T = typing.TypeVar("T", bound=typing.Callable[..., typing.Any]) + +FERN_CONFIG_KEY = "__fern" + + +class RouteArgs(typing_extensions.TypedDict): + openapi_extra: typing.Optional[typing.Dict[str, typing.Any]] + tags: typing.Optional[typing.List[typing.Union[str, enum.Enum]]] + include_in_schema: bool + + +DEFAULT_ROUTE_ARGS = RouteArgs(openapi_extra=None, tags=None, include_in_schema=True) + + +def get_route_args( + endpoint_function: typing.Callable[..., typing.Any], *, default_tag: str +) -> RouteArgs: + unwrapped = inspect.unwrap( + endpoint_function, stop=(lambda f: hasattr(f, FERN_CONFIG_KEY)) + ) + route_args = typing.cast( + RouteArgs, getattr(unwrapped, FERN_CONFIG_KEY, DEFAULT_ROUTE_ARGS) + ) + if route_args["tags"] is None: + return RouteArgs( + openapi_extra=route_args["openapi_extra"], + tags=[default_tag], + include_in_schema=route_args["include_in_schema"], + ) + return route_args + + +def route_args( + openapi_extra: typing.Optional[typing.Dict[str, typing.Any]] = None, + tags: typing.Optional[typing.List[typing.Union[str, enum.Enum]]] = None, + include_in_schema: bool = True, +) -> typing.Callable[[T], T]: + """ + this decorator allows you to forward certain args to the FastAPI route decorator. + + usage: + @route_args(openapi_extra=...) + def your_endpoint_method(... + + currently supported args: + - openapi_extra + - tags + + if there's another FastAPI route arg you need to pass through, please + contact the Fern team! + """ + + def decorator(endpoint_function: T) -> T: + setattr( + endpoint_function, + FERN_CONFIG_KEY, + RouteArgs( + openapi_extra=openapi_extra, + tags=tags, + include_in_schema=include_in_schema, + ), + ) + return endpoint_function + + return decorator diff --git a/seed/fastapi/mixed-file-directory/core/security/__init__.py b/seed/fastapi/mixed-file-directory/core/security/__init__.py new file mode 100644 index 00000000000..e69ee6d9c5a --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/security/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .bearer import BearerToken + +__all__ = ["BearerToken"] diff --git a/seed/fastapi/mixed-file-directory/core/security/bearer.py b/seed/fastapi/mixed-file-directory/core/security/bearer.py new file mode 100644 index 00000000000..023342b668d --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/security/bearer.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import fastapi + +from ..exceptions import UnauthorizedException + + +class BearerToken: + def __init__(self, token: str): + self.token = token + + +def HTTPBearer(request: fastapi.requests.Request) -> BearerToken: + authorization_header_value = request.headers.get("Authorization") + if not authorization_header_value: + raise UnauthorizedException("Missing Authorization header") + scheme, _, token = authorization_header_value.partition(" ") + if scheme.lower() != "bearer": + raise UnauthorizedException("Authorization header scheme is not bearer") + if not token: + raise UnauthorizedException("Authorization header is missing a token") + return BearerToken(token) diff --git a/seed/fastapi/mixed-file-directory/core/serialization.py b/seed/fastapi/mixed-file-directory/core/serialization.py new file mode 100644 index 00000000000..f9aa93a75a7 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/core/serialization.py @@ -0,0 +1,258 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import typing_extensions + +import pydantic + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance( + object_, typing.Mapping + ): + return _convert_mapping(object_, clean_type, direction) + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[ + _alias_key(key, type_, direction, aliases_to_field_names) + ] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/seed/fastapi/mixed-file-directory/register.py b/seed/fastapi/mixed-file-directory/register.py new file mode 100644 index 00000000000..047219dcde4 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/register.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +import fastapi +from .resources.organization.service.service import AbstractOrganizationService +from .resources.user.service.service import AbstractUserService +from .resources.user.resources.events.service.service import AbstractUserEventsService +from .resources.user.resources.events.resources.metadata.service.service import ( + AbstractUserEventsMetadataService, +) +import typing +from fastapi import params +from .core.exceptions.fern_http_exception import FernHTTPException +from .core.exceptions import fern_http_exception_handler +import starlette.exceptions +from .core.exceptions import http_exception_handler +from .core.exceptions import default_exception_handler +from .core.abstract_fern_service import AbstractFernService +import types +import os +import glob +import importlib + + +def register( + _app: fastapi.FastAPI, + *, + organization: AbstractOrganizationService, + user: AbstractUserService, + user_events: AbstractUserEventsService, + user_events_metadata: AbstractUserEventsMetadataService, + dependencies: typing.Optional[typing.Sequence[params.Depends]] = None, +) -> None: + _app.include_router(__register_service(organization), dependencies=dependencies) + _app.include_router(__register_service(user), dependencies=dependencies) + _app.include_router(__register_service(user_events), dependencies=dependencies) + _app.include_router( + __register_service(user_events_metadata), dependencies=dependencies + ) + + _app.add_exception_handler(FernHTTPException, fern_http_exception_handler) # type: ignore + _app.add_exception_handler( + starlette.exceptions.HTTPException, http_exception_handler + ) # type: ignore + _app.add_exception_handler(Exception, default_exception_handler) # type: ignore + + +def __register_service(service: AbstractFernService) -> fastapi.APIRouter: + router = fastapi.APIRouter() + type(service)._init_fern(router) + return router + + +def register_validators(module: types.ModuleType) -> None: + validators_directory: str = os.path.dirname(module.__file__) # type: ignore + for path in glob.glob( + os.path.join(validators_directory, "**/*.py"), recursive=True + ): + if os.path.isfile(path): + relative_path = os.path.relpath(path, start=validators_directory) + module_path = ".".join([module.__name__] + relative_path[:-3].split("/")) + importlib.import_module(module_path) diff --git a/seed/fastapi/mixed-file-directory/resources/__init__.py b/seed/fastapi/mixed-file-directory/resources/__init__.py new file mode 100644 index 00000000000..e6681c2d643 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/__init__.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +from . import organization, user +from .organization import CreateOrganizationRequest, Organization +from .user import User + +__all__ = ["CreateOrganizationRequest", "Organization", "User", "organization", "user"] diff --git a/seed/fastapi/mixed-file-directory/resources/organization/__init__.py b/seed/fastapi/mixed-file-directory/resources/organization/__init__.py new file mode 100644 index 00000000000..66f1cdc2b00 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/organization/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import CreateOrganizationRequest, Organization + +__all__ = ["CreateOrganizationRequest", "Organization"] diff --git a/seed/fastapi/mixed-file-directory/resources/organization/service/__init__.py b/seed/fastapi/mixed-file-directory/resources/organization/service/__init__.py new file mode 100644 index 00000000000..296f1c95350 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/organization/service/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .service import AbstractOrganizationService + +__all__ = ["AbstractOrganizationService"] diff --git a/seed/fastapi/mixed-file-directory/resources/organization/service/service.py b/seed/fastapi/mixed-file-directory/resources/organization/service/service.py new file mode 100644 index 00000000000..524ad8103ce --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/organization/service/service.py @@ -0,0 +1,81 @@ +# This file was auto-generated by Fern from our API Definition. + +from ....core.abstract_fern_service import AbstractFernService +from ..types.create_organization_request import CreateOrganizationRequest +from ..types.organization import Organization +import abc +import fastapi +import inspect +import typing +from ....core.exceptions.fern_http_exception import FernHTTPException +import logging +import functools +from ....core.route_args import get_route_args + + +class AbstractOrganizationService(AbstractFernService): + """ + AbstractOrganizationService is an abstract class containing the methods that you should implement. + + Each method is associated with an API route, which will be registered + with FastAPI when you register your implementation using Fern's register() + function. + """ + + @abc.abstractmethod + def create(self, *, body: CreateOrganizationRequest) -> Organization: + """ + Create a new organization. + """ + ... + + """ + Below are internal methods used by Fern to register your implementation. + You can ignore them. + """ + + @classmethod + def _init_fern(cls, router: fastapi.APIRouter) -> None: + cls.__init_create(router=router) + + @classmethod + def __init_create(cls, router: fastapi.APIRouter) -> None: + endpoint_function = inspect.signature(cls.create) + new_parameters: typing.List[inspect.Parameter] = [] + for index, (parameter_name, parameter) in enumerate( + endpoint_function.parameters.items() + ): + if index == 0: + new_parameters.append(parameter.replace(default=fastapi.Depends(cls))) + elif parameter_name == "body": + new_parameters.append(parameter.replace(default=fastapi.Body(...))) + else: + new_parameters.append(parameter) + setattr( + cls.create, + "__signature__", + endpoint_function.replace(parameters=new_parameters), + ) + + @functools.wraps(cls.create) + def wrapper(*args: typing.Any, **kwargs: typing.Any) -> Organization: + try: + return cls.create(*args, **kwargs) + except FernHTTPException as e: + logging.getLogger(f"{cls.__module__}.{cls.__name__}").warn( + f"Endpoint 'create' unexpectedly threw {e.__class__.__name__}. " + + f"If this was intentional, please add {e.__class__.__name__} to " + + "the endpoint's errors list in your Fern Definition." + ) + raise e + + # this is necessary for FastAPI to find forward-ref'ed type hints. + # https://github.com/tiangolo/fastapi/pull/5077 + wrapper.__globals__.update(cls.create.__globals__) + + router.post( + path="/organizations/", + response_model=Organization, + description=AbstractOrganizationService.create.__doc__, + **get_route_args(cls.create, default_tag="organization"), + )(wrapper) diff --git a/seed/fastapi/mixed-file-directory/resources/organization/types/__init__.py b/seed/fastapi/mixed-file-directory/resources/organization/types/__init__.py new file mode 100644 index 00000000000..5ef97404389 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/organization/types/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from .create_organization_request import CreateOrganizationRequest +from .organization import Organization + +__all__ = ["CreateOrganizationRequest", "Organization"] diff --git a/seed/fastapi/mixed-file-directory/resources/organization/types/create_organization_request.py b/seed/fastapi/mixed-file-directory/resources/organization/types/create_organization_request.py new file mode 100644 index 00000000000..f3389a831b5 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/organization/types/create_organization_request.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class CreateOrganizationRequest(UniversalBaseModel): + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="forbid" + ) # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.forbid diff --git a/seed/fastapi/mixed-file-directory/resources/organization/types/organization.py b/seed/fastapi/mixed-file-directory/resources/organization/types/organization.py new file mode 100644 index 00000000000..e4c9d78bd3f --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/organization/types/organization.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from ....core.pydantic_utilities import UniversalBaseModel +from ....types.id import Id +import typing +from ...user.types.user import User +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +import pydantic + + +class Organization(UniversalBaseModel): + id: Id + name: str + users: typing.List[User] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="forbid" + ) # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.forbid diff --git a/seed/fastapi/mixed-file-directory/resources/user/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/__init__.py new file mode 100644 index 00000000000..89ec051d216 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from .resources import Event, events +from .types import User + +__all__ = ["Event", "User", "events"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/resources/__init__.py new file mode 100644 index 00000000000..6eed10d4d9d --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from . import events +from .events import Event + +__all__ = ["Event", "events"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/__init__.py new file mode 100644 index 00000000000..220b163e5a2 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from .resources import Metadata, metadata +from .types import Event + +__all__ = ["Event", "Metadata", "metadata"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/__init__.py new file mode 100644 index 00000000000..bdb62162d58 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from . import metadata +from .metadata import Metadata + +__all__ = ["Metadata", "metadata"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/__init__.py new file mode 100644 index 00000000000..e8376684efc --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import Metadata + +__all__ = ["Metadata"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/service/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/service/__init__.py new file mode 100644 index 00000000000..0ad3290d60e --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/service/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .service import AbstractUserEventsMetadataService + +__all__ = ["AbstractUserEventsMetadataService"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/service/service.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/service/service.py new file mode 100644 index 00000000000..24ba0b0087c --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/service/service.py @@ -0,0 +1,82 @@ +# This file was auto-generated by Fern from our API Definition. + +from ........core.abstract_fern_service import AbstractFernService +from ..types.metadata import Metadata +import abc +import fastapi +import inspect +import typing +from ........core.exceptions.fern_http_exception import FernHTTPException +import logging +import functools +from ........core.route_args import get_route_args + + +class AbstractUserEventsMetadataService(AbstractFernService): + """ + AbstractUserEventsMetadataService is an abstract class containing the methods that you should implement. + + Each method is associated with an API route, which will be registered + with FastAPI when you register your implementation using Fern's register() + function. + """ + + @abc.abstractmethod + def get_metadata(self, *, id: str) -> Metadata: + """ + Get event metadata. + """ + ... + + """ + Below are internal methods used by Fern to register your implementation. + You can ignore them. + """ + + @classmethod + def _init_fern(cls, router: fastapi.APIRouter) -> None: + cls.__init_get_metadata(router=router) + + @classmethod + def __init_get_metadata(cls, router: fastapi.APIRouter) -> None: + endpoint_function = inspect.signature(cls.get_metadata) + new_parameters: typing.List[inspect.Parameter] = [] + for index, (parameter_name, parameter) in enumerate( + endpoint_function.parameters.items() + ): + if index == 0: + new_parameters.append(parameter.replace(default=fastapi.Depends(cls))) + elif parameter_name == "id": + new_parameters.append( + parameter.replace(default=fastapi.Query(default=...)) + ) + else: + new_parameters.append(parameter) + setattr( + cls.get_metadata, + "__signature__", + endpoint_function.replace(parameters=new_parameters), + ) + + @functools.wraps(cls.get_metadata) + def wrapper(*args: typing.Any, **kwargs: typing.Any) -> Metadata: + try: + return cls.get_metadata(*args, **kwargs) + except FernHTTPException as e: + logging.getLogger(f"{cls.__module__}.{cls.__name__}").warn( + f"Endpoint 'get_metadata' unexpectedly threw {e.__class__.__name__}. " + + f"If this was intentional, please add {e.__class__.__name__} to " + + "the endpoint's errors list in your Fern Definition." + ) + raise e + + # this is necessary for FastAPI to find forward-ref'ed type hints. + # https://github.com/tiangolo/fastapi/pull/5077 + wrapper.__globals__.update(cls.get_metadata.__globals__) + + router.get( + path="/users/events/metadata/", + response_model=Metadata, + description=AbstractUserEventsMetadataService.get_metadata.__doc__, + **get_route_args(cls.get_metadata, default_tag="user.events.metadata"), + )(wrapper) diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/types/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/types/__init__.py new file mode 100644 index 00000000000..6104b066ab0 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/types/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .metadata import Metadata + +__all__ = ["Metadata"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/types/metadata.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/types/metadata.py new file mode 100644 index 00000000000..53a973afd78 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/resources/metadata/types/metadata.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +from ........core.pydantic_utilities import UniversalBaseModel +from ........types.id import Id +import typing +from ........core.pydantic_utilities import IS_PYDANTIC_V2 +import pydantic + + +class Metadata(UniversalBaseModel): + id: Id + value: typing.Optional[typing.Any] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="forbid" + ) # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.forbid diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/service/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/service/__init__.py new file mode 100644 index 00000000000..e79599bf39e --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/service/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .service import AbstractUserEventsService + +__all__ = ["AbstractUserEventsService"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/service/service.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/service/service.py new file mode 100644 index 00000000000..548f7781a82 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/service/service.py @@ -0,0 +1,89 @@ +# This file was auto-generated by Fern from our API Definition. + +from ......core.abstract_fern_service import AbstractFernService +import typing +from ..types.event import Event +import abc +import fastapi +import inspect +from ......core.exceptions.fern_http_exception import FernHTTPException +import logging +import functools +from ......core.route_args import get_route_args + + +class AbstractUserEventsService(AbstractFernService): + """ + AbstractUserEventsService is an abstract class containing the methods that you should implement. + + Each method is associated with an API route, which will be registered + with FastAPI when you register your implementation using Fern's register() + function. + """ + + @abc.abstractmethod + def list_events( + self, *, limit: typing.Optional[int] = None + ) -> typing.Sequence[Event]: + """ + List all user events. + """ + ... + + """ + Below are internal methods used by Fern to register your implementation. + You can ignore them. + """ + + @classmethod + def _init_fern(cls, router: fastapi.APIRouter) -> None: + cls.__init_list_events(router=router) + + @classmethod + def __init_list_events(cls, router: fastapi.APIRouter) -> None: + endpoint_function = inspect.signature(cls.list_events) + new_parameters: typing.List[inspect.Parameter] = [] + for index, (parameter_name, parameter) in enumerate( + endpoint_function.parameters.items() + ): + if index == 0: + new_parameters.append(parameter.replace(default=fastapi.Depends(cls))) + elif parameter_name == "limit": + new_parameters.append( + parameter.replace( + default=fastapi.Query( + default=None, + description="The maximum number of results to return.", + ) + ) + ) + else: + new_parameters.append(parameter) + setattr( + cls.list_events, + "__signature__", + endpoint_function.replace(parameters=new_parameters), + ) + + @functools.wraps(cls.list_events) + def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Sequence[Event]: + try: + return cls.list_events(*args, **kwargs) + except FernHTTPException as e: + logging.getLogger(f"{cls.__module__}.{cls.__name__}").warn( + f"Endpoint 'list_events' unexpectedly threw {e.__class__.__name__}. " + + f"If this was intentional, please add {e.__class__.__name__} to " + + "the endpoint's errors list in your Fern Definition." + ) + raise e + + # this is necessary for FastAPI to find forward-ref'ed type hints. + # https://github.com/tiangolo/fastapi/pull/5077 + wrapper.__globals__.update(cls.list_events.__globals__) + + router.get( + path="/users/events/", + response_model=typing.Sequence[Event], + description=AbstractUserEventsService.list_events.__doc__, + **get_route_args(cls.list_events, default_tag="user.events"), + )(wrapper) diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/types/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/types/__init__.py new file mode 100644 index 00000000000..00cb9e486c8 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/types/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .event import Event + +__all__ = ["Event"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/resources/events/types/event.py b/seed/fastapi/mixed-file-directory/resources/user/resources/events/types/event.py new file mode 100644 index 00000000000..f65e0a989be --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/resources/events/types/event.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +from ......core.pydantic_utilities import UniversalBaseModel +from ......types.id import Id +from ......core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class Event(UniversalBaseModel): + id: Id + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="forbid" + ) # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.forbid diff --git a/seed/fastapi/mixed-file-directory/resources/user/service/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/service/__init__.py new file mode 100644 index 00000000000..30d7a1adb4c --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/service/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .service import AbstractUserService + +__all__ = ["AbstractUserService"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/service/service.py b/seed/fastapi/mixed-file-directory/resources/user/service/service.py new file mode 100644 index 00000000000..39f08b4bb1b --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/service/service.py @@ -0,0 +1,87 @@ +# This file was auto-generated by Fern from our API Definition. + +from ....core.abstract_fern_service import AbstractFernService +import typing +from ..types.user import User +import abc +import fastapi +import inspect +from ....core.exceptions.fern_http_exception import FernHTTPException +import logging +import functools +from ....core.route_args import get_route_args + + +class AbstractUserService(AbstractFernService): + """ + AbstractUserService is an abstract class containing the methods that you should implement. + + Each method is associated with an API route, which will be registered + with FastAPI when you register your implementation using Fern's register() + function. + """ + + @abc.abstractmethod + def list_(self, *, limit: typing.Optional[int] = None) -> typing.Sequence[User]: + """ + List all users. + """ + ... + + """ + Below are internal methods used by Fern to register your implementation. + You can ignore them. + """ + + @classmethod + def _init_fern(cls, router: fastapi.APIRouter) -> None: + cls.__init_list_(router=router) + + @classmethod + def __init_list_(cls, router: fastapi.APIRouter) -> None: + endpoint_function = inspect.signature(cls.list_) + new_parameters: typing.List[inspect.Parameter] = [] + for index, (parameter_name, parameter) in enumerate( + endpoint_function.parameters.items() + ): + if index == 0: + new_parameters.append(parameter.replace(default=fastapi.Depends(cls))) + elif parameter_name == "limit": + new_parameters.append( + parameter.replace( + default=fastapi.Query( + default=None, + description="The maximum number of results to return.", + ) + ) + ) + else: + new_parameters.append(parameter) + setattr( + cls.list_, + "__signature__", + endpoint_function.replace(parameters=new_parameters), + ) + + @functools.wraps(cls.list_) + def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Sequence[User]: + try: + return cls.list_(*args, **kwargs) + except FernHTTPException as e: + logging.getLogger(f"{cls.__module__}.{cls.__name__}").warn( + f"Endpoint 'list_' unexpectedly threw {e.__class__.__name__}. " + + f"If this was intentional, please add {e.__class__.__name__} to " + + "the endpoint's errors list in your Fern Definition." + ) + raise e + + # this is necessary for FastAPI to find forward-ref'ed type hints. + # https://github.com/tiangolo/fastapi/pull/5077 + wrapper.__globals__.update(cls.list_.__globals__) + + router.get( + path="/users/", + response_model=typing.Sequence[User], + description=AbstractUserService.list_.__doc__, + **get_route_args(cls.list_, default_tag="user"), + )(wrapper) diff --git a/seed/fastapi/mixed-file-directory/resources/user/types/__init__.py b/seed/fastapi/mixed-file-directory/resources/user/types/__init__.py new file mode 100644 index 00000000000..b22b663beed --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/types/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .user import User + +__all__ = ["User"] diff --git a/seed/fastapi/mixed-file-directory/resources/user/types/user.py b/seed/fastapi/mixed-file-directory/resources/user/types/user.py new file mode 100644 index 00000000000..6546feae13d --- /dev/null +++ b/seed/fastapi/mixed-file-directory/resources/user/types/user.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +from ....core.pydantic_utilities import UniversalBaseModel +from ....types.id import Id +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class User(UniversalBaseModel): + id: Id + name: str + age: int + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="forbid" + ) # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.forbid diff --git a/seed/fastapi/mixed-file-directory/snippet-templates.json b/seed/fastapi/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/fastapi/mixed-file-directory/snippet.json b/seed/fastapi/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/fastapi/mixed-file-directory/types/__init__.py b/seed/fastapi/mixed-file-directory/types/__init__.py new file mode 100644 index 00000000000..e838afc85bd --- /dev/null +++ b/seed/fastapi/mixed-file-directory/types/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .id import Id + +__all__ = ["Id"] diff --git a/seed/fastapi/mixed-file-directory/types/id.py b/seed/fastapi/mixed-file-directory/types/id.py new file mode 100644 index 00000000000..f066d648f06 --- /dev/null +++ b/seed/fastapi/mixed-file-directory/types/id.py @@ -0,0 +1,3 @@ +# This file was auto-generated by Fern from our API Definition. + +Id = str diff --git a/seed/go-fiber/mixed-file-directory/.github/workflows/ci.yml b/seed/go-fiber/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..d4c0a5dcd95 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Compile + run: go build ./... + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Test + run: go test ./... diff --git a/seed/go-fiber/mixed-file-directory/.mock/definition/__package__.yml b/seed/go-fiber/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/go-fiber/mixed-file-directory/.mock/definition/api.yml b/seed/go-fiber/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/go-fiber/mixed-file-directory/.mock/definition/organization.yml b/seed/go-fiber/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/go-fiber/mixed-file-directory/.mock/definition/user.yml b/seed/go-fiber/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/go-fiber/mixed-file-directory/.mock/definition/user/events.yml b/seed/go-fiber/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/go-fiber/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/go-fiber/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/go-fiber/mixed-file-directory/.mock/fern.config.json b/seed/go-fiber/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/go-fiber/mixed-file-directory/.mock/generators.yml b/seed/go-fiber/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/go-fiber/mixed-file-directory/core/extra_properties.go b/seed/go-fiber/mixed-file-directory/core/extra_properties.go new file mode 100644 index 00000000000..a6af3e12410 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/core/extra_properties.go @@ -0,0 +1,141 @@ +package core + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property. +func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) { + return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value}) +} + +// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties. +func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) { + bytes, err := json.Marshal(marshaler) + if err != nil { + return nil, err + } + if len(extraProperties) == 0 { + return bytes, nil + } + keys, err := getKeys(marshaler) + if err != nil { + return nil, err + } + for _, key := range keys { + if _, ok := extraProperties[key]; ok { + return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key) + } + } + extraBytes, err := json.Marshal(extraProperties) + if err != nil { + return nil, err + } + if isEmptyJSON(bytes) { + if isEmptyJSON(extraBytes) { + return bytes, nil + } + return extraBytes, nil + } + result := bytes[:len(bytes)-1] + result = append(result, ',') + result = append(result, extraBytes[1:len(extraBytes)-1]...) + result = append(result, '}') + return result, nil +} + +// ExtractExtraProperties extracts any extra properties from the given value. +func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) { + val := reflect.ValueOf(value) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil, fmt.Errorf("value must be non-nil to extract extra properties") + } + val = val.Elem() + } + if err := json.Unmarshal(bytes, &value); err != nil { + return nil, err + } + var extraProperties map[string]interface{} + if err := json.Unmarshal(bytes, &extraProperties); err != nil { + return nil, err + } + for i := 0; i < val.Type().NumField(); i++ { + key := jsonKey(val.Type().Field(i)) + if key == "" || key == "-" { + continue + } + delete(extraProperties, key) + } + for _, key := range exclude { + delete(extraProperties, key) + } + if len(extraProperties) == 0 { + return nil, nil + } + return extraProperties, nil +} + +// getKeys returns the keys associated with the given value. The value must be a +// a struct or a map with string keys. +func getKeys(value interface{}) ([]string, error) { + val := reflect.ValueOf(value) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return nil, nil + } + switch val.Kind() { + case reflect.Struct: + return getKeysForStructType(val.Type()), nil + case reflect.Map: + var keys []string + if val.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } + for _, key := range val.MapKeys() { + keys = append(keys, key.String()) + } + return keys, nil + default: + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } +} + +// getKeysForStructType returns all the keys associated with the given struct type, +// visiting embedded fields recursively. +func getKeysForStructType(structType reflect.Type) []string { + if structType.Kind() == reflect.Pointer { + structType = structType.Elem() + } + if structType.Kind() != reflect.Struct { + return nil + } + var keys []string + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.Anonymous { + keys = append(keys, getKeysForStructType(field.Type)...) + continue + } + keys = append(keys, jsonKey(field)) + } + return keys +} + +// jsonKey returns the JSON key from the struct tag of the given field, +// excluding the omitempty flag (if any). +func jsonKey(field reflect.StructField) string { + return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty") +} + +// isEmptyJSON returns true if the given data is empty, the empty JSON object, or +// an explicit null. +func isEmptyJSON(data []byte) bool { + return len(data) <= 2 || bytes.Equal(data, []byte("null")) +} diff --git a/seed/go-fiber/mixed-file-directory/core/extra_properties_test.go b/seed/go-fiber/mixed-file-directory/core/extra_properties_test.go new file mode 100644 index 00000000000..dc66fccd7f1 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/core/extra_properties_test.go @@ -0,0 +1,228 @@ +package core + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMarshaler struct { + Name string `json:"name"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"created_at"` +} + +func (t *testMarshaler) MarshalJSON() ([]byte, error) { + type embed testMarshaler + var marshaler = struct { + embed + BirthDate string `json:"birthDate"` + CreatedAt string `json:"created_at"` + }{ + embed: embed(*t), + BirthDate: t.BirthDate.Format("2006-01-02"), + CreatedAt: t.CreatedAt.Format(time.RFC3339), + } + return MarshalJSONWithExtraProperty(marshaler, "type", "test") +} + +func TestMarshalJSONWithExtraProperties(t *testing.T) { + tests := []struct { + desc string + giveMarshaler interface{} + giveExtraProperties map[string]interface{} + wantBytes []byte + wantError string + }{ + { + desc: "invalid type", + giveMarshaler: []string{"invalid"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`, + }, + { + desc: "invalid key type", + giveMarshaler: map[int]interface{}{42: "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`, + }, + { + desc: "invalid map overwrite", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot add extra property "key" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"}, + wantError: `cannot add extra property "birthDate" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite embedded type", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"name": "bob"}, + wantError: `cannot add extra property "name" because it is already defined on the type`, + }, + { + desc: "nil", + giveMarshaler: nil, + giveExtraProperties: nil, + wantBytes: []byte(`null`), + }, + { + desc: "empty", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{}`), + }, + { + desc: "no extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "only extra properties", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "single extra property", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"extra": "property"}, + wantBytes: []byte(`{"key":"value","extra":"property"}`), + }, + { + desc: "multiple extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"one": 1, "two": 2}, + wantBytes: []byte(`{"key":"value","one":1,"two":2}`), + }, + { + desc: "nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`), + }, + { + desc: "multiple nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "metadata": map[string]interface{}{ + "ip": "127.0.0.1", + }, + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`), + }, + { + desc: "custom marshaler", + giveMarshaler: &testMarshaler{ + Name: "alice", + BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }, + giveExtraProperties: map[string]interface{}{ + "extra": "property", + }, + wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`), + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantBytes, bytes) + + value := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestExtractExtraProperties(t *testing.T) { + t.Run("none", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value) + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value *user + _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + assert.EqualError(t, err, "value must be non-nil to extract extra properties") + }) + + t.Run("non-zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value user + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("exclude", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age") + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) +} diff --git a/seed/go-fiber/mixed-file-directory/core/stringer.go b/seed/go-fiber/mixed-file-directory/core/stringer.go new file mode 100644 index 00000000000..000cf448641 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/core/stringer.go @@ -0,0 +1,13 @@ +package core + +import "encoding/json" + +// StringifyJSON returns a pretty JSON string representation of +// the given value. +func StringifyJSON(value interface{}) (string, error) { + bytes, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/seed/go-fiber/mixed-file-directory/core/time.go b/seed/go-fiber/mixed-file-directory/core/time.go new file mode 100644 index 00000000000..d009ab30c90 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/core/time.go @@ -0,0 +1,137 @@ +package core + +import ( + "encoding/json" + "time" +) + +const dateFormat = "2006-01-02" + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date (e.g. 2006-01-02). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type Date struct { + t *time.Time +} + +// NewDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewDate(t time.Time) *Date { + return &Date{t: &t} +} + +// NewOptionalDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDate(t *time.Time) *Date { + if t == nil { + return nil + } + return &Date{t: t} +} + +// Time returns the Date's underlying time, if any. If the +// date is nil, the zero value is returned. +func (d *Date) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the Date's underlying time.Time, if any. +func (d *Date) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *Date) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(dateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(dateFormat, raw) + if err != nil { + return err + } + + *d = Date{t: &parsedTime} + return nil +} + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type DateTime struct { + t *time.Time +} + +// NewDateTime returns a new *DateTime. +func NewDateTime(t time.Time) *DateTime { + return &DateTime{t: &t} +} + +// NewOptionalDateTime returns a new *DateTime. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDateTime(t *time.Time) *DateTime { + if t == nil { + return nil + } + return &DateTime{t: t} +} + +// Time returns the DateTime's underlying time, if any. If the +// date-time is nil, the zero value is returned. +func (d *DateTime) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the DateTime's underlying time.Time, if any. +func (d *DateTime) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *DateTime) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(time.RFC3339)) +} + +func (d *DateTime) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(time.RFC3339, raw) + if err != nil { + return err + } + + *d = DateTime{t: &parsedTime} + return nil +} diff --git a/seed/go-fiber/mixed-file-directory/go.mod b/seed/go-fiber/mixed-file-directory/go.mod new file mode 100644 index 00000000000..0fa86b5c19b --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/go.mod @@ -0,0 +1,8 @@ +module github.com/mixed-file-directory/fern + +go 1.13 + +require ( + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/seed/go-fiber/mixed-file-directory/go.sum b/seed/go-fiber/mixed-file-directory/go.sum new file mode 100644 index 00000000000..fc3dd9e67e8 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/seed/go-fiber/mixed-file-directory/organization.go b/seed/go-fiber/mixed-file-directory/organization.go new file mode 100644 index 00000000000..e727eb1c016 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/organization.go @@ -0,0 +1,79 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +import ( + json "encoding/json" + fmt "fmt" + core "github.com/mixed-file-directory/fern/core" +) + +type CreateOrganizationRequest struct { + Name string `json:"name" url:"name"` + + extraProperties map[string]interface{} +} + +func (c *CreateOrganizationRequest) GetExtraProperties() map[string]interface{} { + return c.extraProperties +} + +func (c *CreateOrganizationRequest) UnmarshalJSON(data []byte) error { + type unmarshaler CreateOrganizationRequest + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *c = CreateOrganizationRequest(value) + + extraProperties, err := core.ExtractExtraProperties(data, *c) + if err != nil { + return err + } + c.extraProperties = extraProperties + + return nil +} + +func (c *CreateOrganizationRequest) String() string { + if value, err := core.StringifyJSON(c); err == nil { + return value + } + return fmt.Sprintf("%#v", c) +} + +type Organization struct { + Id Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + Users []*User `json:"users,omitempty" url:"users,omitempty"` + + extraProperties map[string]interface{} +} + +func (o *Organization) GetExtraProperties() map[string]interface{} { + return o.extraProperties +} + +func (o *Organization) UnmarshalJSON(data []byte) error { + type unmarshaler Organization + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = Organization(value) + + extraProperties, err := core.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + + return nil +} + +func (o *Organization) String() string { + if value, err := core.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} diff --git a/seed/go-fiber/mixed-file-directory/snippet-templates.json b/seed/go-fiber/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/go-fiber/mixed-file-directory/snippet.json b/seed/go-fiber/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/go-fiber/mixed-file-directory/types.go b/seed/go-fiber/mixed-file-directory/types.go new file mode 100644 index 00000000000..cacf3342a01 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/types.go @@ -0,0 +1,5 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +type Id = string diff --git a/seed/go-fiber/mixed-file-directory/user.go b/seed/go-fiber/mixed-file-directory/user.go new file mode 100644 index 00000000000..a3d8ff6e154 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/user.go @@ -0,0 +1,50 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +import ( + json "encoding/json" + fmt "fmt" + core "github.com/mixed-file-directory/fern/core" +) + +type ListUsersRequest struct { + // The maximum number of results to return. + Limit *int `query:"limit"` +} + +type User struct { + Id Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + + extraProperties map[string]interface{} +} + +func (u *User) GetExtraProperties() map[string]interface{} { + return u.extraProperties +} + +func (u *User) UnmarshalJSON(data []byte) error { + type unmarshaler User + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *u = User(value) + + extraProperties, err := core.ExtractExtraProperties(data, *u) + if err != nil { + return err + } + u.extraProperties = extraProperties + + return nil +} + +func (u *User) String() string { + if value, err := core.StringifyJSON(u); err == nil { + return value + } + return fmt.Sprintf("%#v", u) +} diff --git a/seed/go-fiber/mixed-file-directory/user/events.go b/seed/go-fiber/mixed-file-directory/user/events.go new file mode 100644 index 00000000000..7af5b0baa90 --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/user/events.go @@ -0,0 +1,50 @@ +// This file was auto-generated by Fern from our API Definition. + +package user + +import ( + json "encoding/json" + fmt "fmt" + fern "github.com/mixed-file-directory/fern" + core "github.com/mixed-file-directory/fern/core" +) + +type ListUserEventsRequest struct { + // The maximum number of results to return. + Limit *int `query:"limit"` +} + +type Event struct { + Id fern.Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + + extraProperties map[string]interface{} +} + +func (e *Event) GetExtraProperties() map[string]interface{} { + return e.extraProperties +} + +func (e *Event) UnmarshalJSON(data []byte) error { + type unmarshaler Event + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *e = Event(value) + + extraProperties, err := core.ExtractExtraProperties(data, *e) + if err != nil { + return err + } + e.extraProperties = extraProperties + + return nil +} + +func (e *Event) String() string { + if value, err := core.StringifyJSON(e); err == nil { + return value + } + return fmt.Sprintf("%#v", e) +} diff --git a/seed/go-fiber/mixed-file-directory/user/events/metadata.go b/seed/go-fiber/mixed-file-directory/user/events/metadata.go new file mode 100644 index 00000000000..63833d2b89e --- /dev/null +++ b/seed/go-fiber/mixed-file-directory/user/events/metadata.go @@ -0,0 +1,49 @@ +// This file was auto-generated by Fern from our API Definition. + +package events + +import ( + json "encoding/json" + fmt "fmt" + fern "github.com/mixed-file-directory/fern" + core "github.com/mixed-file-directory/fern/core" +) + +type GetEventMetadataRequest struct { + Id fern.Id `query:"id"` +} + +type Metadata struct { + Id fern.Id `json:"id" url:"id"` + Value interface{} `json:"value,omitempty" url:"value,omitempty"` + + extraProperties map[string]interface{} +} + +func (m *Metadata) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *Metadata) UnmarshalJSON(data []byte) error { + type unmarshaler Metadata + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *m = Metadata(value) + + extraProperties, err := core.ExtractExtraProperties(data, *m) + if err != nil { + return err + } + m.extraProperties = extraProperties + + return nil +} + +func (m *Metadata) String() string { + if value, err := core.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} diff --git a/seed/go-model/mixed-file-directory/.github/workflows/ci.yml b/seed/go-model/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..d4c0a5dcd95 --- /dev/null +++ b/seed/go-model/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Compile + run: go build ./... + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Test + run: go test ./... diff --git a/seed/go-model/mixed-file-directory/.mock/definition/__package__.yml b/seed/go-model/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/go-model/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/go-model/mixed-file-directory/.mock/definition/api.yml b/seed/go-model/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/go-model/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/go-model/mixed-file-directory/.mock/definition/organization.yml b/seed/go-model/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/go-model/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/go-model/mixed-file-directory/.mock/definition/user.yml b/seed/go-model/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/go-model/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/go-model/mixed-file-directory/.mock/definition/user/events.yml b/seed/go-model/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/go-model/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/go-model/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/go-model/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/go-model/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/go-model/mixed-file-directory/.mock/fern.config.json b/seed/go-model/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/go-model/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/go-model/mixed-file-directory/.mock/generators.yml b/seed/go-model/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/go-model/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/go-model/mixed-file-directory/core/extra_properties.go b/seed/go-model/mixed-file-directory/core/extra_properties.go new file mode 100644 index 00000000000..a6af3e12410 --- /dev/null +++ b/seed/go-model/mixed-file-directory/core/extra_properties.go @@ -0,0 +1,141 @@ +package core + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property. +func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) { + return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value}) +} + +// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties. +func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) { + bytes, err := json.Marshal(marshaler) + if err != nil { + return nil, err + } + if len(extraProperties) == 0 { + return bytes, nil + } + keys, err := getKeys(marshaler) + if err != nil { + return nil, err + } + for _, key := range keys { + if _, ok := extraProperties[key]; ok { + return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key) + } + } + extraBytes, err := json.Marshal(extraProperties) + if err != nil { + return nil, err + } + if isEmptyJSON(bytes) { + if isEmptyJSON(extraBytes) { + return bytes, nil + } + return extraBytes, nil + } + result := bytes[:len(bytes)-1] + result = append(result, ',') + result = append(result, extraBytes[1:len(extraBytes)-1]...) + result = append(result, '}') + return result, nil +} + +// ExtractExtraProperties extracts any extra properties from the given value. +func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) { + val := reflect.ValueOf(value) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil, fmt.Errorf("value must be non-nil to extract extra properties") + } + val = val.Elem() + } + if err := json.Unmarshal(bytes, &value); err != nil { + return nil, err + } + var extraProperties map[string]interface{} + if err := json.Unmarshal(bytes, &extraProperties); err != nil { + return nil, err + } + for i := 0; i < val.Type().NumField(); i++ { + key := jsonKey(val.Type().Field(i)) + if key == "" || key == "-" { + continue + } + delete(extraProperties, key) + } + for _, key := range exclude { + delete(extraProperties, key) + } + if len(extraProperties) == 0 { + return nil, nil + } + return extraProperties, nil +} + +// getKeys returns the keys associated with the given value. The value must be a +// a struct or a map with string keys. +func getKeys(value interface{}) ([]string, error) { + val := reflect.ValueOf(value) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return nil, nil + } + switch val.Kind() { + case reflect.Struct: + return getKeysForStructType(val.Type()), nil + case reflect.Map: + var keys []string + if val.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } + for _, key := range val.MapKeys() { + keys = append(keys, key.String()) + } + return keys, nil + default: + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } +} + +// getKeysForStructType returns all the keys associated with the given struct type, +// visiting embedded fields recursively. +func getKeysForStructType(structType reflect.Type) []string { + if structType.Kind() == reflect.Pointer { + structType = structType.Elem() + } + if structType.Kind() != reflect.Struct { + return nil + } + var keys []string + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.Anonymous { + keys = append(keys, getKeysForStructType(field.Type)...) + continue + } + keys = append(keys, jsonKey(field)) + } + return keys +} + +// jsonKey returns the JSON key from the struct tag of the given field, +// excluding the omitempty flag (if any). +func jsonKey(field reflect.StructField) string { + return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty") +} + +// isEmptyJSON returns true if the given data is empty, the empty JSON object, or +// an explicit null. +func isEmptyJSON(data []byte) bool { + return len(data) <= 2 || bytes.Equal(data, []byte("null")) +} diff --git a/seed/go-model/mixed-file-directory/core/extra_properties_test.go b/seed/go-model/mixed-file-directory/core/extra_properties_test.go new file mode 100644 index 00000000000..dc66fccd7f1 --- /dev/null +++ b/seed/go-model/mixed-file-directory/core/extra_properties_test.go @@ -0,0 +1,228 @@ +package core + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMarshaler struct { + Name string `json:"name"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"created_at"` +} + +func (t *testMarshaler) MarshalJSON() ([]byte, error) { + type embed testMarshaler + var marshaler = struct { + embed + BirthDate string `json:"birthDate"` + CreatedAt string `json:"created_at"` + }{ + embed: embed(*t), + BirthDate: t.BirthDate.Format("2006-01-02"), + CreatedAt: t.CreatedAt.Format(time.RFC3339), + } + return MarshalJSONWithExtraProperty(marshaler, "type", "test") +} + +func TestMarshalJSONWithExtraProperties(t *testing.T) { + tests := []struct { + desc string + giveMarshaler interface{} + giveExtraProperties map[string]interface{} + wantBytes []byte + wantError string + }{ + { + desc: "invalid type", + giveMarshaler: []string{"invalid"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`, + }, + { + desc: "invalid key type", + giveMarshaler: map[int]interface{}{42: "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`, + }, + { + desc: "invalid map overwrite", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot add extra property "key" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"}, + wantError: `cannot add extra property "birthDate" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite embedded type", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"name": "bob"}, + wantError: `cannot add extra property "name" because it is already defined on the type`, + }, + { + desc: "nil", + giveMarshaler: nil, + giveExtraProperties: nil, + wantBytes: []byte(`null`), + }, + { + desc: "empty", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{}`), + }, + { + desc: "no extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "only extra properties", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "single extra property", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"extra": "property"}, + wantBytes: []byte(`{"key":"value","extra":"property"}`), + }, + { + desc: "multiple extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"one": 1, "two": 2}, + wantBytes: []byte(`{"key":"value","one":1,"two":2}`), + }, + { + desc: "nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`), + }, + { + desc: "multiple nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "metadata": map[string]interface{}{ + "ip": "127.0.0.1", + }, + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`), + }, + { + desc: "custom marshaler", + giveMarshaler: &testMarshaler{ + Name: "alice", + BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }, + giveExtraProperties: map[string]interface{}{ + "extra": "property", + }, + wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`), + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantBytes, bytes) + + value := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestExtractExtraProperties(t *testing.T) { + t.Run("none", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value) + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value *user + _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + assert.EqualError(t, err, "value must be non-nil to extract extra properties") + }) + + t.Run("non-zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value user + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("exclude", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age") + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) +} diff --git a/seed/go-model/mixed-file-directory/core/stringer.go b/seed/go-model/mixed-file-directory/core/stringer.go new file mode 100644 index 00000000000..000cf448641 --- /dev/null +++ b/seed/go-model/mixed-file-directory/core/stringer.go @@ -0,0 +1,13 @@ +package core + +import "encoding/json" + +// StringifyJSON returns a pretty JSON string representation of +// the given value. +func StringifyJSON(value interface{}) (string, error) { + bytes, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/seed/go-model/mixed-file-directory/core/time.go b/seed/go-model/mixed-file-directory/core/time.go new file mode 100644 index 00000000000..d009ab30c90 --- /dev/null +++ b/seed/go-model/mixed-file-directory/core/time.go @@ -0,0 +1,137 @@ +package core + +import ( + "encoding/json" + "time" +) + +const dateFormat = "2006-01-02" + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date (e.g. 2006-01-02). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type Date struct { + t *time.Time +} + +// NewDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewDate(t time.Time) *Date { + return &Date{t: &t} +} + +// NewOptionalDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDate(t *time.Time) *Date { + if t == nil { + return nil + } + return &Date{t: t} +} + +// Time returns the Date's underlying time, if any. If the +// date is nil, the zero value is returned. +func (d *Date) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the Date's underlying time.Time, if any. +func (d *Date) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *Date) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(dateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(dateFormat, raw) + if err != nil { + return err + } + + *d = Date{t: &parsedTime} + return nil +} + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type DateTime struct { + t *time.Time +} + +// NewDateTime returns a new *DateTime. +func NewDateTime(t time.Time) *DateTime { + return &DateTime{t: &t} +} + +// NewOptionalDateTime returns a new *DateTime. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDateTime(t *time.Time) *DateTime { + if t == nil { + return nil + } + return &DateTime{t: t} +} + +// Time returns the DateTime's underlying time, if any. If the +// date-time is nil, the zero value is returned. +func (d *DateTime) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the DateTime's underlying time.Time, if any. +func (d *DateTime) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *DateTime) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(time.RFC3339)) +} + +func (d *DateTime) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(time.RFC3339, raw) + if err != nil { + return err + } + + *d = DateTime{t: &parsedTime} + return nil +} diff --git a/seed/go-model/mixed-file-directory/go.mod b/seed/go-model/mixed-file-directory/go.mod new file mode 100644 index 00000000000..0fa86b5c19b --- /dev/null +++ b/seed/go-model/mixed-file-directory/go.mod @@ -0,0 +1,8 @@ +module github.com/mixed-file-directory/fern + +go 1.13 + +require ( + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/seed/go-model/mixed-file-directory/go.sum b/seed/go-model/mixed-file-directory/go.sum new file mode 100644 index 00000000000..fc3dd9e67e8 --- /dev/null +++ b/seed/go-model/mixed-file-directory/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/seed/go-model/mixed-file-directory/organization.go b/seed/go-model/mixed-file-directory/organization.go new file mode 100644 index 00000000000..e727eb1c016 --- /dev/null +++ b/seed/go-model/mixed-file-directory/organization.go @@ -0,0 +1,79 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +import ( + json "encoding/json" + fmt "fmt" + core "github.com/mixed-file-directory/fern/core" +) + +type CreateOrganizationRequest struct { + Name string `json:"name" url:"name"` + + extraProperties map[string]interface{} +} + +func (c *CreateOrganizationRequest) GetExtraProperties() map[string]interface{} { + return c.extraProperties +} + +func (c *CreateOrganizationRequest) UnmarshalJSON(data []byte) error { + type unmarshaler CreateOrganizationRequest + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *c = CreateOrganizationRequest(value) + + extraProperties, err := core.ExtractExtraProperties(data, *c) + if err != nil { + return err + } + c.extraProperties = extraProperties + + return nil +} + +func (c *CreateOrganizationRequest) String() string { + if value, err := core.StringifyJSON(c); err == nil { + return value + } + return fmt.Sprintf("%#v", c) +} + +type Organization struct { + Id Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + Users []*User `json:"users,omitempty" url:"users,omitempty"` + + extraProperties map[string]interface{} +} + +func (o *Organization) GetExtraProperties() map[string]interface{} { + return o.extraProperties +} + +func (o *Organization) UnmarshalJSON(data []byte) error { + type unmarshaler Organization + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = Organization(value) + + extraProperties, err := core.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + + return nil +} + +func (o *Organization) String() string { + if value, err := core.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} diff --git a/seed/go-model/mixed-file-directory/snippet-templates.json b/seed/go-model/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/go-model/mixed-file-directory/snippet.json b/seed/go-model/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/go-model/mixed-file-directory/types.go b/seed/go-model/mixed-file-directory/types.go new file mode 100644 index 00000000000..cacf3342a01 --- /dev/null +++ b/seed/go-model/mixed-file-directory/types.go @@ -0,0 +1,5 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +type Id = string diff --git a/seed/go-model/mixed-file-directory/user.go b/seed/go-model/mixed-file-directory/user.go new file mode 100644 index 00000000000..2dc1e36f2c3 --- /dev/null +++ b/seed/go-model/mixed-file-directory/user.go @@ -0,0 +1,45 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +import ( + json "encoding/json" + fmt "fmt" + core "github.com/mixed-file-directory/fern/core" +) + +type User struct { + Id Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + + extraProperties map[string]interface{} +} + +func (u *User) GetExtraProperties() map[string]interface{} { + return u.extraProperties +} + +func (u *User) UnmarshalJSON(data []byte) error { + type unmarshaler User + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *u = User(value) + + extraProperties, err := core.ExtractExtraProperties(data, *u) + if err != nil { + return err + } + u.extraProperties = extraProperties + + return nil +} + +func (u *User) String() string { + if value, err := core.StringifyJSON(u); err == nil { + return value + } + return fmt.Sprintf("%#v", u) +} diff --git a/seed/go-model/mixed-file-directory/user/events.go b/seed/go-model/mixed-file-directory/user/events.go new file mode 100644 index 00000000000..54651566695 --- /dev/null +++ b/seed/go-model/mixed-file-directory/user/events.go @@ -0,0 +1,45 @@ +// This file was auto-generated by Fern from our API Definition. + +package user + +import ( + json "encoding/json" + fmt "fmt" + fern "github.com/mixed-file-directory/fern" + core "github.com/mixed-file-directory/fern/core" +) + +type Event struct { + Id fern.Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + + extraProperties map[string]interface{} +} + +func (e *Event) GetExtraProperties() map[string]interface{} { + return e.extraProperties +} + +func (e *Event) UnmarshalJSON(data []byte) error { + type unmarshaler Event + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *e = Event(value) + + extraProperties, err := core.ExtractExtraProperties(data, *e) + if err != nil { + return err + } + e.extraProperties = extraProperties + + return nil +} + +func (e *Event) String() string { + if value, err := core.StringifyJSON(e); err == nil { + return value + } + return fmt.Sprintf("%#v", e) +} diff --git a/seed/go-model/mixed-file-directory/user/events/metadata.go b/seed/go-model/mixed-file-directory/user/events/metadata.go new file mode 100644 index 00000000000..2c76cb48be0 --- /dev/null +++ b/seed/go-model/mixed-file-directory/user/events/metadata.go @@ -0,0 +1,45 @@ +// This file was auto-generated by Fern from our API Definition. + +package events + +import ( + json "encoding/json" + fmt "fmt" + fern "github.com/mixed-file-directory/fern" + core "github.com/mixed-file-directory/fern/core" +) + +type Metadata struct { + Id fern.Id `json:"id" url:"id"` + Value interface{} `json:"value,omitempty" url:"value,omitempty"` + + extraProperties map[string]interface{} +} + +func (m *Metadata) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *Metadata) UnmarshalJSON(data []byte) error { + type unmarshaler Metadata + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *m = Metadata(value) + + extraProperties, err := core.ExtractExtraProperties(data, *m) + if err != nil { + return err + } + m.extraProperties = extraProperties + + return nil +} + +func (m *Metadata) String() string { + if value, err := core.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} diff --git a/seed/go-sdk/mixed-file-directory/.github/workflows/ci.yml b/seed/go-sdk/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..d4c0a5dcd95 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Compile + run: go build ./... + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Test + run: go test ./... diff --git a/seed/go-sdk/mixed-file-directory/.mock/definition/__package__.yml b/seed/go-sdk/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/go-sdk/mixed-file-directory/.mock/definition/api.yml b/seed/go-sdk/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/go-sdk/mixed-file-directory/.mock/definition/organization.yml b/seed/go-sdk/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/go-sdk/mixed-file-directory/.mock/definition/user.yml b/seed/go-sdk/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/go-sdk/mixed-file-directory/.mock/definition/user/events.yml b/seed/go-sdk/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/go-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/go-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/go-sdk/mixed-file-directory/.mock/fern.config.json b/seed/go-sdk/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/go-sdk/mixed-file-directory/.mock/generators.yml b/seed/go-sdk/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/go-sdk/mixed-file-directory/client/client.go b/seed/go-sdk/mixed-file-directory/client/client.go new file mode 100644 index 00000000000..32a6631e543 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/client/client.go @@ -0,0 +1,36 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + core "github.com/mixed-file-directory/fern/core" + option "github.com/mixed-file-directory/fern/option" + organization "github.com/mixed-file-directory/fern/organization" + userclient "github.com/mixed-file-directory/fern/user/client" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header + + Organization *organization.Client + User *userclient.Client +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + Organization: organization.NewClient(opts...), + User: userclient.NewClient(opts...), + } +} diff --git a/seed/go-sdk/mixed-file-directory/client/client_test.go b/seed/go-sdk/mixed-file-directory/client/client_test.go new file mode 100644 index 00000000000..acc05af197e --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/client/client_test.go @@ -0,0 +1,45 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + option "github.com/mixed-file-directory/fern/option" + assert "github.com/stretchr/testify/assert" + http "net/http" + testing "testing" + time "time" +) + +func TestNewClient(t *testing.T) { + t.Run("default", func(t *testing.T) { + c := NewClient() + assert.Empty(t, c.baseURL) + }) + + t.Run("base url", func(t *testing.T) { + c := NewClient( + option.WithBaseURL("test.co"), + ) + assert.Equal(t, "test.co", c.baseURL) + }) + + t.Run("http client", func(t *testing.T) { + httpClient := &http.Client{ + Timeout: 5 * time.Second, + } + c := NewClient( + option.WithHTTPClient(httpClient), + ) + assert.Empty(t, c.baseURL) + }) + + t.Run("http header", func(t *testing.T) { + header := make(http.Header) + header.Set("X-API-Tenancy", "test") + c := NewClient( + option.WithHTTPHeader(header), + ) + assert.Empty(t, c.baseURL) + assert.Equal(t, "test", c.header.Get("X-API-Tenancy")) + }) +} diff --git a/seed/go-sdk/mixed-file-directory/core/core.go b/seed/go-sdk/mixed-file-directory/core/core.go new file mode 100644 index 00000000000..14c86c95cb0 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/core.go @@ -0,0 +1,287 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "reflect" +) + +const ( + // contentType specifies the JSON Content-Type header value. + contentType = "application/json" + contentTypeHeader = "Content-Type" +) + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// EncodeURL encodes the given arguments into the URL, escaping +// values as needed. +func EncodeURL(urlFormat string, args ...interface{}) string { + escapedArgs := make([]interface{}, 0, len(args)) + for _, arg := range args { + escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", arg))) + } + return fmt.Sprintf(urlFormat, escapedArgs...) +} + +// MergeHeaders merges the given headers together, where the right +// takes precedence over the left. +func MergeHeaders(left, right http.Header) http.Header { + for key, values := range right { + if len(values) > 1 { + left[key] = values + continue + } + if value := right.Get(key); value != "" { + left.Set(key, value) + } + } + return left +} + +// WriteMultipartJSON writes the given value as a JSON part. +// This is used to serialize non-primitive multipart properties +// (i.e. lists, objects, etc). +func WriteMultipartJSON(writer *multipart.Writer, field string, value interface{}) error { + bytes, err := json.Marshal(value) + if err != nil { + return err + } + return writer.WriteField(field, string(bytes)) +} + +// APIError is a lightweight wrapper around the standard error +// interface that preserves the status code from the RPC, if any. +type APIError struct { + err error + + StatusCode int `json:"-"` +} + +// NewAPIError constructs a new API error. +func NewAPIError(statusCode int, err error) *APIError { + return &APIError{ + err: err, + StatusCode: statusCode, + } +} + +// Unwrap returns the underlying error. This also makes the error compatible +// with errors.As and errors.Is. +func (a *APIError) Unwrap() error { + if a == nil { + return nil + } + return a.err +} + +// Error returns the API error's message. +func (a *APIError) Error() string { + if a == nil || (a.err == nil && a.StatusCode == 0) { + return "" + } + if a.err == nil { + return fmt.Sprintf("%d", a.StatusCode) + } + if a.StatusCode == 0 { + return a.err.Error() + } + return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) +} + +// ErrorDecoder decodes *http.Response errors and returns a +// typed API error (e.g. *APIError). +type ErrorDecoder func(statusCode int, body io.Reader) error + +// Caller calls APIs and deserializes their response, if any. +type Caller struct { + client HTTPClient + retrier *Retrier +} + +// CallerParams represents the parameters used to constrcut a new *Caller. +type CallerParams struct { + Client HTTPClient + MaxAttempts uint +} + +// NewCaller returns a new *Caller backed by the given parameters. +func NewCaller(params *CallerParams) *Caller { + var httpClient HTTPClient = http.DefaultClient + if params.Client != nil { + httpClient = params.Client + } + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + return &Caller{ + client: httpClient, + retrier: NewRetrier(retryOptions...), + } +} + +// CallParams represents the parameters used to issue an API call. +type CallParams struct { + URL string + Method string + MaxAttempts uint + Headers http.Header + Client HTTPClient + Request interface{} + Response interface{} + ResponseIsOptional bool + ErrorDecoder ErrorDecoder +} + +// Call issues an API call according to the given call parameters. +func (c *Caller) Call(ctx context.Context, params *CallParams) error { + req, err := newRequest(ctx, params.URL, params.Method, params.Headers, params.Request) + if err != nil { + return err + } + + // If the call has been cancelled, don't issue the request. + if err := ctx.Err(); err != nil { + return err + } + + client := c.client + if params.Client != nil { + // Use the HTTP client scoped to the request. + client = params.Client + } + + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + + resp, err := c.retrier.Run( + client.Do, + req, + params.ErrorDecoder, + retryOptions..., + ) + if err != nil { + return err + } + + // Close the response body after we're done. + defer resp.Body.Close() + + // Check if the call was cancelled before we return the error + // associated with the call and/or unmarshal the response data. + if err := ctx.Err(); err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return decodeError(resp, params.ErrorDecoder) + } + + // Mutate the response parameter in-place. + if params.Response != nil { + if writer, ok := params.Response.(io.Writer); ok { + _, err = io.Copy(writer, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(params.Response) + } + if err != nil { + if err == io.EOF { + if params.ResponseIsOptional { + // The response is optional, so we should ignore the + // io.EOF error + return nil + } + return fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response) + } + return err + } + } + + return nil +} + +// newRequest returns a new *http.Request with all of the fields +// required to issue the call. +func newRequest( + ctx context.Context, + url string, + method string, + endpointHeaders http.Header, + request interface{}, +) (*http.Request, error) { + requestBody, err := newRequestBody(request) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, method, url, requestBody) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + req.Header.Set(contentTypeHeader, contentType) + for name, values := range endpointHeaders { + req.Header[name] = values + } + return req, nil +} + +// newRequestBody returns a new io.Reader that represents the HTTP request body. +func newRequestBody(request interface{}) (io.Reader, error) { + var requestBody io.Reader + if !isNil(request) { + if body, ok := request.(io.Reader); ok { + requestBody = body + } else { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + requestBody = bytes.NewReader(requestBytes) + } + } + return requestBody, nil +} + +// decodeError decodes the error from the given HTTP response. Note that +// it's the caller's responsibility to close the response body. +func decodeError(response *http.Response, errorDecoder ErrorDecoder) error { + if errorDecoder != nil { + // This endpoint has custom errors, so we'll + // attempt to unmarshal the error into a structured + // type based on the status code. + return errorDecoder(response.StatusCode, response.Body) + } + // This endpoint doesn't have any custom error + // types, so we just read the body as-is, and + // put it into a normal error. + bytes, err := io.ReadAll(response.Body) + if err != nil && err != io.EOF { + return err + } + if err == io.EOF { + // The error didn't have a response body, + // so all we can do is return an error + // with the status code. + return NewAPIError(response.StatusCode, nil) + } + return NewAPIError(response.StatusCode, errors.New(string(bytes))) +} + +// isNil is used to determine if the request value is equal to nil (i.e. an interface +// value that holds a nil concrete value is itself non-nil). +func isNil(value interface{}) bool { + return value == nil || reflect.ValueOf(value).IsNil() +} diff --git a/seed/go-sdk/mixed-file-directory/core/core_test.go b/seed/go-sdk/mixed-file-directory/core/core_test.go new file mode 100644 index 00000000000..adf9e3112cd --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/core_test.go @@ -0,0 +1,303 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCase represents a single test case. +type TestCase struct { + description string + + // Server-side assertions. + giveMethod string + giveResponseIsOptional bool + giveHeader http.Header + giveErrorDecoder ErrorDecoder + giveRequest *Request + + // Client-side assertions. + wantResponse *Response + wantError error +} + +// Request a simple request body. +type Request struct { + Id string `json:"id"` +} + +// Response a simple response body. +type Response struct { + Id string `json:"id"` +} + +// NotFoundError represents a 404. +type NotFoundError struct { + *APIError + + Message string `json:"message"` +} + +func TestCall(t *testing.T) { + tests := []*TestCase{ + { + description: "GET success", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &Request{ + Id: "123", + }, + wantResponse: &Response{ + Id: "123", + }, + }, + { + description: "GET not found", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &Request{ + Id: strconv.Itoa(http.StatusNotFound), + }, + giveErrorDecoder: newTestErrorDecoder(t), + wantError: &NotFoundError{ + APIError: NewAPIError( + http.StatusNotFound, + errors.New(`{"message":"ID \"404\" not found"}`), + ), + }, + }, + { + description: "POST empty body", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: nil, + wantError: NewAPIError( + http.StatusBadRequest, + errors.New("invalid request"), + ), + }, + { + description: "POST optional response", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &Request{ + Id: "123", + }, + giveResponseIsOptional: true, + }, + { + description: "POST API error", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &Request{ + Id: strconv.Itoa(http.StatusInternalServerError), + }, + wantError: NewAPIError( + http.StatusInternalServerError, + errors.New("failed to process request"), + ), + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + var ( + server = newTestServer(t, test) + client = server.Client() + ) + caller := NewCaller( + &CallerParams{ + Client: client, + }, + ) + var response *Response + err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: test.giveMethod, + Headers: test.giveHeader, + Request: test.giveRequest, + Response: &response, + ResponseIsOptional: test.giveResponseIsOptional, + ErrorDecoder: test.giveErrorDecoder, + }, + ) + if test.wantError != nil { + assert.EqualError(t, err, test.wantError.Error()) + return + } + require.NoError(t, err) + assert.Equal(t, test.wantResponse, response) + }) + } +} + +func TestMergeHeaders(t *testing.T) { + t.Run("both empty", func(t *testing.T) { + merged := MergeHeaders(make(http.Header), make(http.Header)) + assert.Empty(t, merged) + }) + + t.Run("empty left", func(t *testing.T) { + left := make(http.Header) + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, "0.0.1", merged.Get("X-API-Version")) + }) + + t.Run("empty right", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Version", "0.0.1") + + right := make(http.Header) + + merged := MergeHeaders(left, right) + assert.Equal(t, "0.0.1", merged.Get("X-API-Version")) + }) + + t.Run("single value override", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Version", "0.0.0") + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version")) + }) + + t.Run("multiple value override", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Versions", "0.0.0") + + right := make(http.Header) + right.Add("X-API-Versions", "0.0.1") + right.Add("X-API-Versions", "0.0.2") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions")) + }) + + t.Run("disjoint merge", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Tenancy", "test") + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy")) + assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version")) + }) +} + +// newTestServer returns a new *httptest.Server configured with the +// given test parameters. +func newTestServer(t *testing.T, tc *TestCase) *httptest.Server { + return httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, tc.giveMethod, r.Method) + assert.Equal(t, contentType, r.Header.Get(contentTypeHeader)) + for header, value := range tc.giveHeader { + assert.Equal(t, value, r.Header.Values(header)) + } + + request := new(Request) + + bytes, err := io.ReadAll(r.Body) + if tc.giveRequest == nil { + require.Empty(t, bytes) + w.WriteHeader(http.StatusBadRequest) + _, err = w.Write([]byte("invalid request")) + require.NoError(t, err) + return + } + require.NoError(t, err) + require.NoError(t, json.Unmarshal(bytes, request)) + + switch request.Id { + case strconv.Itoa(http.StatusNotFound): + notFoundError := &NotFoundError{ + APIError: &APIError{ + StatusCode: http.StatusNotFound, + }, + Message: fmt.Sprintf("ID %q not found", request.Id), + } + bytes, err = json.Marshal(notFoundError) + require.NoError(t, err) + + w.WriteHeader(http.StatusNotFound) + _, err = w.Write(bytes) + require.NoError(t, err) + return + + case strconv.Itoa(http.StatusInternalServerError): + w.WriteHeader(http.StatusInternalServerError) + _, err = w.Write([]byte("failed to process request")) + require.NoError(t, err) + return + } + + if tc.giveResponseIsOptional { + w.WriteHeader(http.StatusOK) + return + } + + response := &Response{ + Id: request.Id, + } + bytes, err = json.Marshal(response) + require.NoError(t, err) + + _, err = w.Write(bytes) + require.NoError(t, err) + }, + ), + ) +} + +// newTestErrorDecoder returns an error decoder suitable for tests. +func newTestErrorDecoder(t *testing.T) func(int, io.Reader) error { + return func(statusCode int, body io.Reader) error { + raw, err := io.ReadAll(body) + require.NoError(t, err) + + var ( + apiError = NewAPIError(statusCode, errors.New(string(raw))) + decoder = json.NewDecoder(bytes.NewReader(raw)) + ) + if statusCode == http.StatusNotFound { + value := new(NotFoundError) + value.APIError = apiError + require.NoError(t, decoder.Decode(value)) + + return value + } + return apiError + } +} diff --git a/seed/go-sdk/mixed-file-directory/core/extra_properties.go b/seed/go-sdk/mixed-file-directory/core/extra_properties.go new file mode 100644 index 00000000000..a6af3e12410 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/extra_properties.go @@ -0,0 +1,141 @@ +package core + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property. +func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) { + return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value}) +} + +// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties. +func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) { + bytes, err := json.Marshal(marshaler) + if err != nil { + return nil, err + } + if len(extraProperties) == 0 { + return bytes, nil + } + keys, err := getKeys(marshaler) + if err != nil { + return nil, err + } + for _, key := range keys { + if _, ok := extraProperties[key]; ok { + return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key) + } + } + extraBytes, err := json.Marshal(extraProperties) + if err != nil { + return nil, err + } + if isEmptyJSON(bytes) { + if isEmptyJSON(extraBytes) { + return bytes, nil + } + return extraBytes, nil + } + result := bytes[:len(bytes)-1] + result = append(result, ',') + result = append(result, extraBytes[1:len(extraBytes)-1]...) + result = append(result, '}') + return result, nil +} + +// ExtractExtraProperties extracts any extra properties from the given value. +func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) { + val := reflect.ValueOf(value) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil, fmt.Errorf("value must be non-nil to extract extra properties") + } + val = val.Elem() + } + if err := json.Unmarshal(bytes, &value); err != nil { + return nil, err + } + var extraProperties map[string]interface{} + if err := json.Unmarshal(bytes, &extraProperties); err != nil { + return nil, err + } + for i := 0; i < val.Type().NumField(); i++ { + key := jsonKey(val.Type().Field(i)) + if key == "" || key == "-" { + continue + } + delete(extraProperties, key) + } + for _, key := range exclude { + delete(extraProperties, key) + } + if len(extraProperties) == 0 { + return nil, nil + } + return extraProperties, nil +} + +// getKeys returns the keys associated with the given value. The value must be a +// a struct or a map with string keys. +func getKeys(value interface{}) ([]string, error) { + val := reflect.ValueOf(value) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return nil, nil + } + switch val.Kind() { + case reflect.Struct: + return getKeysForStructType(val.Type()), nil + case reflect.Map: + var keys []string + if val.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } + for _, key := range val.MapKeys() { + keys = append(keys, key.String()) + } + return keys, nil + default: + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } +} + +// getKeysForStructType returns all the keys associated with the given struct type, +// visiting embedded fields recursively. +func getKeysForStructType(structType reflect.Type) []string { + if structType.Kind() == reflect.Pointer { + structType = structType.Elem() + } + if structType.Kind() != reflect.Struct { + return nil + } + var keys []string + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.Anonymous { + keys = append(keys, getKeysForStructType(field.Type)...) + continue + } + keys = append(keys, jsonKey(field)) + } + return keys +} + +// jsonKey returns the JSON key from the struct tag of the given field, +// excluding the omitempty flag (if any). +func jsonKey(field reflect.StructField) string { + return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty") +} + +// isEmptyJSON returns true if the given data is empty, the empty JSON object, or +// an explicit null. +func isEmptyJSON(data []byte) bool { + return len(data) <= 2 || bytes.Equal(data, []byte("null")) +} diff --git a/seed/go-sdk/mixed-file-directory/core/extra_properties_test.go b/seed/go-sdk/mixed-file-directory/core/extra_properties_test.go new file mode 100644 index 00000000000..dc66fccd7f1 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/extra_properties_test.go @@ -0,0 +1,228 @@ +package core + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMarshaler struct { + Name string `json:"name"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"created_at"` +} + +func (t *testMarshaler) MarshalJSON() ([]byte, error) { + type embed testMarshaler + var marshaler = struct { + embed + BirthDate string `json:"birthDate"` + CreatedAt string `json:"created_at"` + }{ + embed: embed(*t), + BirthDate: t.BirthDate.Format("2006-01-02"), + CreatedAt: t.CreatedAt.Format(time.RFC3339), + } + return MarshalJSONWithExtraProperty(marshaler, "type", "test") +} + +func TestMarshalJSONWithExtraProperties(t *testing.T) { + tests := []struct { + desc string + giveMarshaler interface{} + giveExtraProperties map[string]interface{} + wantBytes []byte + wantError string + }{ + { + desc: "invalid type", + giveMarshaler: []string{"invalid"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`, + }, + { + desc: "invalid key type", + giveMarshaler: map[int]interface{}{42: "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`, + }, + { + desc: "invalid map overwrite", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot add extra property "key" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"}, + wantError: `cannot add extra property "birthDate" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite embedded type", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"name": "bob"}, + wantError: `cannot add extra property "name" because it is already defined on the type`, + }, + { + desc: "nil", + giveMarshaler: nil, + giveExtraProperties: nil, + wantBytes: []byte(`null`), + }, + { + desc: "empty", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{}`), + }, + { + desc: "no extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "only extra properties", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "single extra property", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"extra": "property"}, + wantBytes: []byte(`{"key":"value","extra":"property"}`), + }, + { + desc: "multiple extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"one": 1, "two": 2}, + wantBytes: []byte(`{"key":"value","one":1,"two":2}`), + }, + { + desc: "nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`), + }, + { + desc: "multiple nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "metadata": map[string]interface{}{ + "ip": "127.0.0.1", + }, + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`), + }, + { + desc: "custom marshaler", + giveMarshaler: &testMarshaler{ + Name: "alice", + BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }, + giveExtraProperties: map[string]interface{}{ + "extra": "property", + }, + wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`), + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantBytes, bytes) + + value := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestExtractExtraProperties(t *testing.T) { + t.Run("none", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value) + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value *user + _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + assert.EqualError(t, err, "value must be non-nil to extract extra properties") + }) + + t.Run("non-zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value user + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("exclude", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age") + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) +} diff --git a/seed/go-sdk/mixed-file-directory/core/query.go b/seed/go-sdk/mixed-file-directory/core/query.go new file mode 100644 index 00000000000..2670ff7feda --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/query.go @@ -0,0 +1,231 @@ +package core + +import ( + "encoding/base64" + "fmt" + "net/url" + "reflect" + "strings" + "time" + + "github.com/google/uuid" +) + +var ( + bytesType = reflect.TypeOf([]byte{}) + queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem() + timeType = reflect.TypeOf(time.Time{}) + uuidType = reflect.TypeOf(uuid.UUID{}) +) + +// QueryEncoder is an interface implemented by any type that wishes to encode +// itself into URL values in a non-standard way. +type QueryEncoder interface { + EncodeQueryValues(key string, v *url.Values) error +} + +// QueryValues encodes url.Values from request objects. +// +// Note: This type is inspired by Google's query encoding library, but +// supports far less customization and is tailored to fit this SDK's use case. +// +// Ref: https://github.com/google/go-querystring +func QueryValues(v interface{}) (url.Values, error) { + values := make(url.Values) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return values, nil + } + val = val.Elem() + } + + if v == nil { + return values, nil + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(values, val, "") + return values, err +} + +// reflectValue populates the values parameter from the struct fields in val. +// Embedded structs are followed recursively (using the rules defined in the +// Values function documentation) breadth-first. +func reflectValue(values url.Values, val reflect.Value, scope string) error { + typ := val.Type() + for i := 0; i < typ.NumField(); i++ { + sf := typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { + // Skip unexported fields. + continue + } + + sv := val.Field(i) + tag := sf.Tag.Get("url") + if tag == "" || tag == "-" { + continue + } + + name, opts := parseTag(tag) + if name == "" { + name = sf.Name + } + + if scope != "" { + name = scope + "[" + name + "]" + } + + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + + if sv.Type().Implements(queryEncoderType) { + // If sv is a nil pointer and the custom encoder is defined on a non-pointer + // method receiver, set sv to the zero value of the underlying type + if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) { + sv = reflect.New(sv.Type().Elem()) + } + + m := sv.Interface().(QueryEncoder) + if err := m.EncodeQueryValues(name, &values); err != nil { + return err + } + continue + } + + // Recursively dereference pointers, but stop at nil pointers. + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType { + values.Add(name, valueString(sv, opts, sf)) + continue + } + + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { + if sv.Len() == 0 { + // Skip if slice or array is empty. + continue + } + for i := 0; i < sv.Len(); i++ { + value := sv.Index(i) + if isStructPointer(value) && !value.IsNil() { + if err := reflectValue(values, value.Elem(), name); err != nil { + return err + } + } else { + values.Add(name, valueString(value, opts, sf)) + } + } + continue + } + + if sv.Kind() == reflect.Struct { + if err := reflectValue(values, sv, name); err != nil { + return err + } + continue + } + + values.Add(name, valueString(sv, opts, sf)) + } + + return nil +} + +// valueString returns the string representation of a value. +func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + if v.Type() == timeType { + t := v.Interface().(time.Time) + if format := sf.Tag.Get("format"); format == "date" { + return t.Format("2006-01-02") + } + return t.Format(time.RFC3339) + } + + if v.Type() == uuidType { + u := v.Interface().(uuid.UUID) + return u.String() + } + + if v.Type() == bytesType { + b := v.Interface().([]byte) + return base64.StdEncoding.EncodeToString(b) + } + + return fmt.Sprint(v.Interface()) +} + +// isEmptyValue checks if a value should be considered empty for the purposes +// of omitting fields with the "omitempty" option. +func isEmptyValue(v reflect.Value) bool { + type zeroable interface { + IsZero() bool + } + + if !v.IsZero() { + if z, ok := v.Interface().(zeroable); ok { + return z.IsZero() + } + } + + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer: + return false + } + + return false +} + +// isStructPointer returns true if the given reflect.Value is a pointer to a struct. +func isStructPointer(v reflect.Value) bool { + return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct +} + +// tagOptions is the string following a comma in a struct field's "url" tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/seed/go-sdk/mixed-file-directory/core/query_test.go b/seed/go-sdk/mixed-file-directory/core/query_test.go new file mode 100644 index 00000000000..5498fa92aa5 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/query_test.go @@ -0,0 +1,187 @@ +package core + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQueryValues(t *testing.T) { + t.Run("empty optional", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Empty(t, values) + }) + + t.Run("empty required", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Equal(t, "required=", values.Encode()) + }) + + t.Run("allow multiple", func(t *testing.T) { + type example struct { + Values []string `json:"values" url:"values"` + } + + values, err := QueryValues( + &example{ + Values: []string{"foo", "bar", "baz"}, + }, + ) + require.NoError(t, err) + assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode()) + }) + + t.Run("nested object", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + nestedValue := "nestedValue" + values, err := QueryValues( + &example{ + Required: "requiredValue", + Nested: &nested{ + Value: &nestedValue, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode()) + }) + + t.Run("url unspecified", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("url ignored", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound" url:"-"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("datetime", func(t *testing.T) { + type example struct { + DateTime time.Time `json:"dateTime" url:"dateTime"` + } + + values, err := QueryValues( + &example{ + DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode()) + }) + + t.Run("date", func(t *testing.T) { + type example struct { + Date time.Time `json:"date" url:"date" format:"date"` + } + + values, err := QueryValues( + &example{ + Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "date=1994-03-16", values.Encode()) + }) + + t.Run("optional time", func(t *testing.T) { + type example struct { + Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("omitempty with non-pointer zero value", func(t *testing.T) { + type enum string + + type example struct { + Enum enum `json:"enum,omitempty" url:"enum,omitempty"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("object array", func(t *testing.T) { + type object struct { + Key string `json:"key" url:"key"` + Value string `json:"value" url:"value"` + } + type example struct { + Objects []*object `json:"objects,omitempty" url:"objects,omitempty"` + } + + values, err := QueryValues( + &example{ + Objects: []*object{ + { + Key: "hello", + Value: "world", + }, + { + Key: "foo", + Value: "bar", + }, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode()) + }) +} diff --git a/seed/go-sdk/mixed-file-directory/core/request_option.go b/seed/go-sdk/mixed-file-directory/core/request_option.go new file mode 100644 index 00000000000..7156146816f --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/request_option.go @@ -0,0 +1,85 @@ +// This file was auto-generated by Fern from our API Definition. + +package core + +import ( + http "net/http" +) + +// RequestOption adapts the behavior of the client or an individual request. +type RequestOption interface { + applyRequestOptions(*RequestOptions) +} + +// RequestOptions defines all of the possible request options. +// +// This type is primarily used by the generated code and is not meant +// to be used directly; use the option package instead. +type RequestOptions struct { + BaseURL string + HTTPClient HTTPClient + HTTPHeader http.Header + MaxAttempts uint +} + +// NewRequestOptions returns a new *RequestOptions value. +// +// This function is primarily used by the generated code and is not meant +// to be used directly; use RequestOption instead. +func NewRequestOptions(opts ...RequestOption) *RequestOptions { + options := &RequestOptions{ + HTTPHeader: make(http.Header), + } + for _, opt := range opts { + opt.applyRequestOptions(options) + } + return options +} + +// ToHeader maps the configured request options into a http.Header used +// for the request(s). +func (r *RequestOptions) ToHeader() http.Header { return r.cloneHeader() } + +func (r *RequestOptions) cloneHeader() http.Header { + headers := r.HTTPHeader.Clone() + headers.Set("X-Fern-Language", "Go") + headers.Set("X-Fern-SDK-Name", "github.com/mixed-file-directory/fern") + headers.Set("X-Fern-SDK-Version", "0.0.1") + return headers +} + +// BaseURLOption implements the RequestOption interface. +type BaseURLOption struct { + BaseURL string +} + +func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) { + opts.BaseURL = b.BaseURL +} + +// HTTPClientOption implements the RequestOption interface. +type HTTPClientOption struct { + HTTPClient HTTPClient +} + +func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) { + opts.HTTPClient = h.HTTPClient +} + +// HTTPHeaderOption implements the RequestOption interface. +type HTTPHeaderOption struct { + HTTPHeader http.Header +} + +func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) { + opts.HTTPHeader = h.HTTPHeader +} + +// MaxAttemptsOption implements the RequestOption interface. +type MaxAttemptsOption struct { + MaxAttempts uint +} + +func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) { + opts.MaxAttempts = m.MaxAttempts +} diff --git a/seed/go-sdk/mixed-file-directory/core/retrier.go b/seed/go-sdk/mixed-file-directory/core/retrier.go new file mode 100644 index 00000000000..ea24916b786 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/retrier.go @@ -0,0 +1,166 @@ +package core + +import ( + "crypto/rand" + "math/big" + "net/http" + "time" +) + +const ( + defaultRetryAttempts = 2 + minRetryDelay = 500 * time.Millisecond + maxRetryDelay = 5000 * time.Millisecond +) + +// RetryOption adapts the behavior the *Retrier. +type RetryOption func(*retryOptions) + +// RetryFunc is a retriable HTTP function call (i.e. *http.Client.Do). +type RetryFunc func(*http.Request) (*http.Response, error) + +// WithMaxAttempts configures the maximum number of attempts +// of the *Retrier. +func WithMaxAttempts(attempts uint) RetryOption { + return func(opts *retryOptions) { + opts.attempts = attempts + } +} + +// Retrier retries failed requests a configurable number of times with an +// exponential back-off between each retry. +type Retrier struct { + attempts uint +} + +// NewRetrier constructs a new *Retrier with the given options, if any. +func NewRetrier(opts ...RetryOption) *Retrier { + options := new(retryOptions) + for _, opt := range opts { + opt(options) + } + attempts := uint(defaultRetryAttempts) + if options.attempts > 0 { + attempts = options.attempts + } + return &Retrier{ + attempts: attempts, + } +} + +// Run issues the request and, upon failure, retries the request if possible. +// +// The request will be retried as long as the request is deemed retriable and the +// number of retry attempts has not grown larger than the configured retry limit. +func (r *Retrier) Run( + fn RetryFunc, + request *http.Request, + errorDecoder ErrorDecoder, + opts ...RetryOption, +) (*http.Response, error) { + options := new(retryOptions) + for _, opt := range opts { + opt(options) + } + maxRetryAttempts := r.attempts + if options.attempts > 0 { + maxRetryAttempts = options.attempts + } + var ( + retryAttempt uint + previousError error + ) + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt, + previousError, + ) +} + +func (r *Retrier) run( + fn RetryFunc, + request *http.Request, + errorDecoder ErrorDecoder, + maxRetryAttempts uint, + retryAttempt uint, + previousError error, +) (*http.Response, error) { + if retryAttempt >= maxRetryAttempts { + return nil, previousError + } + + // If the call has been cancelled, don't issue the request. + if err := request.Context().Err(); err != nil { + return nil, err + } + + response, err := fn(request) + if err != nil { + return nil, err + } + + if r.shouldRetry(response) { + defer response.Body.Close() + + delay, err := r.retryDelay(retryAttempt) + if err != nil { + return nil, err + } + + time.Sleep(delay) + + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt+1, + decodeError(response, errorDecoder), + ) + } + + return response, nil +} + +// shouldRetry returns true if the request should be retried based on the given +// response status code. +func (r *Retrier) shouldRetry(response *http.Response) bool { + return response.StatusCode == http.StatusTooManyRequests || + response.StatusCode == http.StatusRequestTimeout || + response.StatusCode == http.StatusConflict || + response.StatusCode >= http.StatusInternalServerError +} + +// retryDelay calculates the delay time in milliseconds based on the retry attempt. +func (r *Retrier) retryDelay(retryAttempt uint) (time.Duration, error) { + // Apply exponential backoff. + delay := minRetryDelay + minRetryDelay*time.Duration(retryAttempt*retryAttempt) + + // Do not allow the number to exceed maxRetryDelay. + if delay > maxRetryDelay { + delay = maxRetryDelay + } + + // Apply some itter by randomizing the value in the range of 75%-100%. + max := big.NewInt(int64(delay / 4)) + jitter, err := rand.Int(rand.Reader, max) + if err != nil { + return 0, err + } + + delay -= time.Duration(jitter.Int64()) + + // Never sleep less than the base sleep seconds. + if delay < minRetryDelay { + delay = minRetryDelay + } + + return delay, nil +} + +type retryOptions struct { + attempts uint +} diff --git a/seed/go-sdk/mixed-file-directory/core/stringer.go b/seed/go-sdk/mixed-file-directory/core/stringer.go new file mode 100644 index 00000000000..000cf448641 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/stringer.go @@ -0,0 +1,13 @@ +package core + +import "encoding/json" + +// StringifyJSON returns a pretty JSON string representation of +// the given value. +func StringifyJSON(value interface{}) (string, error) { + bytes, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/seed/go-sdk/mixed-file-directory/core/time.go b/seed/go-sdk/mixed-file-directory/core/time.go new file mode 100644 index 00000000000..d009ab30c90 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/core/time.go @@ -0,0 +1,137 @@ +package core + +import ( + "encoding/json" + "time" +) + +const dateFormat = "2006-01-02" + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date (e.g. 2006-01-02). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type Date struct { + t *time.Time +} + +// NewDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewDate(t time.Time) *Date { + return &Date{t: &t} +} + +// NewOptionalDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDate(t *time.Time) *Date { + if t == nil { + return nil + } + return &Date{t: t} +} + +// Time returns the Date's underlying time, if any. If the +// date is nil, the zero value is returned. +func (d *Date) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the Date's underlying time.Time, if any. +func (d *Date) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *Date) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(dateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(dateFormat, raw) + if err != nil { + return err + } + + *d = Date{t: &parsedTime} + return nil +} + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type DateTime struct { + t *time.Time +} + +// NewDateTime returns a new *DateTime. +func NewDateTime(t time.Time) *DateTime { + return &DateTime{t: &t} +} + +// NewOptionalDateTime returns a new *DateTime. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDateTime(t *time.Time) *DateTime { + if t == nil { + return nil + } + return &DateTime{t: t} +} + +// Time returns the DateTime's underlying time, if any. If the +// date-time is nil, the zero value is returned. +func (d *DateTime) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the DateTime's underlying time.Time, if any. +func (d *DateTime) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *DateTime) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(time.RFC3339)) +} + +func (d *DateTime) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(time.RFC3339, raw) + if err != nil { + return err + } + + *d = DateTime{t: &parsedTime} + return nil +} diff --git a/seed/go-sdk/mixed-file-directory/go.mod b/seed/go-sdk/mixed-file-directory/go.mod new file mode 100644 index 00000000000..f48741fed51 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/go.mod @@ -0,0 +1,9 @@ +module github.com/mixed-file-directory/fern + +go 1.13 + +require ( + github.com/google/uuid v1.4.0 + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/seed/go-sdk/mixed-file-directory/go.sum b/seed/go-sdk/mixed-file-directory/go.sum new file mode 100644 index 00000000000..b3766d4366b --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/seed/go-sdk/mixed-file-directory/option/request_option.go b/seed/go-sdk/mixed-file-directory/option/request_option.go new file mode 100644 index 00000000000..2e3a546e48d --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/option/request_option.go @@ -0,0 +1,41 @@ +// This file was auto-generated by Fern from our API Definition. + +package option + +import ( + core "github.com/mixed-file-directory/fern/core" + http "net/http" +) + +// RequestOption adapts the behavior of an indivdual request. +type RequestOption = core.RequestOption + +// WithBaseURL sets the base URL, overriding the default +// environment, if any. +func WithBaseURL(baseURL string) *core.BaseURLOption { + return &core.BaseURLOption{ + BaseURL: baseURL, + } +} + +// WithHTTPClient uses the given HTTPClient to issue the request. +func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption { + return &core.HTTPClientOption{ + HTTPClient: httpClient, + } +} + +// WithHTTPHeader adds the given http.Header to the request. +func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption { + return &core.HTTPHeaderOption{ + // Clone the headers so they can't be modified after the option call. + HTTPHeader: httpHeader.Clone(), + } +} + +// WithMaxAttempts configures the maximum number of retry attempts. +func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption { + return &core.MaxAttemptsOption{ + MaxAttempts: attempts, + } +} diff --git a/seed/go-sdk/mixed-file-directory/organization.go b/seed/go-sdk/mixed-file-directory/organization.go new file mode 100644 index 00000000000..0f05068bb4d --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/organization.go @@ -0,0 +1,93 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +import ( + json "encoding/json" + fmt "fmt" + core "github.com/mixed-file-directory/fern/core" +) + +type CreateOrganizationRequest struct { + Name string `json:"name" url:"name"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (c *CreateOrganizationRequest) GetExtraProperties() map[string]interface{} { + return c.extraProperties +} + +func (c *CreateOrganizationRequest) UnmarshalJSON(data []byte) error { + type unmarshaler CreateOrganizationRequest + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *c = CreateOrganizationRequest(value) + + extraProperties, err := core.ExtractExtraProperties(data, *c) + if err != nil { + return err + } + c.extraProperties = extraProperties + + c._rawJSON = json.RawMessage(data) + return nil +} + +func (c *CreateOrganizationRequest) String() string { + if len(c._rawJSON) > 0 { + if value, err := core.StringifyJSON(c._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(c); err == nil { + return value + } + return fmt.Sprintf("%#v", c) +} + +type Organization struct { + Id Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + Users []*User `json:"users,omitempty" url:"users,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (o *Organization) GetExtraProperties() map[string]interface{} { + return o.extraProperties +} + +func (o *Organization) UnmarshalJSON(data []byte) error { + type unmarshaler Organization + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = Organization(value) + + extraProperties, err := core.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + + o._rawJSON = json.RawMessage(data) + return nil +} + +func (o *Organization) String() string { + if len(o._rawJSON) > 0 { + if value, err := core.StringifyJSON(o._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} diff --git a/seed/go-sdk/mixed-file-directory/organization/client.go b/seed/go-sdk/mixed-file-directory/organization/client.go new file mode 100644 index 00000000000..6e1a340072e --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/organization/client.go @@ -0,0 +1,68 @@ +// This file was auto-generated by Fern from our API Definition. + +package organization + +import ( + context "context" + fern "github.com/mixed-file-directory/fern" + core "github.com/mixed-file-directory/fern/core" + option "github.com/mixed-file-directory/fern/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + } +} + +// Create a new organization. +func (c *Client) Create( + ctx context.Context, + request *fern.CreateOrganizationRequest, + opts ...option.RequestOption, +) (*fern.Organization, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/organizations/" + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response *fern.Organization + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + MaxAttempts: options.MaxAttempts, + Headers: headers, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ); err != nil { + return nil, err + } + return response, nil +} diff --git a/seed/go-sdk/mixed-file-directory/pointer.go b/seed/go-sdk/mixed-file-directory/pointer.go new file mode 100644 index 00000000000..fd8f9227b66 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/pointer.go @@ -0,0 +1,132 @@ +package mixedfiledirectory + +import ( + "time" + + "github.com/google/uuid" +) + +// Bool returns a pointer to the given bool value. +func Bool(b bool) *bool { + return &b +} + +// Byte returns a pointer to the given byte value. +func Byte(b byte) *byte { + return &b +} + +// Complex64 returns a pointer to the given complex64 value. +func Complex64(c complex64) *complex64 { + return &c +} + +// Complex128 returns a pointer to the given complex128 value. +func Complex128(c complex128) *complex128 { + return &c +} + +// Float32 returns a pointer to the given float32 value. +func Float32(f float32) *float32 { + return &f +} + +// Float64 returns a pointer to the given float64 value. +func Float64(f float64) *float64 { + return &f +} + +// Int returns a pointer to the given int value. +func Int(i int) *int { + return &i +} + +// Int8 returns a pointer to the given int8 value. +func Int8(i int8) *int8 { + return &i +} + +// Int16 returns a pointer to the given int16 value. +func Int16(i int16) *int16 { + return &i +} + +// Int32 returns a pointer to the given int32 value. +func Int32(i int32) *int32 { + return &i +} + +// Int64 returns a pointer to the given int64 value. +func Int64(i int64) *int64 { + return &i +} + +// Rune returns a pointer to the given rune value. +func Rune(r rune) *rune { + return &r +} + +// String returns a pointer to the given string value. +func String(s string) *string { + return &s +} + +// Uint returns a pointer to the given uint value. +func Uint(u uint) *uint { + return &u +} + +// Uint8 returns a pointer to the given uint8 value. +func Uint8(u uint8) *uint8 { + return &u +} + +// Uint16 returns a pointer to the given uint16 value. +func Uint16(u uint16) *uint16 { + return &u +} + +// Uint32 returns a pointer to the given uint32 value. +func Uint32(u uint32) *uint32 { + return &u +} + +// Uint64 returns a pointer to the given uint64 value. +func Uint64(u uint64) *uint64 { + return &u +} + +// Uintptr returns a pointer to the given uintptr value. +func Uintptr(u uintptr) *uintptr { + return &u +} + +// UUID returns a pointer to the given uuid.UUID value. +func UUID(u uuid.UUID) *uuid.UUID { + return &u +} + +// Time returns a pointer to the given time.Time value. +func Time(t time.Time) *time.Time { + return &t +} + +// MustParseDate attempts to parse the given string as a +// date time.Time, and panics upon failure. +func MustParseDate(date string) time.Time { + t, err := time.Parse("2006-01-02", date) + if err != nil { + panic(err) + } + return t +} + +// MustParseDateTime attempts to parse the given string as a +// datetime time.Time, and panics upon failure. +func MustParseDateTime(datetime string) time.Time { + t, err := time.Parse(time.RFC3339, datetime) + if err != nil { + panic(err) + } + return t +} diff --git a/seed/go-sdk/mixed-file-directory/snippet-templates.json b/seed/go-sdk/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/go-sdk/mixed-file-directory/snippet.json b/seed/go-sdk/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..261a2de49a8 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/snippet.json @@ -0,0 +1,48 @@ +{ + "endpoints": [ + { + "id": { + "path": "/organizations", + "method": "POST", + "identifier_override": "endpoint_organization.create" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/mixed-file-directory/fern\"\n\tfernclient \"github.com/mixed-file-directory/fern/client\"\n)\n\nclient := fernclient.NewClient()\nresponse, err := client.Organization.Create(\n\tcontext.TODO(),\n\t\u0026fern.CreateOrganizationRequest{\n\t\tName: \"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/users", + "method": "GET", + "identifier_override": "endpoint_user.list" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/mixed-file-directory/fern\"\n\tfernclient \"github.com/mixed-file-directory/fern/client\"\n)\n\nclient := fernclient.NewClient()\nresponse, err := client.User.List(\n\tcontext.TODO(),\n\t\u0026fern.ListUsersRequest{\n\t\tLimit: fern.Int(\n\t\t\t1,\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/users/events", + "method": "GET", + "identifier_override": "endpoint_user/events.listEvents" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/mixed-file-directory/fern\"\n\tfernclient \"github.com/mixed-file-directory/fern/client\"\n\tuser \"github.com/mixed-file-directory/fern/user\"\n)\n\nclient := fernclient.NewClient()\nresponse, err := client.User.Events.ListEvents(\n\tcontext.TODO(),\n\t\u0026user.ListUserEventsRequest{\n\t\tLimit: fern.Int(\n\t\t\t1,\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/users/events/metadata", + "method": "GET", + "identifier_override": "endpoint_user/events/metadata.getMetadata" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tevents \"github.com/mixed-file-directory/fern/user/events\"\n\tfernclient \"github.com/mixed-file-directory/fern/client\"\n)\n\nclient := fernclient.NewClient()\nresponse, err := client.User.Events.Metadata.GetMetadata(\n\tcontext.TODO(),\n\t\u0026events.GetEventMetadataRequest{\n\t\tId: \"string\",\n\t},\n)\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/go-sdk/mixed-file-directory/types.go b/seed/go-sdk/mixed-file-directory/types.go new file mode 100644 index 00000000000..cacf3342a01 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/types.go @@ -0,0 +1,5 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +type Id = string diff --git a/seed/go-sdk/mixed-file-directory/user.go b/seed/go-sdk/mixed-file-directory/user.go new file mode 100644 index 00000000000..653140665cf --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/user.go @@ -0,0 +1,57 @@ +// This file was auto-generated by Fern from our API Definition. + +package mixedfiledirectory + +import ( + json "encoding/json" + fmt "fmt" + core "github.com/mixed-file-directory/fern/core" +) + +type ListUsersRequest struct { + // The maximum number of results to return. + Limit *int `json:"-" url:"limit,omitempty"` +} + +type User struct { + Id Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (u *User) GetExtraProperties() map[string]interface{} { + return u.extraProperties +} + +func (u *User) UnmarshalJSON(data []byte) error { + type unmarshaler User + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *u = User(value) + + extraProperties, err := core.ExtractExtraProperties(data, *u) + if err != nil { + return err + } + u.extraProperties = extraProperties + + u._rawJSON = json.RawMessage(data) + return nil +} + +func (u *User) String() string { + if len(u._rawJSON) > 0 { + if value, err := core.StringifyJSON(u._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(u); err == nil { + return value + } + return fmt.Sprintf("%#v", u) +} diff --git a/seed/go-sdk/mixed-file-directory/user/client/client.go b/seed/go-sdk/mixed-file-directory/user/client/client.go new file mode 100644 index 00000000000..fe592dcc369 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/user/client/client.go @@ -0,0 +1,79 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + context "context" + fern "github.com/mixed-file-directory/fern" + core "github.com/mixed-file-directory/fern/core" + option "github.com/mixed-file-directory/fern/option" + eventsclient "github.com/mixed-file-directory/fern/user/events/client" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header + + Events *eventsclient.Client +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + Events: eventsclient.NewClient(opts...), + } +} + +// List all users. +func (c *Client) List( + ctx context.Context, + request *fern.ListUsersRequest, + opts ...option.RequestOption, +) ([]*fern.User, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/users/" + + queryParams, err := core.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response []*fern.User + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + Client: options.HTTPClient, + Response: &response, + }, + ); err != nil { + return nil, err + } + return response, nil +} diff --git a/seed/go-sdk/mixed-file-directory/user/events.go b/seed/go-sdk/mixed-file-directory/user/events.go new file mode 100644 index 00000000000..8b87963267f --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/user/events.go @@ -0,0 +1,57 @@ +// This file was auto-generated by Fern from our API Definition. + +package user + +import ( + json "encoding/json" + fmt "fmt" + fern "github.com/mixed-file-directory/fern" + core "github.com/mixed-file-directory/fern/core" +) + +type ListUserEventsRequest struct { + // The maximum number of results to return. + Limit *int `json:"-" url:"limit,omitempty"` +} + +type Event struct { + Id fern.Id `json:"id" url:"id"` + Name string `json:"name" url:"name"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (e *Event) GetExtraProperties() map[string]interface{} { + return e.extraProperties +} + +func (e *Event) UnmarshalJSON(data []byte) error { + type unmarshaler Event + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *e = Event(value) + + extraProperties, err := core.ExtractExtraProperties(data, *e) + if err != nil { + return err + } + e.extraProperties = extraProperties + + e._rawJSON = json.RawMessage(data) + return nil +} + +func (e *Event) String() string { + if len(e._rawJSON) > 0 { + if value, err := core.StringifyJSON(e._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(e); err == nil { + return value + } + return fmt.Sprintf("%#v", e) +} diff --git a/seed/go-sdk/mixed-file-directory/user/events/client/client.go b/seed/go-sdk/mixed-file-directory/user/events/client/client.go new file mode 100644 index 00000000000..1b8e094af15 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/user/events/client/client.go @@ -0,0 +1,79 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + context "context" + core "github.com/mixed-file-directory/fern/core" + option "github.com/mixed-file-directory/fern/option" + user "github.com/mixed-file-directory/fern/user" + metadata "github.com/mixed-file-directory/fern/user/events/metadata" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header + + Metadata *metadata.Client +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + Metadata: metadata.NewClient(opts...), + } +} + +// List all user events. +func (c *Client) ListEvents( + ctx context.Context, + request *user.ListUserEventsRequest, + opts ...option.RequestOption, +) ([]*user.Event, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/users/events/" + + queryParams, err := core.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response []*user.Event + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + Client: options.HTTPClient, + Response: &response, + }, + ); err != nil { + return nil, err + } + return response, nil +} diff --git a/seed/go-sdk/mixed-file-directory/user/events/metadata.go b/seed/go-sdk/mixed-file-directory/user/events/metadata.go new file mode 100644 index 00000000000..da35f2ecc38 --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/user/events/metadata.go @@ -0,0 +1,56 @@ +// This file was auto-generated by Fern from our API Definition. + +package events + +import ( + json "encoding/json" + fmt "fmt" + fern "github.com/mixed-file-directory/fern" + core "github.com/mixed-file-directory/fern/core" +) + +type GetEventMetadataRequest struct { + Id fern.Id `json:"-" url:"id"` +} + +type Metadata struct { + Id fern.Id `json:"id" url:"id"` + Value interface{} `json:"value,omitempty" url:"value,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (m *Metadata) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *Metadata) UnmarshalJSON(data []byte) error { + type unmarshaler Metadata + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *m = Metadata(value) + + extraProperties, err := core.ExtractExtraProperties(data, *m) + if err != nil { + return err + } + m.extraProperties = extraProperties + + m._rawJSON = json.RawMessage(data) + return nil +} + +func (m *Metadata) String() string { + if len(m._rawJSON) > 0 { + if value, err := core.StringifyJSON(m._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} diff --git a/seed/go-sdk/mixed-file-directory/user/events/metadata/client.go b/seed/go-sdk/mixed-file-directory/user/events/metadata/client.go new file mode 100644 index 00000000000..30f8fb886ec --- /dev/null +++ b/seed/go-sdk/mixed-file-directory/user/events/metadata/client.go @@ -0,0 +1,75 @@ +// This file was auto-generated by Fern from our API Definition. + +package metadata + +import ( + context "context" + core "github.com/mixed-file-directory/fern/core" + option "github.com/mixed-file-directory/fern/option" + events "github.com/mixed-file-directory/fern/user/events" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + } +} + +// Get event metadata. +func (c *Client) GetMetadata( + ctx context.Context, + request *events.GetEventMetadataRequest, + opts ...option.RequestOption, +) (*events.Metadata, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/users/events/metadata/" + + queryParams, err := core.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response *events.Metadata + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + Client: options.HTTPClient, + Response: &response, + }, + ); err != nil { + return nil, err + } + return response, nil +} diff --git a/seed/java-model/mixed-file-directory/.github/workflows/ci.yml b/seed/java-model/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..8598a73092a --- /dev/null +++ b/seed/java-model/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Compile + run: ./gradlew compileJava + + test: + needs: [ compile ] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Test + run: ./gradlew test + publish: + needs: [ compile, test ] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Publish to maven + run: | + ./gradlew publish + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLISH_REGISTRY_URL: "" diff --git a/seed/java-model/mixed-file-directory/.gitignore b/seed/java-model/mixed-file-directory/.gitignore new file mode 100644 index 00000000000..d4199abc2cd --- /dev/null +++ b/seed/java-model/mixed-file-directory/.gitignore @@ -0,0 +1,24 @@ +*.class +.project +.gradle +? +.classpath +.checkstyle +.settings +.node +build + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Eclipse/IntelliJ APT +generated_src/ +generated_testSrc/ +generated/ + +bin +build \ No newline at end of file diff --git a/seed/java-model/mixed-file-directory/.mock/definition/__package__.yml b/seed/java-model/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/java-model/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/java-model/mixed-file-directory/.mock/definition/api.yml b/seed/java-model/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/java-model/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/java-model/mixed-file-directory/.mock/definition/organization.yml b/seed/java-model/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/java-model/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/java-model/mixed-file-directory/.mock/definition/user.yml b/seed/java-model/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/java-model/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/java-model/mixed-file-directory/.mock/definition/user/events.yml b/seed/java-model/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/java-model/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/java-model/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/java-model/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/java-model/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/java-model/mixed-file-directory/.mock/fern.config.json b/seed/java-model/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/java-model/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/java-model/mixed-file-directory/.mock/generators.yml b/seed/java-model/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/java-model/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/java-model/mixed-file-directory/build.gradle b/seed/java-model/mixed-file-directory/build.gradle new file mode 100644 index 00000000000..765cfec9724 --- /dev/null +++ b/seed/java-model/mixed-file-directory/build.gradle @@ -0,0 +1,67 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'com.diffplug.spotless' version '6.11.0' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + api 'com.fasterxml.jackson.core:jackson-databind:2.13.0' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.12.3' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3' +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +spotless { + java { + palantirJavaFormat() + } +} + +java { + withSourcesJar() + withJavadocJar() +} + +test { + useJUnitPlatform() + testLogging { + showStandardStreams = true + } +} +publishing { + publications { + maven(MavenPublication) { + groupId = 'com.fern' + artifactId = 'mixed-file-directory' + version = '0.0.1' + from components.java + pom { + scm { + connection = 'scm:git:git://github.com/mixed-file-directory/fern.git' + developerConnection = 'scm:git:git://github.com/mixed-file-directory/fern.git' + url = 'https://github.com/mixed-file-directory/fern' + } + } + } + } + repositories { + maven { + url "$System.env.MAVEN_PUBLISH_REGISTRY_URL" + credentials { + username "$System.env.MAVEN_USERNAME" + password "$System.env.MAVEN_PASSWORD" + } + } + } +} + diff --git a/seed/java-model/mixed-file-directory/settings.gradle b/seed/java-model/mixed-file-directory/settings.gradle new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-model/mixed-file-directory/snippet-templates.json b/seed/java-model/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-model/mixed-file-directory/snippet.json b/seed/java-model/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/DateTimeDeserializer.java b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/DateTimeDeserializer.java new file mode 100644 index 00000000000..934f58b3689 --- /dev/null +++ b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/DateTimeDeserializer.java @@ -0,0 +1,55 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; + +/** + * Custom deserializer that handles converting ISO8601 dates into {@link OffsetDateTime} objects. + */ +class DateTimeDeserializer extends JsonDeserializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer()); + } + + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken token = parser.currentToken(); + if (token == JsonToken.VALUE_NUMBER_INT) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); + } else { + TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( + parser.getValueAsString(), OffsetDateTime::from, LocalDateTime::from); + + if (temporal.query(TemporalQueries.offset()) == null) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return OffsetDateTime.from(temporal); + } + } + } +} diff --git a/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ObjectMappers.java b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ObjectMappers.java new file mode 100644 index 00000000000..b68f964d702 --- /dev/null +++ b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ObjectMappers.java @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; + +public final class ObjectMappers { + public static final ObjectMapper JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(DateTimeDeserializer.getModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + private ObjectMappers() {} + + public static String stringify(Object o) { + try { + return JSON_MAPPER + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } catch (IOException e) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); + } + } +} diff --git a/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/organization/CreateOrganizationRequest.java b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/organization/CreateOrganizationRequest.java new file mode 100644 index 00000000000..1feb60dbd67 --- /dev/null +++ b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/organization/CreateOrganizationRequest.java @@ -0,0 +1,86 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.model.organization; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = CreateOrganizationRequest.Builder.class) +public final class CreateOrganizationRequest { + private final String name; + + private CreateOrganizationRequest(String name) { + this.name = name; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof CreateOrganizationRequest && equalTo((CreateOrganizationRequest) other); + } + + private boolean equalTo(CreateOrganizationRequest other) { + return name.equals(other.name); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.name); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static NameStage builder() { + return new Builder(); + } + + public interface NameStage { + _FinalStage name(String name); + + Builder from(CreateOrganizationRequest other); + } + + public interface _FinalStage { + CreateOrganizationRequest build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements NameStage, _FinalStage { + private String name; + + private Builder() {} + + @java.lang.Override + public Builder from(CreateOrganizationRequest other) { + name(other.getName()); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public CreateOrganizationRequest build() { + return new CreateOrganizationRequest(name); + } + } +} diff --git a/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/organization/Organization.java b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/organization/Organization.java new file mode 100644 index 00000000000..6ad877bcb73 --- /dev/null +++ b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/organization/Organization.java @@ -0,0 +1,149 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.model.organization; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import com.seed.mixedFileDirectory.model.user.User; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = Organization.Builder.class) +public final class Organization { + private final String id; + + private final String name; + + private final List users; + + private Organization(String id, String name, List users) { + this.id = id; + this.name = name; + this.users = users; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("users") + public List getUsers() { + return users; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Organization && equalTo((Organization) other); + } + + private boolean equalTo(Organization other) { + return id.equals(other.id) && name.equals(other.name) && users.equals(other.users); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.users); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(String id); + + Builder from(Organization other); + } + + public interface NameStage { + _FinalStage name(String name); + } + + public interface _FinalStage { + Organization build(); + + _FinalStage users(List users); + + _FinalStage addUsers(User users); + + _FinalStage addAllUsers(List users); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, NameStage, _FinalStage { + private String id; + + private String name; + + private List users = new ArrayList<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(Organization other) { + id(other.getId()); + name(other.getName()); + users(other.getUsers()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public _FinalStage addAllUsers(List users) { + this.users.addAll(users); + return this; + } + + @java.lang.Override + public _FinalStage addUsers(User users) { + this.users.add(users); + return this; + } + + @java.lang.Override + @JsonSetter(value = "users", nulls = Nulls.SKIP) + public _FinalStage users(List users) { + this.users.clear(); + this.users.addAll(users); + return this; + } + + @java.lang.Override + public Organization build() { + return new Organization(id, name, users); + } + } +} diff --git a/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/User.java b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/User.java new file mode 100644 index 00000000000..f3c0eb93b87 --- /dev/null +++ b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/User.java @@ -0,0 +1,130 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.model.user; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = User.Builder.class) +public final class User { + private final String id; + + private final String name; + + private final int age; + + private User(String id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("age") + public int getAge() { + return age; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof User && equalTo((User) other); + } + + private boolean equalTo(User other) { + return id.equals(other.id) && name.equals(other.name) && age == other.age; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.age); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(String id); + + Builder from(User other); + } + + public interface NameStage { + AgeStage name(String name); + } + + public interface AgeStage { + _FinalStage age(int age); + } + + public interface _FinalStage { + User build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, NameStage, AgeStage, _FinalStage { + private String id; + + private String name; + + private int age; + + private Builder() {} + + @java.lang.Override + public Builder from(User other) { + id(other.getId()); + name(other.getName()); + age(other.getAge()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public AgeStage name(String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("age") + public _FinalStage age(int age) { + this.age = age; + return this; + } + + @java.lang.Override + public User build() { + return new User(id, name, age); + } + } +} diff --git a/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/events/Event.java b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/events/Event.java new file mode 100644 index 00000000000..bfdf640716e --- /dev/null +++ b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/events/Event.java @@ -0,0 +1,108 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.model.user.events; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = Event.Builder.class) +public final class Event { + private final String id; + + private final String name; + + private Event(String id, String name) { + this.id = id; + this.name = name; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Event && equalTo((Event) other); + } + + private boolean equalTo(Event other) { + return id.equals(other.id) && name.equals(other.name); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(String id); + + Builder from(Event other); + } + + public interface NameStage { + _FinalStage name(String name); + } + + public interface _FinalStage { + Event build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, NameStage, _FinalStage { + private String id; + + private String name; + + private Builder() {} + + @java.lang.Override + public Builder from(Event other) { + id(other.getId()); + name(other.getName()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public Event build() { + return new Event(id, name); + } + } +} diff --git a/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/events/metadata/Metadata.java b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/events/metadata/Metadata.java new file mode 100644 index 00000000000..75398b7eaaf --- /dev/null +++ b/seed/java-model/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/model/user/events/metadata/Metadata.java @@ -0,0 +1,108 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.model.user.events.metadata; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = Metadata.Builder.class) +public final class Metadata { + private final String id; + + private final Object value; + + private Metadata(String id, Object value) { + this.id = id; + this.value = value; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("value") + public Object getValue() { + return value; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Metadata && equalTo((Metadata) other); + } + + private boolean equalTo(Metadata other) { + return id.equals(other.id) && value.equals(other.value); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.value); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + ValueStage id(String id); + + Builder from(Metadata other); + } + + public interface ValueStage { + _FinalStage value(Object value); + } + + public interface _FinalStage { + Metadata build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, ValueStage, _FinalStage { + private String id; + + private Object value; + + private Builder() {} + + @java.lang.Override + public Builder from(Metadata other) { + id(other.getId()); + value(other.getValue()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public ValueStage id(String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("value") + public _FinalStage value(Object value) { + this.value = Objects.requireNonNull(value, "value must not be null"); + return this; + } + + @java.lang.Override + public Metadata build() { + return new Metadata(id, value); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/.github/workflows/ci.yml b/seed/java-sdk/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..8598a73092a --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Compile + run: ./gradlew compileJava + + test: + needs: [ compile ] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Test + run: ./gradlew test + publish: + needs: [ compile, test ] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Publish to maven + run: | + ./gradlew publish + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLISH_REGISTRY_URL: "" diff --git a/seed/java-sdk/mixed-file-directory/.gitignore b/seed/java-sdk/mixed-file-directory/.gitignore new file mode 100644 index 00000000000..d4199abc2cd --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.gitignore @@ -0,0 +1,24 @@ +*.class +.project +.gradle +? +.classpath +.checkstyle +.settings +.node +build + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Eclipse/IntelliJ APT +generated_src/ +generated_testSrc/ +generated/ + +bin +build \ No newline at end of file diff --git a/seed/java-sdk/mixed-file-directory/.mock/definition/__package__.yml b/seed/java-sdk/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/java-sdk/mixed-file-directory/.mock/definition/api.yml b/seed/java-sdk/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/java-sdk/mixed-file-directory/.mock/definition/organization.yml b/seed/java-sdk/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/java-sdk/mixed-file-directory/.mock/definition/user.yml b/seed/java-sdk/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/java-sdk/mixed-file-directory/.mock/definition/user/events.yml b/seed/java-sdk/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/java-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/java-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/java-sdk/mixed-file-directory/.mock/fern.config.json b/seed/java-sdk/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/java-sdk/mixed-file-directory/.mock/generators.yml b/seed/java-sdk/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/java-sdk/mixed-file-directory/build.gradle b/seed/java-sdk/mixed-file-directory/build.gradle new file mode 100644 index 00000000000..a651f6d3ff8 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'com.diffplug.spotless' version '6.11.0' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + api 'com.squareup.okhttp3:okhttp:4.12.0' + api 'com.fasterxml.jackson.core:jackson-databind:2.13.0' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.12.3' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2' +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +spotless { + java { + palantirJavaFormat() + } +} + +java { + withSourcesJar() + withJavadocJar() +} + +test { + useJUnitPlatform() + testLogging { + showStandardStreams = true + } +} +publishing { + publications { + maven(MavenPublication) { + groupId = 'com.fern' + artifactId = 'mixed-file-directory' + version = '0.0.1' + from components.java + pom { + scm { + connection = 'scm:git:git://github.com/mixed-file-directory/fern.git' + developerConnection = 'scm:git:git://github.com/mixed-file-directory/fern.git' + url = 'https://github.com/mixed-file-directory/fern' + } + } + } + } + repositories { + maven { + url "$System.env.MAVEN_PUBLISH_REGISTRY_URL" + credentials { + username "$System.env.MAVEN_USERNAME" + password "$System.env.MAVEN_PASSWORD" + } + } + } +} + diff --git a/seed/java-sdk/mixed-file-directory/sample-app/build.gradle b/seed/java-sdk/mixed-file-directory/sample-app/build.gradle new file mode 100644 index 00000000000..4ee8f227b7a --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/sample-app/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java-library' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + implementation rootProject +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + diff --git a/seed/java-sdk/mixed-file-directory/sample-app/src/main/java/sample/App.java b/seed/java-sdk/mixed-file-directory/sample-app/src/main/java/sample/App.java new file mode 100644 index 00000000000..3b19eafd041 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/sample-app/src/main/java/sample/App.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package sample; + +import java.lang.String; + +public final class App { + public static void main(String[] args) { + // import com.seed.mixedFileDirectory.SeedMixedFileDirectoryClient + } +} diff --git a/seed/java-sdk/mixed-file-directory/settings.gradle b/seed/java-sdk/mixed-file-directory/settings.gradle new file mode 100644 index 00000000000..aed36fec10b --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/settings.gradle @@ -0,0 +1 @@ +include 'sample-app' \ No newline at end of file diff --git a/seed/java-sdk/mixed-file-directory/snippet-templates.json b/seed/java-sdk/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-sdk/mixed-file-directory/snippet.json b/seed/java-sdk/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/SeedMixedFileDirectoryClient.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/SeedMixedFileDirectoryClient.java new file mode 100644 index 00000000000..8481cd46de4 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/SeedMixedFileDirectoryClient.java @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory; + +import com.seed.mixedFileDirectory.core.ClientOptions; +import com.seed.mixedFileDirectory.core.Suppliers; +import com.seed.mixedFileDirectory.resources.organization.OrganizationClient; +import com.seed.mixedFileDirectory.resources.user.UserClient; +import java.util.function.Supplier; + +public class SeedMixedFileDirectoryClient { + protected final ClientOptions clientOptions; + + protected final Supplier organizationClient; + + protected final Supplier userClient; + + public SeedMixedFileDirectoryClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.organizationClient = Suppliers.memoize(() -> new OrganizationClient(clientOptions)); + this.userClient = Suppliers.memoize(() -> new UserClient(clientOptions)); + } + + public OrganizationClient organization() { + return this.organizationClient.get(); + } + + public UserClient user() { + return this.userClient.get(); + } + + public static SeedMixedFileDirectoryClientBuilder builder() { + return new SeedMixedFileDirectoryClientBuilder(); + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/SeedMixedFileDirectoryClientBuilder.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/SeedMixedFileDirectoryClientBuilder.java new file mode 100644 index 00000000000..6fecaeb8035 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/SeedMixedFileDirectoryClientBuilder.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory; + +import com.seed.mixedFileDirectory.core.ClientOptions; +import com.seed.mixedFileDirectory.core.Environment; + +public final class SeedMixedFileDirectoryClientBuilder { + private ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); + + private Environment environment; + + public SeedMixedFileDirectoryClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + public SeedMixedFileDirectoryClient build() { + clientOptionsBuilder.environment(this.environment); + return new SeedMixedFileDirectoryClient(clientOptionsBuilder.build()); + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ClientOptions.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ClientOptions.java new file mode 100644 index 00000000000..8bb8d7b7662 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ClientOptions.java @@ -0,0 +1,103 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import okhttp3.OkHttpClient; + +public final class ClientOptions { + private final Environment environment; + + private final Map headers; + + private final Map> headerSuppliers; + + private final OkHttpClient httpClient; + + private ClientOptions( + Environment environment, + Map headers, + Map> headerSuppliers, + OkHttpClient httpClient) { + this.environment = environment; + this.headers = new HashMap<>(); + this.headers.putAll(headers); + this.headers.putAll(new HashMap() { + { + put("X-Fern-Language", "JAVA"); + } + }); + this.headerSuppliers = headerSuppliers; + this.httpClient = httpClient; + } + + public Environment environment() { + return this.environment; + } + + public Map headers(RequestOptions requestOptions) { + Map values = new HashMap<>(this.headers); + headerSuppliers.forEach((key, supplier) -> { + values.put(key, supplier.get()); + }); + if (requestOptions != null) { + values.putAll(requestOptions.getHeaders()); + } + return values; + } + + public OkHttpClient httpClient() { + return this.httpClient; + } + + public OkHttpClient httpClientWithTimeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.httpClient; + } + return this.httpClient + .newBuilder() + .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit()) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Environment environment; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + public Builder environment(Environment environment) { + this.environment = environment; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + public ClientOptions build() { + OkHttpClient okhttpClient = new OkHttpClient.Builder() + .addInterceptor(new RetryInterceptor(3)) + .build(); + return new ClientOptions(environment, headers, headerSuppliers, okhttpClient); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/DateTimeDeserializer.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/DateTimeDeserializer.java new file mode 100644 index 00000000000..934f58b3689 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/DateTimeDeserializer.java @@ -0,0 +1,55 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; + +/** + * Custom deserializer that handles converting ISO8601 dates into {@link OffsetDateTime} objects. + */ +class DateTimeDeserializer extends JsonDeserializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer()); + } + + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken token = parser.currentToken(); + if (token == JsonToken.VALUE_NUMBER_INT) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); + } else { + TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( + parser.getValueAsString(), OffsetDateTime::from, LocalDateTime::from); + + if (temporal.query(TemporalQueries.offset()) == null) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return OffsetDateTime.from(temporal); + } + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Environment.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Environment.java new file mode 100644 index 00000000000..519360354bd --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Environment.java @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +public final class Environment { + private final String url; + + private Environment(String url) { + this.url = url; + } + + public String getUrl() { + return this.url; + } + + public static Environment custom(String url) { + return new Environment(url); + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/MediaTypes.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/MediaTypes.java new file mode 100644 index 00000000000..961074e8283 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/MediaTypes.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import okhttp3.MediaType; + +public final class MediaTypes { + + public static final MediaType APPLICATION_JSON = MediaType.parse("application/json"); + + private MediaTypes() {} +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ObjectMappers.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ObjectMappers.java new file mode 100644 index 00000000000..b68f964d702 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ObjectMappers.java @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; + +public final class ObjectMappers { + public static final ObjectMapper JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(DateTimeDeserializer.getModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + private ObjectMappers() {} + + public static String stringify(Object o) { + try { + return JSON_MAPPER + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } catch (IOException e) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/RequestOptions.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/RequestOptions.java new file mode 100644 index 00000000000..eb6199fe585 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/RequestOptions.java @@ -0,0 +1,58 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +public final class RequestOptions { + private final Optional timeout; + + private final TimeUnit timeoutTimeUnit; + + private RequestOptions(Optional timeout, TimeUnit timeoutTimeUnit) { + this.timeout = timeout; + this.timeoutTimeUnit = timeoutTimeUnit; + } + + public Optional getTimeout() { + return timeout; + } + + public TimeUnit getTimeoutTimeUnit() { + return timeoutTimeUnit; + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + return headers; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Optional timeout = Optional.empty(); + + private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS; + + public Builder timeout(Integer timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) { + this.timeout = Optional.of(timeout); + this.timeoutTimeUnit = timeoutTimeUnit; + return this; + } + + public RequestOptions build() { + return new RequestOptions(timeout, timeoutTimeUnit); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ResponseBodyInputStream.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ResponseBodyInputStream.java new file mode 100644 index 00000000000..acc3f45372b --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ResponseBodyInputStream.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import java.io.FilterInputStream; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom InputStream that wraps the InputStream from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the stream is closed. + * + * This class extends FilterInputStream and takes an OkHttp Response object as a parameter. + * It retrieves the InputStream from the Response and overrides the close method to close + * both the InputStream and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyInputStream extends FilterInputStream { + private final Response response; + + /** + * Constructs a ResponseBodyInputStream that wraps the InputStream from the given OkHttp + * Response object. + * + * @param response the OkHttp Response object from which the InputStream is retrieved + * @throws IOException if an I/O error occurs while retrieving the InputStream + */ + public ResponseBodyInputStream(Response response) throws IOException { + super(response.body().byteStream()); + this.response = response; + } + + /** + * Closes the InputStream and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the stream is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the stream is closed + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ResponseBodyReader.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ResponseBodyReader.java new file mode 100644 index 00000000000..51e91481f20 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/ResponseBodyReader.java @@ -0,0 +1,44 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import java.io.FilterReader; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom Reader that wraps the Reader from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the reader is closed. + * + * This class extends FilterReader and takes an OkHttp Response object as a parameter. + * It retrieves the Reader from the Response and overrides the close method to close + * both the Reader and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyReader extends FilterReader { + private final Response response; + + /** + * Constructs a ResponseBodyReader that wraps the Reader from the given OkHttp Response object. + * + * @param response the OkHttp Response object from which the Reader is retrieved + * @throws IOException if an I/O error occurs while retrieving the Reader + */ + public ResponseBodyReader(Response response) throws IOException { + super(response.body().charStream()); + this.response = response; + } + + /** + * Closes the Reader and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the reader is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the reader is closed + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/RetryInterceptor.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/RetryInterceptor.java new file mode 100644 index 00000000000..2286696e5ca --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/RetryInterceptor.java @@ -0,0 +1,78 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; +import java.util.Random; +import okhttp3.Interceptor; +import okhttp3.Response; + +public class RetryInterceptor implements Interceptor { + + private static final Duration ONE_SECOND = Duration.ofSeconds(1); + private final ExponentialBackoff backoff; + private final Random random = new Random(); + + public RetryInterceptor(int maxRetries) { + this.backoff = new ExponentialBackoff(maxRetries); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + + if (shouldRetry(response.code())) { + return retryChain(response, chain); + } + + return response; + } + + private Response retryChain(Response response, Chain chain) throws IOException { + Optional nextBackoff = this.backoff.nextBackoff(); + while (nextBackoff.isPresent()) { + try { + Thread.sleep(nextBackoff.get().toMillis()); + } catch (InterruptedException e) { + throw new IOException("Interrupted while trying request", e); + } + response.close(); + response = chain.proceed(chain.request()); + if (shouldRetry(response.code())) { + nextBackoff = this.backoff.nextBackoff(); + } else { + return response; + } + } + + return response; + } + + private static boolean shouldRetry(int statusCode) { + return statusCode == 408 || statusCode == 409 || statusCode == 429 || statusCode >= 500; + } + + private final class ExponentialBackoff { + + private final int maxNumRetries; + + private int retryNumber = 0; + + ExponentialBackoff(int maxNumRetries) { + this.maxNumRetries = maxNumRetries; + } + + public Optional nextBackoff() { + retryNumber += 1; + if (retryNumber > maxNumRetries) { + return Optional.empty(); + } + + int upperBound = (int) Math.pow(2, retryNumber); + return Optional.of(ONE_SECOND.multipliedBy(random.nextInt(upperBound))); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/SeedMixedFileDirectoryApiException.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/SeedMixedFileDirectoryApiException.java new file mode 100644 index 00000000000..2d2e5fae661 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/SeedMixedFileDirectoryApiException.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +/** + * This exception type will be thrown for any non-2XX API responses. + */ +public class SeedMixedFileDirectoryApiException extends SeedMixedFileDirectoryException { + /** + * The error code of the response that triggered the exception. + */ + private final int statusCode; + + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public SeedMixedFileDirectoryApiException(String message, int statusCode, Object body) { + super(message); + this.statusCode = statusCode; + this.body = body; + } + + /** + * @return the statusCode + */ + public int statusCode() { + return this.statusCode; + } + + /** + * @return the body + */ + public Object body() { + return this.body; + } + + @java.lang.Override + public String toString() { + return "SeedMixedFileDirectoryApiException{" + "message: " + getMessage() + ", statusCode: " + statusCode + + ", body: " + body + "}"; + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/SeedMixedFileDirectoryException.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/SeedMixedFileDirectoryException.java new file mode 100644 index 00000000000..564b0096fd1 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/SeedMixedFileDirectoryException.java @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +/** + * This class serves as the base exception for all errors in the SDK. + */ +public class SeedMixedFileDirectoryException extends RuntimeException { + public SeedMixedFileDirectoryException(String message) { + super(message); + } + + public SeedMixedFileDirectoryException(String message, Exception e) { + super(message, e); + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Stream.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Stream.java new file mode 100644 index 00000000000..c8186b96d1d --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Stream.java @@ -0,0 +1,97 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import java.io.Reader; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Scanner; + +/** + * The {@code Stream} class implmenets {@link Iterable} to provide a simple mechanism for reading and parsing + * objects of a given type from data streamed via a {@link Reader} using a specified delimiter. + *

+ * {@code Stream} assumes that data is being pushed to the provided {@link Reader} asynchronously and utilizes a + * {@code Scanner} to block during iteration if the next object is not available. + * + * @param The type of objects in the stream. + */ +public final class Stream implements Iterable { + /** + * The {@link Class} of the objects in the stream. + */ + private final Class valueType; + /** + * The {@link Scanner} used for reading from the input stream and blocking when neede during iteration. + */ + private final Scanner scanner; + + /** + * Constructs a new {@code Stream} with the specified value type, reader, and delimiter. + * + * @param valueType The class of the objects in the stream. + * @param reader The reader that provides the streamed data. + * @param delimiter The delimiter used to separate elements in the stream. + */ + public Stream(Class valueType, Reader reader, String delimiter) { + this.scanner = new Scanner(reader).useDelimiter(delimiter); + this.valueType = valueType; + } + + /** + * Returns an iterator over the elements in this stream that blocks during iteration when the next object is + * not yet available. + * + * @return An iterator that can be used to traverse the elements in the stream. + */ + @Override + public Iterator iterator() { + return new Iterator() { + /** + * Returns {@code true} if there are more elements in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + @Override + public boolean hasNext() { + return scanner.hasNext(); + } + + /** + * Returns the next element in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return The next element in the stream. + * @throws NoSuchElementException If there are no more elements in the stream. + */ + @Override + public T next() { + if (!scanner.hasNext()) { + throw new NoSuchElementException(); + } else { + try { + T parsedResponse = ObjectMappers.JSON_MAPPER.readValue( + scanner.next().trim(), valueType); + return parsedResponse; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Removing elements from {@code Stream} is not supported. + * + * @throws UnsupportedOperationException Always, as removal is not supported. + */ + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Suppliers.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Suppliers.java new file mode 100644 index 00000000000..fbb2af369fd --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/core/Suppliers.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.core; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public final class Suppliers { + private Suppliers() {} + + public static Supplier memoize(Supplier delegate) { + AtomicReference value = new AtomicReference<>(); + return () -> { + T val = value.get(); + if (val == null) { + val = value.updateAndGet(cur -> cur == null ? Objects.requireNonNull(delegate.get()) : cur); + } + return val; + }; + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/OrganizationClient.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/OrganizationClient.java new file mode 100644 index 00000000000..405eeb5b121 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/OrganizationClient.java @@ -0,0 +1,77 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.organization; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.mixedFileDirectory.core.ClientOptions; +import com.seed.mixedFileDirectory.core.MediaTypes; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import com.seed.mixedFileDirectory.core.RequestOptions; +import com.seed.mixedFileDirectory.core.SeedMixedFileDirectoryApiException; +import com.seed.mixedFileDirectory.core.SeedMixedFileDirectoryException; +import com.seed.mixedFileDirectory.resources.organization.types.CreateOrganizationRequest; +import com.seed.mixedFileDirectory.resources.organization.types.Organization; +import java.io.IOException; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class OrganizationClient { + protected final ClientOptions clientOptions; + + public OrganizationClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * Create a new organization. + */ + public Organization create(CreateOrganizationRequest request) { + return create(request, null); + } + + /** + * Create a new organization. + */ + public Organization create(CreateOrganizationRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("organizations") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new SeedMixedFileDirectoryException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), Organization.class); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new SeedMixedFileDirectoryApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class)); + } catch (IOException e) { + throw new SeedMixedFileDirectoryException("Network error executing HTTP request", e); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/types/CreateOrganizationRequest.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/types/CreateOrganizationRequest.java new file mode 100644 index 00000000000..cd894445795 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/types/CreateOrganizationRequest.java @@ -0,0 +1,102 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.organization.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = CreateOrganizationRequest.Builder.class) +public final class CreateOrganizationRequest { + private final String name; + + private final Map additionalProperties; + + private CreateOrganizationRequest(String name, Map additionalProperties) { + this.name = name; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof CreateOrganizationRequest && equalTo((CreateOrganizationRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(CreateOrganizationRequest other) { + return name.equals(other.name); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.name); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static NameStage builder() { + return new Builder(); + } + + public interface NameStage { + _FinalStage name(@NotNull String name); + + Builder from(CreateOrganizationRequest other); + } + + public interface _FinalStage { + CreateOrganizationRequest build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements NameStage, _FinalStage { + private String name; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(CreateOrganizationRequest other) { + name(other.getName()); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public CreateOrganizationRequest build() { + return new CreateOrganizationRequest(name, additionalProperties); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/types/Organization.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/types/Organization.java new file mode 100644 index 00000000000..a791aad0774 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/organization/types/Organization.java @@ -0,0 +1,165 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.organization.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import com.seed.mixedFileDirectory.resources.user.types.User; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = Organization.Builder.class) +public final class Organization { + private final String id; + + private final String name; + + private final List users; + + private final Map additionalProperties; + + private Organization(String id, String name, List users, Map additionalProperties) { + this.id = id; + this.name = name; + this.users = users; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("users") + public List getUsers() { + return users; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Organization && equalTo((Organization) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(Organization other) { + return id.equals(other.id) && name.equals(other.name) && users.equals(other.users); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.users); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(@NotNull String id); + + Builder from(Organization other); + } + + public interface NameStage { + _FinalStage name(@NotNull String name); + } + + public interface _FinalStage { + Organization build(); + + _FinalStage users(List users); + + _FinalStage addUsers(User users); + + _FinalStage addAllUsers(List users); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, NameStage, _FinalStage { + private String id; + + private String name; + + private List users = new ArrayList<>(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(Organization other) { + id(other.getId()); + name(other.getName()); + users(other.getUsers()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(@NotNull String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public _FinalStage addAllUsers(List users) { + this.users.addAll(users); + return this; + } + + @java.lang.Override + public _FinalStage addUsers(User users) { + this.users.add(users); + return this; + } + + @java.lang.Override + @JsonSetter(value = "users", nulls = Nulls.SKIP) + public _FinalStage users(List users) { + this.users.clear(); + this.users.addAll(users); + return this; + } + + @java.lang.Override + public Organization build() { + return new Organization(id, name, users, additionalProperties); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/UserClient.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/UserClient.java new file mode 100644 index 00000000000..a6a7ccb0782 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/UserClient.java @@ -0,0 +1,88 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.seed.mixedFileDirectory.core.ClientOptions; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import com.seed.mixedFileDirectory.core.RequestOptions; +import com.seed.mixedFileDirectory.core.SeedMixedFileDirectoryApiException; +import com.seed.mixedFileDirectory.core.SeedMixedFileDirectoryException; +import com.seed.mixedFileDirectory.core.Suppliers; +import com.seed.mixedFileDirectory.resources.user.events.EventsClient; +import com.seed.mixedFileDirectory.resources.user.requests.ListUsersRequest; +import com.seed.mixedFileDirectory.resources.user.types.User; +import java.io.IOException; +import java.util.List; +import java.util.function.Supplier; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class UserClient { + protected final ClientOptions clientOptions; + + protected final Supplier eventsClient; + + public UserClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.eventsClient = Suppliers.memoize(() -> new EventsClient(clientOptions)); + } + + /** + * List all users. + */ + public List list() { + return list(ListUsersRequest.builder().build()); + } + + /** + * List all users. + */ + public List list(ListUsersRequest request) { + return list(request, null); + } + + /** + * List all users. + */ + public List list(ListUsersRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("users"); + if (request.getLimit().isPresent()) { + httpUrl.addQueryParameter("limit", request.getLimit().get().toString()); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), new TypeReference>() {}); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new SeedMixedFileDirectoryApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class)); + } catch (IOException e) { + throw new SeedMixedFileDirectoryException("Network error executing HTTP request", e); + } + } + + public EventsClient events() { + return this.eventsClient.get(); + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/EventsClient.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/EventsClient.java new file mode 100644 index 00000000000..c2aed7c2c58 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/EventsClient.java @@ -0,0 +1,88 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user.events; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.seed.mixedFileDirectory.core.ClientOptions; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import com.seed.mixedFileDirectory.core.RequestOptions; +import com.seed.mixedFileDirectory.core.SeedMixedFileDirectoryApiException; +import com.seed.mixedFileDirectory.core.SeedMixedFileDirectoryException; +import com.seed.mixedFileDirectory.core.Suppliers; +import com.seed.mixedFileDirectory.resources.user.events.metadata.MetadataClient; +import com.seed.mixedFileDirectory.resources.user.events.requests.ListUserEventsRequest; +import com.seed.mixedFileDirectory.resources.user.events.types.Event; +import java.io.IOException; +import java.util.List; +import java.util.function.Supplier; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class EventsClient { + protected final ClientOptions clientOptions; + + protected final Supplier metadataClient; + + public EventsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.metadataClient = Suppliers.memoize(() -> new MetadataClient(clientOptions)); + } + + /** + * List all user events. + */ + public List listEvents() { + return listEvents(ListUserEventsRequest.builder().build()); + } + + /** + * List all user events. + */ + public List listEvents(ListUserEventsRequest request) { + return listEvents(request, null); + } + + /** + * List all user events. + */ + public List listEvents(ListUserEventsRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("users/events"); + if (request.getLimit().isPresent()) { + httpUrl.addQueryParameter("limit", request.getLimit().get().toString()); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), new TypeReference>() {}); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new SeedMixedFileDirectoryApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class)); + } catch (IOException e) { + throw new SeedMixedFileDirectoryException("Network error executing HTTP request", e); + } + } + + public MetadataClient metadata() { + return this.metadataClient.get(); + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/MetadataClient.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/MetadataClient.java new file mode 100644 index 00000000000..d53800a6494 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/MetadataClient.java @@ -0,0 +1,67 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user.events.metadata; + +import com.seed.mixedFileDirectory.core.ClientOptions; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import com.seed.mixedFileDirectory.core.RequestOptions; +import com.seed.mixedFileDirectory.core.SeedMixedFileDirectoryApiException; +import com.seed.mixedFileDirectory.core.SeedMixedFileDirectoryException; +import com.seed.mixedFileDirectory.resources.user.events.metadata.requests.GetEventMetadataRequest; +import com.seed.mixedFileDirectory.resources.user.events.metadata.types.Metadata; +import java.io.IOException; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class MetadataClient { + protected final ClientOptions clientOptions; + + public MetadataClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * Get event metadata. + */ + public Metadata getMetadata(GetEventMetadataRequest request) { + return getMetadata(request, null); + } + + /** + * Get event metadata. + */ + public Metadata getMetadata(GetEventMetadataRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("users/events/metadata"); + httpUrl.addQueryParameter("id", request.getId()); + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), Metadata.class); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new SeedMixedFileDirectoryApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class)); + } catch (IOException e) { + throw new SeedMixedFileDirectoryException("Network error executing HTTP request", e); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/requests/GetEventMetadataRequest.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/requests/GetEventMetadataRequest.java new file mode 100644 index 00000000000..244224a8b30 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/requests/GetEventMetadataRequest.java @@ -0,0 +1,102 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user.events.metadata.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = GetEventMetadataRequest.Builder.class) +public final class GetEventMetadataRequest { + private final String id; + + private final Map additionalProperties; + + private GetEventMetadataRequest(String id, Map additionalProperties) { + this.id = id; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof GetEventMetadataRequest && equalTo((GetEventMetadataRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(GetEventMetadataRequest other) { + return id.equals(other.id); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + _FinalStage id(@NotNull String id); + + Builder from(GetEventMetadataRequest other); + } + + public interface _FinalStage { + GetEventMetadataRequest build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, _FinalStage { + private String id; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(GetEventMetadataRequest other) { + id(other.getId()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public _FinalStage id(@NotNull String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + public GetEventMetadataRequest build() { + return new GetEventMetadataRequest(id, additionalProperties); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/types/Metadata.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/types/Metadata.java new file mode 100644 index 00000000000..7ecd332026a --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/metadata/types/Metadata.java @@ -0,0 +1,124 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user.events.metadata.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = Metadata.Builder.class) +public final class Metadata { + private final String id; + + private final Object value; + + private final Map additionalProperties; + + private Metadata(String id, Object value, Map additionalProperties) { + this.id = id; + this.value = value; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("value") + public Object getValue() { + return value; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Metadata && equalTo((Metadata) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(Metadata other) { + return id.equals(other.id) && value.equals(other.value); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.value); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + ValueStage id(@NotNull String id); + + Builder from(Metadata other); + } + + public interface ValueStage { + _FinalStage value(@NotNull Object value); + } + + public interface _FinalStage { + Metadata build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, ValueStage, _FinalStage { + private String id; + + private Object value; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(Metadata other) { + id(other.getId()); + value(other.getValue()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public ValueStage id(@NotNull String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("value") + public _FinalStage value(@NotNull Object value) { + this.value = Objects.requireNonNull(value, "value must not be null"); + return this; + } + + @java.lang.Override + public Metadata build() { + return new Metadata(id, value, additionalProperties); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/requests/ListUserEventsRequest.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/requests/ListUserEventsRequest.java new file mode 100644 index 00000000000..d6ae161a542 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/requests/ListUserEventsRequest.java @@ -0,0 +1,98 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user.events.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = ListUserEventsRequest.Builder.class) +public final class ListUserEventsRequest { + private final Optional limit; + + private final Map additionalProperties; + + private ListUserEventsRequest(Optional limit, Map additionalProperties) { + this.limit = limit; + this.additionalProperties = additionalProperties; + } + + /** + * @return The maximum number of results to return. + */ + @JsonProperty("limit") + public Optional getLimit() { + return limit; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof ListUserEventsRequest && equalTo((ListUserEventsRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(ListUserEventsRequest other) { + return limit.equals(other.limit); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.limit); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional limit = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(ListUserEventsRequest other) { + limit(other.getLimit()); + return this; + } + + @JsonSetter(value = "limit", nulls = Nulls.SKIP) + public Builder limit(Optional limit) { + this.limit = limit; + return this; + } + + public Builder limit(Integer limit) { + this.limit = Optional.ofNullable(limit); + return this; + } + + public ListUserEventsRequest build() { + return new ListUserEventsRequest(limit, additionalProperties); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/types/Event.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/types/Event.java new file mode 100644 index 00000000000..a04f1f04b0c --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/events/types/Event.java @@ -0,0 +1,124 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user.events.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = Event.Builder.class) +public final class Event { + private final String id; + + private final String name; + + private final Map additionalProperties; + + private Event(String id, String name, Map additionalProperties) { + this.id = id; + this.name = name; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Event && equalTo((Event) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(Event other) { + return id.equals(other.id) && name.equals(other.name); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(@NotNull String id); + + Builder from(Event other); + } + + public interface NameStage { + _FinalStage name(@NotNull String name); + } + + public interface _FinalStage { + Event build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, NameStage, _FinalStage { + private String id; + + private String name; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(Event other) { + id(other.getId()); + name(other.getName()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(@NotNull String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public Event build() { + return new Event(id, name, additionalProperties); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/requests/ListUsersRequest.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/requests/ListUsersRequest.java new file mode 100644 index 00000000000..5d1ce2b057c --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/requests/ListUsersRequest.java @@ -0,0 +1,98 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = ListUsersRequest.Builder.class) +public final class ListUsersRequest { + private final Optional limit; + + private final Map additionalProperties; + + private ListUsersRequest(Optional limit, Map additionalProperties) { + this.limit = limit; + this.additionalProperties = additionalProperties; + } + + /** + * @return The maximum number of results to return. + */ + @JsonProperty("limit") + public Optional getLimit() { + return limit; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof ListUsersRequest && equalTo((ListUsersRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(ListUsersRequest other) { + return limit.equals(other.limit); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.limit); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional limit = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(ListUsersRequest other) { + limit(other.getLimit()); + return this; + } + + @JsonSetter(value = "limit", nulls = Nulls.SKIP) + public Builder limit(Optional limit) { + this.limit = limit; + return this; + } + + public Builder limit(Integer limit) { + this.limit = Optional.ofNullable(limit); + return this; + } + + public ListUsersRequest build() { + return new ListUsersRequest(limit, additionalProperties); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/types/User.java b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/types/User.java new file mode 100644 index 00000000000..a607db1dbaa --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/main/java/com/seed/mixedFileDirectory/resources/user/types/User.java @@ -0,0 +1,146 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory.resources.user.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.mixedFileDirectory.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = User.Builder.class) +public final class User { + private final String id; + + private final String name; + + private final int age; + + private final Map additionalProperties; + + private User(String id, String name, int age, Map additionalProperties) { + this.id = id; + this.name = name; + this.age = age; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("age") + public int getAge() { + return age; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof User && equalTo((User) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(User other) { + return id.equals(other.id) && name.equals(other.name) && age == other.age; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.age); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(@NotNull String id); + + Builder from(User other); + } + + public interface NameStage { + AgeStage name(@NotNull String name); + } + + public interface AgeStage { + _FinalStage age(int age); + } + + public interface _FinalStage { + User build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements IdStage, NameStage, AgeStage, _FinalStage { + private String id; + + private String name; + + private int age; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(User other) { + id(other.getId()); + name(other.getName()); + age(other.getAge()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(@NotNull String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public AgeStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("age") + public _FinalStage age(int age) { + this.age = age; + return this; + } + + @java.lang.Override + public User build() { + return new User(id, name, age, additionalProperties); + } + } +} diff --git a/seed/java-sdk/mixed-file-directory/src/test/java/com/seed/mixedFileDirectory/TestClient.java b/seed/java-sdk/mixed-file-directory/src/test/java/com/seed/mixedFileDirectory/TestClient.java new file mode 100644 index 00000000000..55c9433f961 --- /dev/null +++ b/seed/java-sdk/mixed-file-directory/src/test/java/com/seed/mixedFileDirectory/TestClient.java @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.mixedFileDirectory; + +public final class TestClient { + public void test() { + // Add tests here and mark this file in .fernignore + assert true; + } +} diff --git a/seed/java-spring/mixed-file-directory/.mock/definition/__package__.yml b/seed/java-spring/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/java-spring/mixed-file-directory/.mock/definition/api.yml b/seed/java-spring/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/java-spring/mixed-file-directory/.mock/definition/organization.yml b/seed/java-spring/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/java-spring/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/java-spring/mixed-file-directory/.mock/definition/user.yml b/seed/java-spring/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/java-spring/mixed-file-directory/.mock/definition/user/events.yml b/seed/java-spring/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/java-spring/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/java-spring/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/java-spring/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/java-spring/mixed-file-directory/.mock/fern.config.json b/seed/java-spring/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/java-spring/mixed-file-directory/.mock/generators.yml b/seed/java-spring/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/java-spring/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/java-spring/mixed-file-directory/core/APIException.java b/seed/java-spring/mixed-file-directory/core/APIException.java new file mode 100644 index 00000000000..27289cf9b2e --- /dev/null +++ b/seed/java-spring/mixed-file-directory/core/APIException.java @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package core; + +import java.lang.Exception; + +public class APIException extends Exception { +} diff --git a/seed/java-spring/mixed-file-directory/core/DateTimeDeserializer.java b/seed/java-spring/mixed-file-directory/core/DateTimeDeserializer.java new file mode 100644 index 00000000000..3d3174aec00 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/core/DateTimeDeserializer.java @@ -0,0 +1,56 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; + +/** + * Custom deserializer that handles converting ISO8601 dates into {@link OffsetDateTime} objects. + */ +class DateTimeDeserializer extends JsonDeserializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer()); + } + + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken token = parser.currentToken(); + if (token == JsonToken.VALUE_NUMBER_INT) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); + } else { + TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( + parser.getValueAsString(), OffsetDateTime::from, LocalDateTime::from); + + if (temporal.query(TemporalQueries.offset()) == null) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return OffsetDateTime.from(temporal); + } + } + } +} \ No newline at end of file diff --git a/seed/java-spring/mixed-file-directory/core/ObjectMappers.java b/seed/java-spring/mixed-file-directory/core/ObjectMappers.java new file mode 100644 index 00000000000..e02822614a8 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/core/ObjectMappers.java @@ -0,0 +1,41 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; +import java.lang.Integer; +import java.lang.Object; +import java.lang.String; + +public final class ObjectMappers { + public static final ObjectMapper JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(DateTimeDeserializer.getModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + private ObjectMappers() { + } + + public static String stringify(Object o) { + try { + return JSON_MAPPER.setSerializationInclusion(JsonInclude.Include.ALWAYS) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } + catch (IOException e) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); + } + } + } diff --git a/seed/java-spring/mixed-file-directory/resources/organization/OrganizationService.java b/seed/java-spring/mixed-file-directory/resources/organization/OrganizationService.java new file mode 100644 index 00000000000..8f2310ee217 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/organization/OrganizationService.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.organization; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import resources.organization.types.CreateOrganizationRequest; +import resources.organization.types.Organization; + +@RequestMapping( + path = "/organizations" +) +public interface OrganizationService { + @PostMapping( + value = "/", + produces = "application/json", + consumes = "application/json" + ) + Organization create(@RequestBody CreateOrganizationRequest body); +} diff --git a/seed/java-spring/mixed-file-directory/resources/organization/types/CreateOrganizationRequest.java b/seed/java-spring/mixed-file-directory/resources/organization/types/CreateOrganizationRequest.java new file mode 100644 index 00000000000..d3b3c86e4ac --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/organization/types/CreateOrganizationRequest.java @@ -0,0 +1,95 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.organization.types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import core.ObjectMappers; +import java.lang.Object; +import java.lang.String; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize( + builder = CreateOrganizationRequest.Builder.class +) +public final class CreateOrganizationRequest { + private final String name; + + private CreateOrganizationRequest(String name) { + this.name = name; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof CreateOrganizationRequest && equalTo((CreateOrganizationRequest) other); + } + + private boolean equalTo(CreateOrganizationRequest other) { + return name.equals(other.name); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.name); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static NameStage builder() { + return new Builder(); + } + + public interface NameStage { + _FinalStage name(@NotNull String name); + + Builder from(CreateOrganizationRequest other); + } + + public interface _FinalStage { + CreateOrganizationRequest build(); + } + + @JsonIgnoreProperties( + ignoreUnknown = true + ) + public static final class Builder implements NameStage, _FinalStage { + private String name; + + private Builder() { + } + + @java.lang.Override + public Builder from(CreateOrganizationRequest other) { + name(other.getName()); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public CreateOrganizationRequest build() { + return new CreateOrganizationRequest(name); + } + } +} diff --git a/seed/java-spring/mixed-file-directory/resources/organization/types/Organization.java b/seed/java-spring/mixed-file-directory/resources/organization/types/Organization.java new file mode 100644 index 00000000000..b19774fb84f --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/organization/types/Organization.java @@ -0,0 +1,162 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.organization.types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import core.ObjectMappers; +import java.lang.Object; +import java.lang.String; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import resources.user.types.User; +import types.Id; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize( + builder = Organization.Builder.class +) +public final class Organization { + private final Id id; + + private final String name; + + private final List users; + + private Organization(Id id, String name, List users) { + this.id = id; + this.name = name; + this.users = users; + } + + @JsonProperty("id") + public Id getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("users") + public List getUsers() { + return users; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Organization && equalTo((Organization) other); + } + + private boolean equalTo(Organization other) { + return id.equals(other.id) && name.equals(other.name) && users.equals(other.users); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.users); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(@NotNull Id id); + + Builder from(Organization other); + } + + public interface NameStage { + _FinalStage name(@NotNull String name); + } + + public interface _FinalStage { + Organization build(); + + _FinalStage users(List users); + + _FinalStage addUsers(User users); + + _FinalStage addAllUsers(List users); + } + + @JsonIgnoreProperties( + ignoreUnknown = true + ) + public static final class Builder implements IdStage, NameStage, _FinalStage { + private Id id; + + private String name; + + private List users = new ArrayList<>(); + + private Builder() { + } + + @java.lang.Override + public Builder from(Organization other) { + id(other.getId()); + name(other.getName()); + users(other.getUsers()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(@NotNull Id id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public _FinalStage addAllUsers(List users) { + this.users.addAll(users); + return this; + } + + @java.lang.Override + public _FinalStage addUsers(User users) { + this.users.add(users); + return this; + } + + @java.lang.Override + @JsonSetter( + value = "users", + nulls = Nulls.SKIP + ) + public _FinalStage users(List users) { + this.users.clear(); + this.users.addAll(users); + return this; + } + + @java.lang.Override + public Organization build() { + return new Organization(id, name, users); + } + } +} diff --git a/seed/java-spring/mixed-file-directory/resources/user/UserService.java b/seed/java-spring/mixed-file-directory/resources/user/UserService.java new file mode 100644 index 00000000000..5e5c06c66c4 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/user/UserService.java @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.user; + +import java.lang.Integer; +import java.util.List; +import java.util.Optional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import resources.user.types.User; + +@RequestMapping( + path = "/users" +) +public interface UserService { + @GetMapping( + value = "/", + produces = "application/json" + ) + List list(@RequestParam("limit") Optional limit); +} diff --git a/seed/java-spring/mixed-file-directory/resources/user/events/EventsService.java b/seed/java-spring/mixed-file-directory/resources/user/events/EventsService.java new file mode 100644 index 00000000000..ae8d2b202ae --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/user/events/EventsService.java @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.user.events; + +import java.lang.Integer; +import java.util.List; +import java.util.Optional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import resources.user.events.types.Event; + +@RequestMapping( + path = "/users/events" +) +public interface EventsService { + @GetMapping( + value = "/", + produces = "application/json" + ) + List listEvents(@RequestParam("limit") Optional limit); +} diff --git a/seed/java-spring/mixed-file-directory/resources/user/events/metadata/MetadataService.java b/seed/java-spring/mixed-file-directory/resources/user/events/metadata/MetadataService.java new file mode 100644 index 00000000000..a05b6703396 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/user/events/metadata/MetadataService.java @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.user.events.metadata; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import resources.user.events.metadata.types.Metadata; +import types.Id; + +@RequestMapping( + path = "/users/events/metadata" +) +public interface MetadataService { + @GetMapping( + value = "/", + produces = "application/json" + ) + Metadata getMetadata(@RequestParam("id") Id id); +} diff --git a/seed/java-spring/mixed-file-directory/resources/user/events/metadata/types/Metadata.java b/seed/java-spring/mixed-file-directory/resources/user/events/metadata/types/Metadata.java new file mode 100644 index 00000000000..ebf3af31eeb --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/user/events/metadata/types/Metadata.java @@ -0,0 +1,118 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.user.events.metadata.types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import core.ObjectMappers; +import java.lang.Object; +import java.lang.String; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import types.Id; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize( + builder = Metadata.Builder.class +) +public final class Metadata { + private final Id id; + + private final Object value; + + private Metadata(Id id, Object value) { + this.id = id; + this.value = value; + } + + @JsonProperty("id") + public Id getId() { + return id; + } + + @JsonProperty("value") + public Object getValue() { + return value; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Metadata && equalTo((Metadata) other); + } + + private boolean equalTo(Metadata other) { + return id.equals(other.id) && value.equals(other.value); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.value); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + ValueStage id(@NotNull Id id); + + Builder from(Metadata other); + } + + public interface ValueStage { + _FinalStage value(@NotNull Object value); + } + + public interface _FinalStage { + Metadata build(); + } + + @JsonIgnoreProperties( + ignoreUnknown = true + ) + public static final class Builder implements IdStage, ValueStage, _FinalStage { + private Id id; + + private Object value; + + private Builder() { + } + + @java.lang.Override + public Builder from(Metadata other) { + id(other.getId()); + value(other.getValue()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public ValueStage id(@NotNull Id id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("value") + public _FinalStage value(@NotNull Object value) { + this.value = Objects.requireNonNull(value, "value must not be null"); + return this; + } + + @java.lang.Override + public Metadata build() { + return new Metadata(id, value); + } + } +} diff --git a/seed/java-spring/mixed-file-directory/resources/user/events/types/Event.java b/seed/java-spring/mixed-file-directory/resources/user/events/types/Event.java new file mode 100644 index 00000000000..3d6c99166ad --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/user/events/types/Event.java @@ -0,0 +1,118 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.user.events.types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import core.ObjectMappers; +import java.lang.Object; +import java.lang.String; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import types.Id; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize( + builder = Event.Builder.class +) +public final class Event { + private final Id id; + + private final String name; + + private Event(Id id, String name) { + this.id = id; + this.name = name; + } + + @JsonProperty("id") + public Id getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof Event && equalTo((Event) other); + } + + private boolean equalTo(Event other) { + return id.equals(other.id) && name.equals(other.name); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(@NotNull Id id); + + Builder from(Event other); + } + + public interface NameStage { + _FinalStage name(@NotNull String name); + } + + public interface _FinalStage { + Event build(); + } + + @JsonIgnoreProperties( + ignoreUnknown = true + ) + public static final class Builder implements IdStage, NameStage, _FinalStage { + private Id id; + + private String name; + + private Builder() { + } + + @java.lang.Override + public Builder from(Event other) { + id(other.getId()); + name(other.getName()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(@NotNull Id id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public _FinalStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + public Event build() { + return new Event(id, name); + } + } +} diff --git a/seed/java-spring/mixed-file-directory/resources/user/types/User.java b/seed/java-spring/mixed-file-directory/resources/user/types/User.java new file mode 100644 index 00000000000..ffb126a3008 --- /dev/null +++ b/seed/java-spring/mixed-file-directory/resources/user/types/User.java @@ -0,0 +1,140 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package resources.user.types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import core.ObjectMappers; +import java.lang.Object; +import java.lang.String; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import types.Id; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize( + builder = User.Builder.class +) +public final class User { + private final Id id; + + private final String name; + + private final int age; + + private User(Id id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } + + @JsonProperty("id") + public Id getId() { + return id; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("age") + public int getAge() { + return age; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof User && equalTo((User) other); + } + + private boolean equalTo(User other) { + return id.equals(other.id) && name.equals(other.name) && age == other.age; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.age); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static IdStage builder() { + return new Builder(); + } + + public interface IdStage { + NameStage id(@NotNull Id id); + + Builder from(User other); + } + + public interface NameStage { + AgeStage name(@NotNull String name); + } + + public interface AgeStage { + _FinalStage age(int age); + } + + public interface _FinalStage { + User build(); + } + + @JsonIgnoreProperties( + ignoreUnknown = true + ) + public static final class Builder implements IdStage, NameStage, AgeStage, _FinalStage { + private Id id; + + private String name; + + private int age; + + private Builder() { + } + + @java.lang.Override + public Builder from(User other) { + id(other.getId()); + name(other.getName()); + age(other.getAge()); + return this; + } + + @java.lang.Override + @JsonSetter("id") + public NameStage id(@NotNull Id id) { + this.id = Objects.requireNonNull(id, "id must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public AgeStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("age") + public _FinalStage age(int age) { + this.age = age; + return this; + } + + @java.lang.Override + public User build() { + return new User(id, name, age); + } + } +} diff --git a/seed/java-spring/mixed-file-directory/snippet-templates.json b/seed/java-spring/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-spring/mixed-file-directory/snippet.json b/seed/java-spring/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-spring/mixed-file-directory/types/Id.java b/seed/java-spring/mixed-file-directory/types/Id.java new file mode 100644 index 00000000000..5856a76910c --- /dev/null +++ b/seed/java-spring/mixed-file-directory/types/Id.java @@ -0,0 +1,49 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.lang.Object; +import java.lang.String; + +public final class Id { + private final String value; + + private Id(String value) { + this.value = value; + } + + @JsonValue + public String get() { + return this.value; + } + + @java.lang.Override + public boolean equals(Object other) { + return this == other || (other instanceof Id && this.value.equals(((Id) other).value)); + } + + @java.lang.Override + public int hashCode() { + return value.hashCode(); + } + + @java.lang.Override + public String toString() { + return value; + } + + @JsonCreator( + mode = JsonCreator.Mode.DELEGATING + ) + public static Id of(String value) { + return new Id(value); + } + + public static Id valueOf(String value) { + return of(value); + } +} diff --git a/seed/openapi/mixed-file-directory/.mock/definition/__package__.yml b/seed/openapi/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/openapi/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/openapi/mixed-file-directory/.mock/definition/api.yml b/seed/openapi/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/openapi/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/openapi/mixed-file-directory/.mock/definition/organization.yml b/seed/openapi/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/openapi/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/openapi/mixed-file-directory/.mock/definition/user.yml b/seed/openapi/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/openapi/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/openapi/mixed-file-directory/.mock/definition/user/events.yml b/seed/openapi/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/openapi/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/openapi/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/openapi/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/openapi/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/openapi/mixed-file-directory/.mock/fern.config.json b/seed/openapi/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/openapi/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/openapi/mixed-file-directory/.mock/generators.yml b/seed/openapi/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/openapi/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/openapi/mixed-file-directory/openapi.yml b/seed/openapi/mixed-file-directory/openapi.yml new file mode 100644 index 00000000000..4cc9f3adbb3 --- /dev/null +++ b/seed/openapi/mixed-file-directory/openapi.yml @@ -0,0 +1,155 @@ +openapi: 3.0.1 +info: + title: mixed-file-directory + version: '' +paths: + /organizations/: + post: + description: Create a new organization. + operationId: organization_create + tags: + - Organization + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOrganizationRequest' + /users/: + get: + description: List all users. + operationId: user_list + tags: + - User + parameters: + - name: limit + in: query + description: The maximum number of results to return. + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + /users/events/: + get: + description: List all user events. + operationId: user_events_listEvents + tags: + - UserEvents + parameters: + - name: limit + in: query + description: The maximum number of results to return. + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/userEvent' + /users/events/metadata/: + get: + description: Get event metadata. + operationId: user_events_metadata_getMetadata + tags: + - UserEventsMetadata + parameters: + - name: id + in: query + required: true + schema: + $ref: '#/components/schemas/Id' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/usereventsMetadata' +components: + schemas: + Id: + title: Id + type: string + Organization: + title: Organization + type: object + properties: + id: + $ref: '#/components/schemas/Id' + name: + type: string + users: + type: array + items: + $ref: '#/components/schemas/User' + required: + - id + - name + - users + CreateOrganizationRequest: + title: CreateOrganizationRequest + type: object + properties: + name: + type: string + required: + - name + User: + title: User + type: object + properties: + id: + $ref: '#/components/schemas/Id' + name: + type: string + age: + type: integer + required: + - id + - name + - age + userEvent: + title: userEvent + type: object + properties: + id: + $ref: '#/components/schemas/Id' + name: + type: string + required: + - id + - name + usereventsMetadata: + title: usereventsMetadata + type: object + properties: + id: + $ref: '#/components/schemas/Id' + value: {} + required: + - id + - value + securitySchemes: {} diff --git a/seed/openapi/mixed-file-directory/snippet-templates.json b/seed/openapi/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/openapi/mixed-file-directory/snippet.json b/seed/openapi/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/postman/mixed-file-directory/.mock/definition/__package__.yml b/seed/postman/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/postman/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/postman/mixed-file-directory/.mock/definition/api.yml b/seed/postman/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/postman/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/postman/mixed-file-directory/.mock/definition/organization.yml b/seed/postman/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/postman/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/postman/mixed-file-directory/.mock/definition/user.yml b/seed/postman/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/postman/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/postman/mixed-file-directory/.mock/definition/user/events.yml b/seed/postman/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/postman/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/postman/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/postman/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/postman/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/postman/mixed-file-directory/.mock/fern.config.json b/seed/postman/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/postman/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/postman/mixed-file-directory/.mock/generators.yml b/seed/postman/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/postman/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/postman/mixed-file-directory/collection.json b/seed/postman/mixed-file-directory/collection.json new file mode 100644 index 00000000000..39f843d1c6b --- /dev/null +++ b/seed/postman/mixed-file-directory/collection.json @@ -0,0 +1,351 @@ +{ + "info": { + "name": "Mixed File Directory", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "description": null + }, + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + } + ], + "auth": null, + "item": [ + { + "_type": "container", + "description": null, + "name": "Organization", + "item": [ + { + "_type": "endpoint", + "name": "Create", + "request": { + "description": "Create a new organization.", + "url": { + "raw": "{{baseUrl}}/organizations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "organizations" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "POST", + "auth": null, + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"string\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "response": [ + { + "name": "Success", + "status": "OK", + "code": 200, + "originalRequest": { + "description": "Create a new organization.", + "url": { + "raw": "{{baseUrl}}/organizations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "organizations" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "POST", + "auth": null, + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"string\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "description": null, + "body": "{\n \"id\": \"string\",\n \"name\": \"string\",\n \"users\": [\n {\n \"id\": \"string\",\n \"name\": \"string\",\n \"age\": 1\n }\n ]\n}", + "_postman_previewlanguage": "json" + } + ] + } + ] + }, + { + "_type": "container", + "description": null, + "name": "User", + "item": [ + { + "_type": "container", + "description": null, + "name": "Events", + "item": [ + { + "_type": "container", + "description": null, + "name": "Metadata", + "item": [ + { + "_type": "endpoint", + "name": "Get Metadata", + "request": { + "description": "Get event metadata.", + "url": { + "raw": "{{baseUrl}}/users/events/metadata?id=string", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "events", + "metadata" + ], + "query": [ + { + "key": "id", + "description": null, + "value": "string" + } + ], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "GET", + "auth": null, + "body": null + }, + "response": [ + { + "name": "Success", + "status": "OK", + "code": 200, + "originalRequest": { + "description": "Get event metadata.", + "url": { + "raw": "{{baseUrl}}/users/events/metadata?id=string", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "events", + "metadata" + ], + "query": [ + { + "key": "id", + "description": null, + "value": "string" + } + ], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "GET", + "auth": null, + "body": null + }, + "description": null, + "body": "{\n \"id\": \"string\",\n \"value\": {\n \"key\": \"value\"\n }\n}", + "_postman_previewlanguage": "json" + } + ] + } + ] + }, + { + "_type": "endpoint", + "name": "List Events", + "request": { + "description": "List all user events.", + "url": { + "raw": "{{baseUrl}}/users/events?limit=1", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "events" + ], + "query": [ + { + "key": "limit", + "description": "The maximum number of results to return.", + "value": "1" + } + ], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "GET", + "auth": null, + "body": null + }, + "response": [ + { + "name": "Success", + "status": "OK", + "code": 200, + "originalRequest": { + "description": "List all user events.", + "url": { + "raw": "{{baseUrl}}/users/events?limit=1", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "events" + ], + "query": [ + { + "key": "limit", + "description": "The maximum number of results to return.", + "value": "1" + } + ], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "GET", + "auth": null, + "body": null + }, + "description": null, + "body": "[\n {\n \"id\": \"string\",\n \"name\": \"string\"\n }\n]", + "_postman_previewlanguage": "json" + } + ] + } + ] + }, + { + "_type": "endpoint", + "name": "List", + "request": { + "description": "List all users.", + "url": { + "raw": "{{baseUrl}}/users?limit=1", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users" + ], + "query": [ + { + "key": "limit", + "description": "The maximum number of results to return.", + "value": "1" + } + ], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "GET", + "auth": null, + "body": null + }, + "response": [ + { + "name": "Success", + "status": "OK", + "code": 200, + "originalRequest": { + "description": "List all users.", + "url": { + "raw": "{{baseUrl}}/users?limit=1", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users" + ], + "query": [ + { + "key": "limit", + "description": "The maximum number of results to return.", + "value": "1" + } + ], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "GET", + "auth": null, + "body": null + }, + "description": null, + "body": "[\n {\n \"id\": \"string\",\n \"name\": \"string\",\n \"age\": 1\n }\n]", + "_postman_previewlanguage": "json" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/seed/postman/mixed-file-directory/snippet-templates.json b/seed/postman/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/postman/mixed-file-directory/snippet.json b/seed/postman/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/pydantic/mixed-file-directory/.github/workflows/ci.yml b/seed/pydantic/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..b204fa604e2 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: ci + +on: [push] +jobs: + compile: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Compile + run: poetry run mypy . + test: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + + - name: Test + run: poetry run pytest -rP . + + publish: + needs: [compile, test] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Publish to pypi + run: | + poetry config repositories.remote + poetry --no-interaction -v publish --build --repository remote --username "$" --password "$" + env: + : ${{ secrets. }} + : ${{ secrets. }} diff --git a/seed/pydantic/mixed-file-directory/.gitignore b/seed/pydantic/mixed-file-directory/.gitignore new file mode 100644 index 00000000000..0da665feeef --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.gitignore @@ -0,0 +1,5 @@ +dist/ +.mypy_cache/ +__pycache__/ +poetry.toml +.ruff_cache/ diff --git a/seed/pydantic/mixed-file-directory/.mock/definition/__package__.yml b/seed/pydantic/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/pydantic/mixed-file-directory/.mock/definition/api.yml b/seed/pydantic/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/pydantic/mixed-file-directory/.mock/definition/organization.yml b/seed/pydantic/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/pydantic/mixed-file-directory/.mock/definition/user.yml b/seed/pydantic/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/pydantic/mixed-file-directory/.mock/definition/user/events.yml b/seed/pydantic/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/pydantic/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/pydantic/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/pydantic/mixed-file-directory/.mock/fern.config.json b/seed/pydantic/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/pydantic/mixed-file-directory/.mock/generators.yml b/seed/pydantic/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/pydantic/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/pydantic/mixed-file-directory/README.md b/seed/pydantic/mixed-file-directory/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/pydantic/mixed-file-directory/pyproject.toml b/seed/pydantic/mixed-file-directory/pyproject.toml new file mode 100644 index 00000000000..b5767196b3e --- /dev/null +++ b/seed/pydantic/mixed-file-directory/pyproject.toml @@ -0,0 +1,59 @@ +[tool.poetry] +name = "fern_mixed-file-directory" +version = "0.0.1" +description = "" +readme = "README.md" +authors = [] +keywords = [] + +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed" +] +packages = [ + { include = "seed/mixed_file_directory", from = "src"} +] + +[project.urls] +Repository = 'https://github.com/mixed-file-directory/fern' + +[tool.poetry.dependencies] +python = "^3.8" +pydantic = ">= 1.9.2" +pydantic-core = "^2.18.2" + +[tool.poetry.dev-dependencies] +mypy = "1.0.1" +pytest = "^7.4.0" +pytest-asyncio = "^0.23.5" +python-dateutil = "^2.9.0" +types-python-dateutil = "^2.9.0.20240316" +ruff = "^0.5.6" + +[tool.pytest.ini_options] +testpaths = [ "tests" ] +asyncio_mode = "auto" + +[tool.mypy] +plugins = ["pydantic.mypy"] + +[tool.ruff] +line-length = 120 + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/seed/pydantic/mixed-file-directory/snippet-templates.json b/seed/pydantic/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/pydantic/mixed-file-directory/snippet.json b/seed/pydantic/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/__init__.py new file mode 100644 index 00000000000..34fc1641c93 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from .id import Id +from .resources import CreateOrganizationRequest, Organization, User, organization, user + +__all__ = ["CreateOrganizationRequest", "Id", "Organization", "User", "organization", "user"] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/__init__.py new file mode 100644 index 00000000000..9c7cd65aa25 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/__init__.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, +) +from .serialization import FieldMetadata + +__all__ = [ + "FieldMetadata", + "IS_PYDANTIC_V2", + "UniversalBaseModel", + "UniversalRootModel", + "parse_obj_as", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", +] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/datetime_utils.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/datetime_utils.py new file mode 100644 index 00000000000..7c9864a944c --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/datetime_utils.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/pydantic_utilities.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/pydantic_utilities.py new file mode 100644 index 00000000000..3c4ca2f49b3 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/pydantic_utilities.py @@ -0,0 +1,249 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +import typing +from collections import defaultdict + +import typing_extensions + +import pydantic + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + # isort will try to reformat the comments on these imports, which breaks mypy + # isort: off + from pydantic.v1.datetime_parse import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + parse_date as parse_date, + ) + from pydantic.v1.datetime_parse import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + parse_datetime as parse_datetime, + ) + from pydantic.v1.json import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + ENCODERS_BY_TYPE as encoders_by_type, + ) + from pydantic.v1.typing import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + get_args as get_args, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + get_origin as get_origin, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + is_literal_type as is_literal_type, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + is_union as is_union, + ) + from pydantic.v1.fields import ModelField as ModelField # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore # Pydantic v1 + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore # Pydantic v1 + from pydantic.fields import ModelField as ModelField # type: ignore # Pydantic v1 + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore # Pydantic v1 + from pydantic.typing import get_args as get_args # type: ignore # Pydantic v1 + from pydantic.typing import get_origin as get_origin # type: ignore # Pydantic v1 + from pydantic.typing import is_literal_type as is_literal_type # type: ignore # Pydantic v1 + from pydantic.typing import is_union as is_union # type: ignore # Pydantic v1 + + # isort: on + + +T = typing.TypeVar("T") +Model = typing.TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore # Pydantic v2 + return adapter.validate_python(dealiased_object) + else: + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback( + obj: typing.Any, fallback_serializer: typing.Callable[[typing.Any], typing.Any] +) -> typing.Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + else: + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + protected_namespaces=(), + json_encoders={dt.datetime: serialize_datetime}, + ) # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore # Pydantic v2 + else: + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multi-plexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset: typing.Any = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none: typing.Any = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore # Pydantic v2 + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore # Pydantic v2 + ) + + else: + _fields_set = self.__fields_set__ + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default != None: + _fields_set.add(name) + + kwargs_with_defaults_exclude_unset_include_fields: typing.Any = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write") + + +def deep_union_pydantic_dicts( + source: typing.Dict[str, typing.Any], destination: typing.Dict[str, typing.Any] +) -> typing.Dict[str, typing.Any]: + for key, value in source.items(): + if isinstance(value, dict): + node = destination.setdefault(key, {}) + deep_union_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore # Pydantic v2 + pass + + UniversalRootModel: typing_extensions.TypeAlias = V2RootModel # type: ignore +else: + UniversalRootModel: typing_extensions.TypeAlias = UniversalBaseModel # type: ignore + + +def encode_by_type(o: typing.Any) -> typing.Any: + encoders_by_class_tuples: typing.Dict[typing.Callable[[typing.Any], typing.Any], typing.Tuple[typing.Any, ...]] = ( + defaultdict(tuple) + ) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: typing.Type["Model"]) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore # Pydantic v2 + else: + model.update_forward_refs() + + +# Mirrors Pydantic's internal typing +AnyCallable = typing.Callable[..., typing.Any] + + +def universal_root_validator( + pre: bool = False, +) -> typing.Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return pydantic.model_validator(mode="before" if pre else "after")(func) # type: ignore # Pydantic v2 + else: + return pydantic.root_validator(pre=pre)(func) # type: ignore # Pydantic v1 + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> typing.Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return pydantic.field_validator(field_name, mode="before" if pre else "after")(func) # type: ignore # Pydantic v2 + else: + return pydantic.validator(field_name, pre=pre)(func) # type: ignore # Pydantic v1 + + return decorator + + +PydanticField = typing.Union[ModelField, pydantic.fields.FieldInfo] + + +def _get_model_fields( + model: typing.Type["Model"], +) -> typing.Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return model.model_fields # type: ignore # Pydantic v2 + else: + return model.__fields__ # type: ignore # Pydantic v1 + + +def _get_field_default(field: PydanticField) -> typing.Any: + try: + value = field.get_default() # type: ignore # Pydantic < v1.10.15 + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/serialization.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/serialization.py new file mode 100644 index 00000000000..5605f1b6f65 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/core/serialization.py @@ -0,0 +1,254 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import typing_extensions + +import pydantic + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/id.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/id.py new file mode 100644 index 00000000000..f066d648f06 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/id.py @@ -0,0 +1,3 @@ +# This file was auto-generated by Fern from our API Definition. + +Id = str diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/py.typed b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/__init__.py new file mode 100644 index 00000000000..e6681c2d643 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/__init__.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +from . import organization, user +from .organization import CreateOrganizationRequest, Organization +from .user import User + +__all__ = ["CreateOrganizationRequest", "Organization", "User", "organization", "user"] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/__init__.py new file mode 100644 index 00000000000..5ef97404389 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from .create_organization_request import CreateOrganizationRequest +from .organization import Organization + +__all__ = ["CreateOrganizationRequest", "Organization"] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/create_organization_request.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/create_organization_request.py new file mode 100644 index 00000000000..dc871f1b0a9 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/create_organization_request.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class CreateOrganizationRequest(UniversalBaseModel): + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/organization.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/organization.py new file mode 100644 index 00000000000..fd4b75b2e65 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/organization/organization.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +from ...core.pydantic_utilities import UniversalBaseModel +from ...id import Id +import typing +from ..user.user import User +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +import pydantic + + +class Organization(UniversalBaseModel): + id: Id + name: str + users: typing.List[User] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/__init__.py new file mode 100644 index 00000000000..836653b3650 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from .resources import Event, events +from .user import User + +__all__ = ["Event", "User", "events"] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/__init__.py new file mode 100644 index 00000000000..6eed10d4d9d --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from . import events +from .events import Event + +__all__ = ["Event", "events"] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/__init__.py new file mode 100644 index 00000000000..f916ddd6020 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from .event import Event +from .resources import Metadata, metadata + +__all__ = ["Event", "Metadata", "metadata"] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/event.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/event.py new file mode 100644 index 00000000000..997a476335c --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/event.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +from .....core.pydantic_utilities import UniversalBaseModel +from .....id import Id +from .....core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class Event(UniversalBaseModel): + id: Id + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/__init__.py new file mode 100644 index 00000000000..bdb62162d58 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from . import metadata +from .metadata import Metadata + +__all__ = ["Metadata", "metadata"] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/metadata/__init__.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/metadata/__init__.py new file mode 100644 index 00000000000..6104b066ab0 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/metadata/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .metadata import Metadata + +__all__ = ["Metadata"] diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/metadata/metadata.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/metadata/metadata.py new file mode 100644 index 00000000000..62424e9f991 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/resources/events/resources/metadata/metadata.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +from .......core.pydantic_utilities import UniversalBaseModel +from .......id import Id +import typing +from .......core.pydantic_utilities import IS_PYDANTIC_V2 +import pydantic + + +class Metadata(UniversalBaseModel): + id: Id + value: typing.Optional[typing.Any] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow diff --git a/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/user.py b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/user.py new file mode 100644 index 00000000000..73b0cc0eba0 --- /dev/null +++ b/seed/pydantic/mixed-file-directory/src/seed/mixed_file_directory/resources/user/user.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +from ...core.pydantic_utilities import UniversalBaseModel +from ...id import Id +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class User(UniversalBaseModel): + id: Id + name: str + age: int + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow diff --git a/seed/pydantic/mixed-file-directory/tests/custom/test_client.py b/seed/pydantic/mixed-file-directory/tests/custom/test_client.py new file mode 100644 index 00000000000..73f811f5ede --- /dev/null +++ b/seed/pydantic/mixed-file-directory/tests/custom/test_client.py @@ -0,0 +1,7 @@ +import pytest + + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True == True diff --git a/seed/python-sdk/mixed-file-directory/.github/workflows/ci.yml b/seed/python-sdk/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..b7316b8cab7 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: ci + +on: [push] +jobs: + compile: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Compile + run: poetry run mypy . + test: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + + - name: Install Fern + run: npm install -g fern-api + - name: Test + run: fern test --command "poetry run pytest -rP ." + + publish: + needs: [compile, test] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Publish to pypi + run: | + poetry config repositories.remote + poetry --no-interaction -v publish --build --repository remote --username "$" --password "$" + env: + : ${{ secrets. }} + : ${{ secrets. }} diff --git a/seed/python-sdk/mixed-file-directory/.gitignore b/seed/python-sdk/mixed-file-directory/.gitignore new file mode 100644 index 00000000000..0da665feeef --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.gitignore @@ -0,0 +1,5 @@ +dist/ +.mypy_cache/ +__pycache__/ +poetry.toml +.ruff_cache/ diff --git a/seed/python-sdk/mixed-file-directory/.mock/definition/__package__.yml b/seed/python-sdk/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/python-sdk/mixed-file-directory/.mock/definition/api.yml b/seed/python-sdk/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/python-sdk/mixed-file-directory/.mock/definition/organization.yml b/seed/python-sdk/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/python-sdk/mixed-file-directory/.mock/definition/user.yml b/seed/python-sdk/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/python-sdk/mixed-file-directory/.mock/definition/user/events.yml b/seed/python-sdk/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/python-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/python-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/python-sdk/mixed-file-directory/.mock/fern.config.json b/seed/python-sdk/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/python-sdk/mixed-file-directory/.mock/generators.yml b/seed/python-sdk/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/python-sdk/mixed-file-directory/README.md b/seed/python-sdk/mixed-file-directory/README.md new file mode 100644 index 00000000000..30622431594 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/README.md @@ -0,0 +1,134 @@ +# Seed Python Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-SDK%20generated%20by%20Fern-brightgreen)](https://github.com/fern-api/fern) +[![pypi](https://img.shields.io/pypi/v/fern_mixed-file-directory)](https://pypi.python.org/pypi/fern_mixed-file-directory) + +The Seed Python library provides convenient access to the Seed API from Python. + +## Installation + +```sh +pip install fern_mixed-file-directory +``` + +## Usage + +Instantiate and use the client with the following: + +```python +from seed import SeedMixedFileDirectory + +client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", +) +client.organization.create( + name="string", +) +``` + +## Async Client + +The SDK also exports an `async` client so that you can make non-blocking calls to our API. + +```python +import asyncio + +from seed import AsyncSeedMixedFileDirectory + +client = AsyncSeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", +) + + +async def main() -> None: + await client.organization.create( + name="string", + ) + + +asyncio.run(main()) +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```python +from seed.core.api_error import ApiError + +try: + client.organization.create(...) +except ApiError as e: + print(e.status_code) + print(e.body) +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retriable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retriable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `max_retries` request option to configure this behavior. + +```python +client.organization.create(..., { + "max_retries": 1 +}) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. + +```python + +from seed import SeedMixedFileDirectory + +client = SeedMixedFileDirectory( + ..., + timeout=20.0, +) + + +# Override timeout for a specific method +client.organization.create(..., { + "timeout_in_seconds": 1 +}) +``` + +### Custom Client + +You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies +and transports. +```python +import httpx +from seed import SeedMixedFileDirectory + +client = SeedMixedFileDirectory( + ..., + httpx_client=httpx.Client( + proxies="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/python-sdk/mixed-file-directory/pyproject.toml b/seed/python-sdk/mixed-file-directory/pyproject.toml new file mode 100644 index 00000000000..0b90626a364 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/pyproject.toml @@ -0,0 +1,61 @@ +[tool.poetry] +name = "fern_mixed-file-directory" +version = "0.0.1" +description = "" +readme = "README.md" +authors = [] +keywords = [] + +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed" +] +packages = [ + { include = "seed", from = "src"} +] + +[project.urls] +Repository = 'https://github.com/mixed-file-directory/fern' + +[tool.poetry.dependencies] +python = "^3.8" +httpx = ">=0.21.2" +pydantic = ">= 1.9.2" +pydantic-core = "^2.18.2" +typing_extensions = ">= 4.0.0" + +[tool.poetry.dev-dependencies] +mypy = "1.0.1" +pytest = "^7.4.0" +pytest-asyncio = "^0.23.5" +python-dateutil = "^2.9.0" +types-python-dateutil = "^2.9.0.20240316" +ruff = "^0.5.6" + +[tool.pytest.ini_options] +testpaths = [ "tests" ] +asyncio_mode = "auto" + +[tool.mypy] +plugins = ["pydantic.mypy"] + +[tool.ruff] +line-length = 120 + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/seed/python-sdk/mixed-file-directory/reference.md b/seed/python-sdk/mixed-file-directory/reference.md new file mode 100644 index 00000000000..2ec46ac0fda --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/reference.md @@ -0,0 +1,285 @@ +# Reference +## Organization +

client.organization.create(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a new organization. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedMixedFileDirectory + +client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", +) +client.organization.create( + name="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## User +
client.user.list(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List all users. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedMixedFileDirectory + +client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", +) +client.user.list( + limit=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — The maximum number of results to return. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## User Events +
client.user.events.list_events(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List all user events. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedMixedFileDirectory + +client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", +) +client.user.events.list_events( + limit=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — The maximum number of results to return. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## User Events Metadata +
client.user.events.metadata.get_metadata(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get event metadata. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedMixedFileDirectory + +client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", +) +client.user.events.metadata.get_metadata( + id="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `Id` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/seed/python-sdk/mixed-file-directory/snippet-templates.json b/seed/python-sdk/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..565fe588e66 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/snippet-templates.json @@ -0,0 +1,362 @@ +[ + { + "sdk": { + "package": "fern_mixed-file-directory", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/organizations", + "method": "POST", + "identifierOverride": "endpoint_organization.create" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedMixedFileDirectory" + ], + "isOptional": true, + "templateString": "client = SeedMixedFileDirectory(base_url=\"https://yourhost.com/path/to/api\", )", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.organization.create(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "name=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": "name", + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedMixedFileDirectory" + ], + "isOptional": true, + "templateString": "client = AsyncSeedMixedFileDirectory(base_url=\"https://yourhost.com/path/to/api\", )", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.organization.create(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "name=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": "name", + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_mixed-file-directory", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/users", + "method": "GET", + "identifierOverride": "endpoint_user.list" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedMixedFileDirectory" + ], + "isOptional": true, + "templateString": "client = SeedMixedFileDirectory(base_url=\"https://yourhost.com/path/to/api\", )", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.user.list(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "limit=$FERN_INPUT", + "templateInputs": [ + { + "location": "QUERY", + "path": "limit", + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedMixedFileDirectory" + ], + "isOptional": true, + "templateString": "client = AsyncSeedMixedFileDirectory(base_url=\"https://yourhost.com/path/to/api\", )", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.user.list(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "limit=$FERN_INPUT", + "templateInputs": [ + { + "location": "QUERY", + "path": "limit", + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_mixed-file-directory", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/users/events", + "method": "GET", + "identifierOverride": "endpoint_user/events.listEvents" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedMixedFileDirectory" + ], + "isOptional": true, + "templateString": "client = SeedMixedFileDirectory(base_url=\"https://yourhost.com/path/to/api\", )", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.user.events.list_events(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "limit=$FERN_INPUT", + "templateInputs": [ + { + "location": "QUERY", + "path": "limit", + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedMixedFileDirectory" + ], + "isOptional": true, + "templateString": "client = AsyncSeedMixedFileDirectory(base_url=\"https://yourhost.com/path/to/api\", )", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.user.events.list_events(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "limit=$FERN_INPUT", + "templateInputs": [ + { + "location": "QUERY", + "path": "limit", + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_mixed-file-directory", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/users/events/metadata", + "method": "GET", + "identifierOverride": "endpoint_user/events/metadata.getMetadata" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedMixedFileDirectory" + ], + "isOptional": true, + "templateString": "client = SeedMixedFileDirectory(base_url=\"https://yourhost.com/path/to/api\", )", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.user.events.metadata.get_metadata(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "id=$FERN_INPUT", + "templateInputs": [ + { + "location": "QUERY", + "path": "id", + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedMixedFileDirectory" + ], + "isOptional": true, + "templateString": "client = AsyncSeedMixedFileDirectory(base_url=\"https://yourhost.com/path/to/api\", )", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.user.events.metadata.get_metadata(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "id=$FERN_INPUT", + "templateInputs": [ + { + "location": "QUERY", + "path": "id", + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + } +] \ No newline at end of file diff --git a/seed/python-sdk/mixed-file-directory/snippet.json b/seed/python-sdk/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..2d7e5262975 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/snippet.json @@ -0,0 +1,57 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": "default", + "id": { + "path": "/organizations/", + "method": "POST", + "identifier_override": "endpoint_organization.create" + }, + "snippet": { + "sync_client": "from seed import SeedMixedFileDirectory\n\nclient = SeedMixedFileDirectory(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.organization.create(\n name=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedMixedFileDirectory\n\nclient = AsyncSeedMixedFileDirectory(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.organization.create(\n name=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/users/", + "method": "GET", + "identifier_override": "endpoint_user.list" + }, + "snippet": { + "sync_client": "from seed import SeedMixedFileDirectory\n\nclient = SeedMixedFileDirectory(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.list(\n limit=1,\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedMixedFileDirectory\n\nclient = AsyncSeedMixedFileDirectory(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.list(\n limit=1,\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/users/events/", + "method": "GET", + "identifier_override": "endpoint_user/events.listEvents" + }, + "snippet": { + "sync_client": "from seed import SeedMixedFileDirectory\n\nclient = SeedMixedFileDirectory(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.events.list_events(\n limit=1,\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedMixedFileDirectory\n\nclient = AsyncSeedMixedFileDirectory(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.events.list_events(\n limit=1,\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/users/events/metadata/", + "method": "GET", + "identifier_override": "endpoint_user/events/metadata.getMetadata" + }, + "snippet": { + "sync_client": "from seed import SeedMixedFileDirectory\n\nclient = SeedMixedFileDirectory(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.events.metadata.get_metadata(\n id=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedMixedFileDirectory\n\nclient = AsyncSeedMixedFileDirectory(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.events.metadata.get_metadata(\n id=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + } + ] +} \ No newline at end of file diff --git a/seed/python-sdk/mixed-file-directory/src/seed/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/__init__.py new file mode 100644 index 00000000000..3afff74f4af --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/__init__.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import Id +from . import organization, user +from .client import AsyncSeedMixedFileDirectory, SeedMixedFileDirectory +from .organization import CreateOrganizationRequest, Organization +from .user import User +from .version import __version__ + +__all__ = [ + "AsyncSeedMixedFileDirectory", + "CreateOrganizationRequest", + "Id", + "Organization", + "SeedMixedFileDirectory", + "User", + "__version__", + "organization", + "user", +] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/client.py b/seed/python-sdk/mixed-file-directory/src/seed/client.py new file mode 100644 index 00000000000..4c7e03b86ba --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/client.py @@ -0,0 +1,108 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +import httpx +from .core.client_wrapper import SyncClientWrapper +from .organization.client import OrganizationClient +from .user.client import UserClient +from .core.client_wrapper import AsyncClientWrapper +from .organization.client import AsyncOrganizationClient +from .user.client import AsyncUserClient + + +class SeedMixedFileDirectory: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from seed import SeedMixedFileDirectory + + client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + self._client_wrapper = SyncClientWrapper( + base_url=base_url, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.organization = OrganizationClient(client_wrapper=self._client_wrapper) + self.user = UserClient(client_wrapper=self._client_wrapper) + + +class AsyncSeedMixedFileDirectory: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from seed import AsyncSeedMixedFileDirectory + + client = AsyncSeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + self._client_wrapper = AsyncClientWrapper( + base_url=base_url, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.organization = AsyncOrganizationClient(client_wrapper=self._client_wrapper) + self.user = AsyncUserClient(client_wrapper=self._client_wrapper) diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/core/__init__.py new file mode 100644 index 00000000000..4213c349a1d --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/__init__.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +from .api_error import ApiError +from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper +from .datetime_utils import serialize_datetime +from .file import File, convert_file_dict_to_httpx_tuples +from .http_client import AsyncHttpClient, HttpClient +from .jsonable_encoder import jsonable_encoder +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, +) +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions +from .serialization import FieldMetadata, convert_and_respect_annotation_metadata + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "BaseClientWrapper", + "FieldMetadata", + "File", + "HttpClient", + "IS_PYDANTIC_V2", + "RequestOptions", + "SyncClientWrapper", + "UniversalBaseModel", + "UniversalRootModel", + "convert_and_respect_annotation_metadata", + "convert_file_dict_to_httpx_tuples", + "encode_query", + "jsonable_encoder", + "parse_obj_as", + "remove_none_from_dict", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", +] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/api_error.py b/seed/python-sdk/mixed-file-directory/src/seed/core/api_error.py new file mode 100644 index 00000000000..2e9fc5431cd --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/api_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + + +class ApiError(Exception): + status_code: typing.Optional[int] + body: typing.Any + + def __init__(self, *, status_code: typing.Optional[int] = None, body: typing.Any = None): + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"status_code: {self.status_code}, body: {self.body}" diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/client_wrapper.py b/seed/python-sdk/mixed-file-directory/src/seed/core/client_wrapper.py new file mode 100644 index 00000000000..95fe49fd708 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/client_wrapper.py @@ -0,0 +1,48 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +import httpx +from .http_client import HttpClient +from .http_client import AsyncHttpClient + + +class BaseClientWrapper: + def __init__(self, *, base_url: str, timeout: typing.Optional[float] = None): + self._base_url = base_url + self._timeout = timeout + + def get_headers(self) -> typing.Dict[str, str]: + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + "X-Fern-SDK-Name": "fern_mixed-file-directory", + "X-Fern-SDK-Version": "0.0.1", + } + return headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + +class SyncClientWrapper(BaseClientWrapper): + def __init__(self, *, base_url: str, timeout: typing.Optional[float] = None, httpx_client: httpx.Client): + super().__init__(base_url=base_url, timeout=timeout) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers(), + base_timeout=self.get_timeout(), + base_url=self.get_base_url(), + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__(self, *, base_url: str, timeout: typing.Optional[float] = None, httpx_client: httpx.AsyncClient): + super().__init__(base_url=base_url, timeout=timeout) + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers(), + base_timeout=self.get_timeout(), + base_url=self.get_base_url(), + ) diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/datetime_utils.py b/seed/python-sdk/mixed-file-directory/src/seed/core/datetime_utils.py new file mode 100644 index 00000000000..7c9864a944c --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/datetime_utils.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/file.py b/seed/python-sdk/mixed-file-directory/src/seed/core/file.py new file mode 100644 index 00000000000..6e0f92bfcb1 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/file.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = typing.Union[typing.IO[bytes], bytes, str] +File = typing.Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + typing.Tuple[typing.Optional[str], FileContent], + # (filename, file (or bytes), content_type) + typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str]], + # (filename, file (or bytes), content_type, headers) + typing.Tuple[ + typing.Optional[str], + FileContent, + typing.Optional[str], + typing.Mapping[str, str], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: typing.Dict[str, typing.Union[File, typing.List[File]]], +) -> typing.List[typing.Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/http_client.py b/seed/python-sdk/mixed-file-directory/src/seed/core/http_client.py new file mode 100644 index 00000000000..b07401b50eb --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/http_client.py @@ -0,0 +1,477 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import json +import re +import time +import typing +import urllib.parse +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx + +from .file import File, convert_file_dict_to_httpx_tuples +from .jsonable_encoder import jsonable_encoder +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions + +INITIAL_RETRY_DELAY_SECONDS = 0.5 +MAX_RETRY_DELAY_SECONDS = 10 +MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: + return retry_after + + # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. + retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + + # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. + timeout = retry_delay * (1 - 0.25 * random()) + return timeout if timeout >= 0 else 0 + + +def _should_retry(response: httpx.Response) -> bool: + retriable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retriable_400s + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + # If you have an empty JSON body, you should just send None + return (json_body if json_body != {} else None), data_body if data_body != {} else None + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Optional[float], + base_headers: typing.Dict[str, str], + base_url: typing.Optional[str] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = self.base_url if maybe_base_url is None else maybe_base_url + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + response = self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Optional[float], + base_headers: typing.Dict[str, str], + base_url: typing.Optional[str] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = self.base_url if maybe_base_url is None else maybe_base_url + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + # Add the input to each of these and do None-safety checks + response = await self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + async with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + timeout=timeout, + ) as stream: + yield stream diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/jsonable_encoder.py b/seed/python-sdk/mixed-file-directory/src/seed/core/jsonable_encoder.py new file mode 100644 index 00000000000..1b631e9017c --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/jsonable_encoder.py @@ -0,0 +1,101 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union + +import pydantic + +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/pydantic_utilities.py b/seed/python-sdk/mixed-file-directory/src/seed/core/pydantic_utilities.py new file mode 100644 index 00000000000..3c4ca2f49b3 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/pydantic_utilities.py @@ -0,0 +1,249 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +import typing +from collections import defaultdict + +import typing_extensions + +import pydantic + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + # isort will try to reformat the comments on these imports, which breaks mypy + # isort: off + from pydantic.v1.datetime_parse import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + parse_date as parse_date, + ) + from pydantic.v1.datetime_parse import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + parse_datetime as parse_datetime, + ) + from pydantic.v1.json import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + ENCODERS_BY_TYPE as encoders_by_type, + ) + from pydantic.v1.typing import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + get_args as get_args, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + get_origin as get_origin, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + is_literal_type as is_literal_type, + ) + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + is_union as is_union, + ) + from pydantic.v1.fields import ModelField as ModelField # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore # Pydantic v1 + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore # Pydantic v1 + from pydantic.fields import ModelField as ModelField # type: ignore # Pydantic v1 + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore # Pydantic v1 + from pydantic.typing import get_args as get_args # type: ignore # Pydantic v1 + from pydantic.typing import get_origin as get_origin # type: ignore # Pydantic v1 + from pydantic.typing import is_literal_type as is_literal_type # type: ignore # Pydantic v1 + from pydantic.typing import is_union as is_union # type: ignore # Pydantic v1 + + # isort: on + + +T = typing.TypeVar("T") +Model = typing.TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore # Pydantic v2 + return adapter.validate_python(dealiased_object) + else: + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback( + obj: typing.Any, fallback_serializer: typing.Callable[[typing.Any], typing.Any] +) -> typing.Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + else: + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + protected_namespaces=(), + json_encoders={dt.datetime: serialize_datetime}, + ) # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore # Pydantic v2 + else: + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multi-plexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset: typing.Any = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none: typing.Any = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore # Pydantic v2 + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore # Pydantic v2 + ) + + else: + _fields_set = self.__fields_set__ + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default != None: + _fields_set.add(name) + + kwargs_with_defaults_exclude_unset_include_fields: typing.Any = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write") + + +def deep_union_pydantic_dicts( + source: typing.Dict[str, typing.Any], destination: typing.Dict[str, typing.Any] +) -> typing.Dict[str, typing.Any]: + for key, value in source.items(): + if isinstance(value, dict): + node = destination.setdefault(key, {}) + deep_union_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore # Pydantic v2 + pass + + UniversalRootModel: typing_extensions.TypeAlias = V2RootModel # type: ignore +else: + UniversalRootModel: typing_extensions.TypeAlias = UniversalBaseModel # type: ignore + + +def encode_by_type(o: typing.Any) -> typing.Any: + encoders_by_class_tuples: typing.Dict[typing.Callable[[typing.Any], typing.Any], typing.Tuple[typing.Any, ...]] = ( + defaultdict(tuple) + ) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: typing.Type["Model"]) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore # Pydantic v2 + else: + model.update_forward_refs() + + +# Mirrors Pydantic's internal typing +AnyCallable = typing.Callable[..., typing.Any] + + +def universal_root_validator( + pre: bool = False, +) -> typing.Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return pydantic.model_validator(mode="before" if pre else "after")(func) # type: ignore # Pydantic v2 + else: + return pydantic.root_validator(pre=pre)(func) # type: ignore # Pydantic v1 + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> typing.Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return pydantic.field_validator(field_name, mode="before" if pre else "after")(func) # type: ignore # Pydantic v2 + else: + return pydantic.validator(field_name, pre=pre)(func) # type: ignore # Pydantic v1 + + return decorator + + +PydanticField = typing.Union[ModelField, pydantic.fields.FieldInfo] + + +def _get_model_fields( + model: typing.Type["Model"], +) -> typing.Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return model.model_fields # type: ignore # Pydantic v2 + else: + return model.__fields__ # type: ignore # Pydantic v1 + + +def _get_field_default(field: PydanticField) -> typing.Any: + try: + value = field.get_default() # type: ignore # Pydantic < v1.10.15 + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/query_encoder.py b/seed/python-sdk/mixed-file-directory/src/seed/core/query_encoder.py new file mode 100644 index 00000000000..3183001d404 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/remove_none_from_dict.py b/seed/python-sdk/mixed-file-directory/src/seed/core/remove_none_from_dict.py new file mode 100644 index 00000000000..c2298143f14 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/request_options.py b/seed/python-sdk/mixed-file-directory/src/seed/core/request_options.py new file mode 100644 index 00000000000..d0bf0dbcecd --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/request_options.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + """ + + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/core/serialization.py b/seed/python-sdk/mixed-file-directory/src/seed/core/serialization.py new file mode 100644 index 00000000000..5605f1b6f65 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/core/serialization.py @@ -0,0 +1,254 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import typing_extensions + +import pydantic + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/seed/python-sdk/mixed-file-directory/src/seed/organization/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/organization/__init__.py new file mode 100644 index 00000000000..66f1cdc2b00 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/organization/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import CreateOrganizationRequest, Organization + +__all__ = ["CreateOrganizationRequest", "Organization"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/organization/client.py b/seed/python-sdk/mixed-file-directory/src/seed/organization/client.py new file mode 100644 index 00000000000..748cf12019a --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/organization/client.py @@ -0,0 +1,129 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from ..core.client_wrapper import SyncClientWrapper +from ..core.request_options import RequestOptions +from .types.organization import Organization +from ..core.pydantic_utilities import parse_obj_as +from json.decoder import JSONDecodeError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class OrganizationClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create(self, *, name: str, request_options: typing.Optional[RequestOptions] = None) -> Organization: + """ + Create a new organization. + + Parameters + ---------- + name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Organization + + Examples + -------- + from seed import SeedMixedFileDirectory + + client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + client.organization.create( + name="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "organizations/", + method="POST", + json={ + "name": name, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + Organization, + parse_obj_as( + type_=Organization, # type: ignore + object_=_response.json(), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +class AsyncOrganizationClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create(self, *, name: str, request_options: typing.Optional[RequestOptions] = None) -> Organization: + """ + Create a new organization. + + Parameters + ---------- + name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Organization + + Examples + -------- + import asyncio + + from seed import AsyncSeedMixedFileDirectory + + client = AsyncSeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organization.create( + name="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "organizations/", + method="POST", + json={ + "name": name, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + Organization, + parse_obj_as( + type_=Organization, # type: ignore + object_=_response.json(), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/seed/python-sdk/mixed-file-directory/src/seed/organization/types/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/organization/types/__init__.py new file mode 100644 index 00000000000..5ef97404389 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/organization/types/__init__.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +from .create_organization_request import CreateOrganizationRequest +from .organization import Organization + +__all__ = ["CreateOrganizationRequest", "Organization"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/organization/types/create_organization_request.py b/seed/python-sdk/mixed-file-directory/src/seed/organization/types/create_organization_request.py new file mode 100644 index 00000000000..1d5aeb5f689 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/organization/types/create_organization_request.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class CreateOrganizationRequest(UniversalBaseModel): + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/mixed-file-directory/src/seed/organization/types/organization.py b/seed/python-sdk/mixed-file-directory/src/seed/organization/types/organization.py new file mode 100644 index 00000000000..d86ef2bcdf5 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/organization/types/organization.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from ...core.pydantic_utilities import UniversalBaseModel +from ...types.id import Id +import typing +from ...user.types.user import User +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +import pydantic + + +class Organization(UniversalBaseModel): + id: Id + name: str + users: typing.List[User] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/mixed-file-directory/src/seed/py.typed b/seed/python-sdk/mixed-file-directory/src/seed/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/python-sdk/mixed-file-directory/src/seed/types/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/types/__init__.py new file mode 100644 index 00000000000..e838afc85bd --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/types/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .id import Id + +__all__ = ["Id"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/types/id.py b/seed/python-sdk/mixed-file-directory/src/seed/types/id.py new file mode 100644 index 00000000000..f066d648f06 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/types/id.py @@ -0,0 +1,3 @@ +# This file was auto-generated by Fern from our API Definition. + +Id = str diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/user/__init__.py new file mode 100644 index 00000000000..21825cd3e08 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/__init__.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import User +from . import events +from .events import Event + +__all__ = ["Event", "User", "events"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/client.py b/seed/python-sdk/mixed-file-directory/src/seed/user/client.py new file mode 100644 index 00000000000..5441c768ac1 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/client.py @@ -0,0 +1,134 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.client_wrapper import SyncClientWrapper +from .events.client import EventsClient +import typing +from ..core.request_options import RequestOptions +from .types.user import User +from ..core.pydantic_utilities import parse_obj_as +from json.decoder import JSONDecodeError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper +from .events.client import AsyncEventsClient + + +class UserClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + self.events = EventsClient(client_wrapper=self._client_wrapper) + + def list( + self, *, limit: typing.Optional[int] = None, request_options: typing.Optional[RequestOptions] = None + ) -> typing.List[User]: + """ + List all users. + + Parameters + ---------- + limit : typing.Optional[int] + The maximum number of results to return. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[User] + + Examples + -------- + from seed import SeedMixedFileDirectory + + client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + client.user.list( + limit=1, + ) + """ + _response = self._client_wrapper.httpx_client.request( + "users/", + method="GET", + params={ + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + typing.List[User], + parse_obj_as( + type_=typing.List[User], # type: ignore + object_=_response.json(), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +class AsyncUserClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + self.events = AsyncEventsClient(client_wrapper=self._client_wrapper) + + async def list( + self, *, limit: typing.Optional[int] = None, request_options: typing.Optional[RequestOptions] = None + ) -> typing.List[User]: + """ + List all users. + + Parameters + ---------- + limit : typing.Optional[int] + The maximum number of results to return. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[User] + + Examples + -------- + import asyncio + + from seed import AsyncSeedMixedFileDirectory + + client = AsyncSeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.list( + limit=1, + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "users/", + method="GET", + params={ + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + typing.List[User], + parse_obj_as( + type_=typing.List[User], # type: ignore + object_=_response.json(), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/events/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/user/events/__init__.py new file mode 100644 index 00000000000..8b24ee9066d --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/events/__init__.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import Event +from . import metadata +from .metadata import Metadata + +__all__ = ["Event", "Metadata", "metadata"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/events/client.py b/seed/python-sdk/mixed-file-directory/src/seed/user/events/client.py new file mode 100644 index 00000000000..bff39c6d86f --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/events/client.py @@ -0,0 +1,134 @@ +# This file was auto-generated by Fern from our API Definition. + +from ...core.client_wrapper import SyncClientWrapper +from .metadata.client import MetadataClient +import typing +from ...core.request_options import RequestOptions +from .types.event import Event +from ...core.pydantic_utilities import parse_obj_as +from json.decoder import JSONDecodeError +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper +from .metadata.client import AsyncMetadataClient + + +class EventsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + self.metadata = MetadataClient(client_wrapper=self._client_wrapper) + + def list_events( + self, *, limit: typing.Optional[int] = None, request_options: typing.Optional[RequestOptions] = None + ) -> typing.List[Event]: + """ + List all user events. + + Parameters + ---------- + limit : typing.Optional[int] + The maximum number of results to return. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[Event] + + Examples + -------- + from seed import SeedMixedFileDirectory + + client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + client.user.events.list_events( + limit=1, + ) + """ + _response = self._client_wrapper.httpx_client.request( + "users/events/", + method="GET", + params={ + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + typing.List[Event], + parse_obj_as( + type_=typing.List[Event], # type: ignore + object_=_response.json(), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +class AsyncEventsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + self.metadata = AsyncMetadataClient(client_wrapper=self._client_wrapper) + + async def list_events( + self, *, limit: typing.Optional[int] = None, request_options: typing.Optional[RequestOptions] = None + ) -> typing.List[Event]: + """ + List all user events. + + Parameters + ---------- + limit : typing.Optional[int] + The maximum number of results to return. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[Event] + + Examples + -------- + import asyncio + + from seed import AsyncSeedMixedFileDirectory + + client = AsyncSeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.events.list_events( + limit=1, + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "users/events/", + method="GET", + params={ + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + typing.List[Event], + parse_obj_as( + type_=typing.List[Event], # type: ignore + object_=_response.json(), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/__init__.py new file mode 100644 index 00000000000..e8376684efc --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import Metadata + +__all__ = ["Metadata"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/client.py b/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/client.py new file mode 100644 index 00000000000..205a23ce9bc --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/client.py @@ -0,0 +1,125 @@ +# This file was auto-generated by Fern from our API Definition. + +from ....core.client_wrapper import SyncClientWrapper +from ....types.id import Id +import typing +from ....core.request_options import RequestOptions +from .types.metadata import Metadata +from ....core.pydantic_utilities import parse_obj_as +from json.decoder import JSONDecodeError +from ....core.api_error import ApiError +from ....core.client_wrapper import AsyncClientWrapper + + +class MetadataClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_metadata(self, *, id: Id, request_options: typing.Optional[RequestOptions] = None) -> Metadata: + """ + Get event metadata. + + Parameters + ---------- + id : Id + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Metadata + + Examples + -------- + from seed import SeedMixedFileDirectory + + client = SeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + client.user.events.metadata.get_metadata( + id="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "users/events/metadata/", + method="GET", + params={ + "id": id, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + Metadata, + parse_obj_as( + type_=Metadata, # type: ignore + object_=_response.json(), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +class AsyncMetadataClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_metadata(self, *, id: Id, request_options: typing.Optional[RequestOptions] = None) -> Metadata: + """ + Get event metadata. + + Parameters + ---------- + id : Id + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Metadata + + Examples + -------- + import asyncio + + from seed import AsyncSeedMixedFileDirectory + + client = AsyncSeedMixedFileDirectory( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.events.metadata.get_metadata( + id="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "users/events/metadata/", + method="GET", + params={ + "id": id, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + Metadata, + parse_obj_as( + type_=Metadata, # type: ignore + object_=_response.json(), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/types/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/types/__init__.py new file mode 100644 index 00000000000..6104b066ab0 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/types/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .metadata import Metadata + +__all__ = ["Metadata"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/types/metadata.py b/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/types/metadata.py new file mode 100644 index 00000000000..c1125a1ac78 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/events/metadata/types/metadata.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +from .....core.pydantic_utilities import UniversalBaseModel +from .....types.id import Id +import typing +from .....core.pydantic_utilities import IS_PYDANTIC_V2 +import pydantic + + +class Metadata(UniversalBaseModel): + id: Id + value: typing.Optional[typing.Any] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/events/types/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/user/events/types/__init__.py new file mode 100644 index 00000000000..00cb9e486c8 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/events/types/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .event import Event + +__all__ = ["Event"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/events/types/event.py b/seed/python-sdk/mixed-file-directory/src/seed/user/events/types/event.py new file mode 100644 index 00000000000..b81bc4497de --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/events/types/event.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +from ....core.pydantic_utilities import UniversalBaseModel +from ....types.id import Id +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class Event(UniversalBaseModel): + id: Id + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/types/__init__.py b/seed/python-sdk/mixed-file-directory/src/seed/user/types/__init__.py new file mode 100644 index 00000000000..b22b663beed --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/types/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .user import User + +__all__ = ["User"] diff --git a/seed/python-sdk/mixed-file-directory/src/seed/user/types/user.py b/seed/python-sdk/mixed-file-directory/src/seed/user/types/user.py new file mode 100644 index 00000000000..c0b0ac2c572 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/user/types/user.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +from ...core.pydantic_utilities import UniversalBaseModel +from ...types.id import Id +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +import typing +import pydantic + + +class User(UniversalBaseModel): + id: Id + name: str + age: int + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/mixed-file-directory/src/seed/version.py b/seed/python-sdk/mixed-file-directory/src/seed/version.py new file mode 100644 index 00000000000..b41d25f110c --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/src/seed/version.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version("fern_mixed-file-directory") diff --git a/seed/python-sdk/mixed-file-directory/tests/__init__.py b/seed/python-sdk/mixed-file-directory/tests/__init__.py new file mode 100644 index 00000000000..f3ea2659bb1 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/seed/python-sdk/mixed-file-directory/tests/conftest.py b/seed/python-sdk/mixed-file-directory/tests/conftest.py new file mode 100644 index 00000000000..f043c769052 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/conftest.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed import SeedMixedFileDirectory +import os +import pytest +from seed import AsyncSeedMixedFileDirectory + + +@pytest.fixture +def client() -> SeedMixedFileDirectory: + return SeedMixedFileDirectory(base_url=os.getenv("TESTS_BASE_URL", "base_url")) + + +@pytest.fixture +def async_client() -> AsyncSeedMixedFileDirectory: + return AsyncSeedMixedFileDirectory(base_url=os.getenv("TESTS_BASE_URL", "base_url")) diff --git a/seed/python-sdk/mixed-file-directory/tests/custom/test_client.py b/seed/python-sdk/mixed-file-directory/tests/custom/test_client.py new file mode 100644 index 00000000000..73f811f5ede --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/custom/test_client.py @@ -0,0 +1,7 @@ +import pytest + + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True == True diff --git a/seed/python-sdk/mixed-file-directory/tests/test_organization.py b/seed/python-sdk/mixed-file-directory/tests/test_organization.py new file mode 100644 index 00000000000..0c6d56ddf1b --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/test_organization.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed import SeedMixedFileDirectory +from seed import AsyncSeedMixedFileDirectory +import typing +from .utilities import validate_response + + +async def test_create(client: SeedMixedFileDirectory, async_client: AsyncSeedMixedFileDirectory) -> None: + expected_response: typing.Any = { + "id": "string", + "name": "string", + "users": [{"id": "string", "name": "string", "age": 1}], + } + expected_types: typing.Any = { + "id": None, + "name": None, + "users": ("list", {0: {"id": None, "name": None, "age": "integer"}}), + } + response = client.organization.create(name="string") + validate_response(response, expected_response, expected_types) + + async_response = await async_client.organization.create(name="string") + validate_response(async_response, expected_response, expected_types) diff --git a/seed/python-sdk/mixed-file-directory/tests/test_user.py b/seed/python-sdk/mixed-file-directory/tests/test_user.py new file mode 100644 index 00000000000..897282088f8 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/test_user.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed import SeedMixedFileDirectory +from seed import AsyncSeedMixedFileDirectory +import typing +from .utilities import validate_response + + +async def test_list_(client: SeedMixedFileDirectory, async_client: AsyncSeedMixedFileDirectory) -> None: + expected_response: typing.Any = [{"id": "string", "name": "string", "age": 1}] + expected_types: typing.Tuple[typing.Any, typing.Any] = ("list", {0: {"id": None, "name": None, "age": "integer"}}) + response = client.user.list(limit=1) + validate_response(response, expected_response, expected_types) + + async_response = await async_client.user.list(limit=1) + validate_response(async_response, expected_response, expected_types) diff --git a/seed/python-sdk/mixed-file-directory/tests/user/__init__.py b/seed/python-sdk/mixed-file-directory/tests/user/__init__.py new file mode 100644 index 00000000000..f3ea2659bb1 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/user/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/seed/python-sdk/mixed-file-directory/tests/user/events/__init__.py b/seed/python-sdk/mixed-file-directory/tests/user/events/__init__.py new file mode 100644 index 00000000000..f3ea2659bb1 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/user/events/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/seed/python-sdk/mixed-file-directory/tests/user/events/test_metadata.py b/seed/python-sdk/mixed-file-directory/tests/user/events/test_metadata.py new file mode 100644 index 00000000000..fee5272ffbb --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/user/events/test_metadata.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed import SeedMixedFileDirectory +from seed import AsyncSeedMixedFileDirectory +import typing +from ...utilities import validate_response + + +async def test_get_metadata(client: SeedMixedFileDirectory, async_client: AsyncSeedMixedFileDirectory) -> None: + expected_response: typing.Any = {"id": "string", "value": {"key": "value"}} + expected_types: typing.Any = {"id": None, "value": None} + response = client.user.events.metadata.get_metadata(id="string") + validate_response(response, expected_response, expected_types) + + async_response = await async_client.user.events.metadata.get_metadata(id="string") + validate_response(async_response, expected_response, expected_types) diff --git a/seed/python-sdk/mixed-file-directory/tests/user/test_events.py b/seed/python-sdk/mixed-file-directory/tests/user/test_events.py new file mode 100644 index 00000000000..a14185d1c27 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/user/test_events.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed import SeedMixedFileDirectory +from seed import AsyncSeedMixedFileDirectory +import typing +from ..utilities import validate_response + + +async def test_list_events(client: SeedMixedFileDirectory, async_client: AsyncSeedMixedFileDirectory) -> None: + expected_response: typing.Any = [{"id": "string", "name": "string"}] + expected_types: typing.Tuple[typing.Any, typing.Any] = ("list", {0: {"id": None, "name": None}}) + response = client.user.events.list_events(limit=1) + validate_response(response, expected_response, expected_types) + + async_response = await async_client.user.events.list_events(limit=1) + validate_response(async_response, expected_response, expected_types) diff --git a/seed/python-sdk/mixed-file-directory/tests/utilities.py b/seed/python-sdk/mixed-file-directory/tests/utilities.py new file mode 100644 index 00000000000..3d228806a9c --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utilities.py @@ -0,0 +1,162 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +import uuid + +from dateutil import parser + +import pydantic + + +def cast_field(json_expectation: typing.Any, type_expectation: typing.Any) -> typing.Any: + # Cast these specific types which come through as string and expect our + # models to cast to the correct type. + if type_expectation == "uuid": + return uuid.UUID(json_expectation) + elif type_expectation == "date": + return parser.parse(json_expectation).date() + elif type_expectation == "datetime": + return parser.parse(json_expectation) + elif type_expectation == "set": + return set(json_expectation) + elif type_expectation == "integer": + # Necessary as we allow numeric keys, but JSON makes them strings + return int(json_expectation) + + return json_expectation + + +def validate_field(response: typing.Any, json_expectation: typing.Any, type_expectation: typing.Any) -> None: + # Allow for an escape hatch if the object cannot be validated + if type_expectation == "no_validate": + return + + is_container_of_complex_type = False + # Parse types in containers, note that dicts are handled within `validate_response` + if isinstance(json_expectation, list): + if isinstance(type_expectation, tuple): + container_expectation = type_expectation[0] + contents_expectation = type_expectation[1] + + cast_json_expectation = [] + for idx, ex in enumerate(json_expectation): + if isinstance(contents_expectation, dict): + entry_expectation = contents_expectation.get(idx) + if isinstance(entry_expectation, dict): + is_container_of_complex_type = True + validate_response( + response=response[idx], + json_expectation=ex, + type_expectations=entry_expectation, + ) + else: + cast_json_expectation.append(cast_field(ex, entry_expectation)) + else: + cast_json_expectation.append(ex) + json_expectation = cast_json_expectation + + # Note that we explicitly do not allow for sets of pydantic models as they are not hashable, so + # if any of the values of the set have a type_expectation of a dict, we're assuming it's a pydantic + # model and keeping it a list. + if container_expectation != "set" or not any( + map( + lambda value: isinstance(value, dict), + list(contents_expectation.values()), + ) + ): + json_expectation = cast_field(json_expectation, container_expectation) + elif isinstance(type_expectation, tuple): + container_expectation = type_expectation[0] + contents_expectation = type_expectation[1] + if isinstance(contents_expectation, dict): + json_expectation = { + cast_field( + key, + contents_expectation.get(idx)[0] # type: ignore + if contents_expectation.get(idx) is not None + else None, + ): cast_field( + value, + contents_expectation.get(idx)[1] # type: ignore + if contents_expectation.get(idx) is not None + else None, + ) + for idx, (key, value) in enumerate(json_expectation.items()) + } + else: + json_expectation = cast_field(json_expectation, container_expectation) + elif type_expectation is not None: + json_expectation = cast_field(json_expectation, type_expectation) + + # When dealing with containers of models, etc. we're validating them implicitly, so no need to check the resultant list + if not is_container_of_complex_type: + assert ( + json_expectation == response + ), "Primitives found, expected: {0} (type: {1}), Actual: {2} (type: {3})".format( + json_expectation, type(json_expectation), response, type(response) + ) + + +# Arg type_expectations is a deeply nested structure that matches the response, but with the values replaced with the expected types +def validate_response(response: typing.Any, json_expectation: typing.Any, type_expectations: typing.Any) -> None: + # Allow for an escape hatch if the object cannot be validated + if type_expectations == "no_validate": + return + + if ( + not isinstance(response, list) + and not isinstance(response, dict) + and not issubclass(type(response), pydantic.BaseModel) + ): + validate_field( + response=response, + json_expectation=json_expectation, + type_expectation=type_expectations, + ) + return + + if isinstance(response, list): + assert len(response) == len(json_expectation), "Length mismatch, expected: {0}, Actual: {1}".format( + len(response), len(json_expectation) + ) + content_expectation = type_expectations + if isinstance(type_expectations, tuple): + content_expectation = type_expectations[1] + for idx, item in enumerate(response): + validate_response( + response=item, + json_expectation=json_expectation[idx], + type_expectations=content_expectation[idx], + ) + else: + response_json = response + if issubclass(type(response), pydantic.BaseModel): + response_json = response.dict(by_alias=True) + + for key, value in json_expectation.items(): + assert key in response_json, "Field {0} not found within the response object: {1}".format( + key, response_json + ) + + type_expectation = None + if type_expectations is not None and isinstance(type_expectations, dict): + type_expectation = type_expectations.get(key) + + # If your type_expectation is a tuple then you have a container field, process it as such + # Otherwise, we're just validating a single field that's a pydantic model. + if isinstance(value, dict) and not isinstance(type_expectation, tuple): + validate_response( + response=response_json[key], + json_expectation=value, + type_expectations=type_expectation, + ) + else: + validate_field( + response=response_json[key], + json_expectation=value, + type_expectation=type_expectation, + ) + + # Ensure there are no additional fields here either + del response_json[key] + assert len(response_json) == 0, "Additional fields found, expected None: {0}".format(response_json) diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/__init__.py b/seed/python-sdk/mixed-file-directory/tests/utils/__init__.py new file mode 100644 index 00000000000..f3ea2659bb1 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/__init__.py b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/__init__.py new file mode 100644 index 00000000000..3a1c852e71e --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/__init__.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from .circle import CircleParams +from .object_with_defaults import ObjectWithDefaultsParams +from .object_with_optional_field import ObjectWithOptionalFieldParams +from .shape import ShapeParams, Shape_CircleParams, Shape_SquareParams +from .square import SquareParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +__all__ = [ + "CircleParams", + "ObjectWithDefaultsParams", + "ObjectWithOptionalFieldParams", + "ShapeParams", + "Shape_CircleParams", + "Shape_SquareParams", + "SquareParams", + "UndiscriminatedShapeParams", +] diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/circle.py b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/circle.py new file mode 100644 index 00000000000..6522dc58c5a --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/circle.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +import typing_extensions +from seed.core.serialization import FieldMetadata + + +class CircleParams(typing_extensions.TypedDict): + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/color.py b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/color.py new file mode 100644 index 00000000000..2aa2c4c52f0 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/color.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +Color = typing.Union[typing.Literal["red", "blue"], typing.Any] diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/object_with_defaults.py b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/object_with_defaults.py new file mode 100644 index 00000000000..ef14f7b2c9d --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/object_with_defaults.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +import typing_extensions + + +class ObjectWithDefaultsParams(typing_extensions.TypedDict): + """ + Defines properties with default values and validation rules. + """ + + decimal: typing_extensions.NotRequired[float] + string: typing_extensions.NotRequired[str] + required_string: str diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/object_with_optional_field.py b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/object_with_optional_field.py new file mode 100644 index 00000000000..fc5a379b967 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/object_with_optional_field.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +import typing +import typing_extensions +from seed.core.serialization import FieldMetadata +import datetime as dt +import uuid +from .color import Color +from .shape import ShapeParams +from .undiscriminated_shape import UndiscriminatedShapeParams + + +class ObjectWithOptionalFieldParams(typing_extensions.TypedDict): + literal: typing.Literal["lit_one"] + string: typing_extensions.NotRequired[str] + integer: typing_extensions.NotRequired[int] + long_: typing_extensions.NotRequired[typing_extensions.Annotated[int, FieldMetadata(alias="long")]] + double: typing_extensions.NotRequired[float] + bool_: typing_extensions.NotRequired[typing_extensions.Annotated[bool, FieldMetadata(alias="bool")]] + datetime: typing_extensions.NotRequired[dt.datetime] + date: typing_extensions.NotRequired[dt.date] + uuid_: typing_extensions.NotRequired[typing_extensions.Annotated[uuid.UUID, FieldMetadata(alias="uuid")]] + base_64: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="base64")]] + list_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Sequence[str], FieldMetadata(alias="list")]] + set_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Set[str], FieldMetadata(alias="set")]] + map_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Dict[int, str], FieldMetadata(alias="map")]] + enum: typing_extensions.NotRequired[Color] + union: typing_extensions.NotRequired[ShapeParams] + second_union: typing_extensions.NotRequired[ShapeParams] + undiscriminated_union: typing_extensions.NotRequired[UndiscriminatedShapeParams] + any: typing.Optional[typing.Any] diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/shape.py b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/shape.py new file mode 100644 index 00000000000..ae113ae0609 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/shape.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations +import typing_extensions +import typing_extensions +import typing +from seed.core.serialization import FieldMetadata + + +class Base(typing_extensions.TypedDict): + id: str + + +class Shape_CircleParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["circle"], FieldMetadata(alias="shapeType")] + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] + + +class Shape_SquareParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["square"], FieldMetadata(alias="shapeType")] + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] + + +ShapeParams = typing.Union[Shape_CircleParams, Shape_SquareParams] diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/square.py b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/square.py new file mode 100644 index 00000000000..7f6f79a3ddc --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/square.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +import typing_extensions +from seed.core.serialization import FieldMetadata + + +class SquareParams(typing_extensions.TypedDict): + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/undiscriminated_shape.py b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/undiscriminated_shape.py new file mode 100644 index 00000000000..68876a23c38 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/assets/models/undiscriminated_shape.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing +from .circle import CircleParams +from .square import SquareParams + +UndiscriminatedShapeParams = typing.Union[CircleParams, SquareParams] diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/test_http_client.py b/seed/python-sdk/mixed-file-directory/tests/utils/test_http_client.py new file mode 100644 index 00000000000..a541bae6531 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/test_http_client.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed.core.http_client import get_request_body +from seed.core.request_options import RequestOptions + + +def get_request_options() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later"}} + + +def test_get_json_request_body() -> None: + json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) + assert json_body == {"hello": "world"} + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={"goodbye": "world"}, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"goodbye": "world", "see you": "later"} + assert data_body_extras is None + + +def test_get_files_request_body() -> None: + json_body, data_body = get_request_body(json=None, data={"hello": "world"}, request_options=None, omit=None) + assert data_body == {"hello": "world"} + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data={"goodbye": "world"}, request_options=get_request_options(), omit=None + ) + + assert data_body_extras == {"goodbye": "world", "see you": "later"} + assert json_body_extras is None + + +def test_get_none_request_body() -> None: + json_body, data_body = get_request_body(json=None, data=None, request_options=None, omit=None) + assert data_body is None + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"see you": "later"} + assert data_body_extras is None + + +def test_get_empty_json_request_body() -> None: + unrelated_request_options: RequestOptions = {"max_retries": 3} + json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None) + assert json_body is None + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={}, data=None, request_options=unrelated_request_options, omit=None + ) + + assert json_body_extras is None + assert data_body_extras is None diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/test_query_encoding.py b/seed/python-sdk/mixed-file-directory/tests/utils/test_query_encoding.py new file mode 100644 index 00000000000..e075394a502 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/test_query_encoding.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + + +from seed.core.query_encoder import encode_query + + +def test_query_encoding_deep_objects() -> None: + assert encode_query({"hello world": "hello world"}) == [("hello world", "hello world")] + assert encode_query({"hello_world": {"hello": "world"}}) == [("hello_world[hello]", "world")] + assert encode_query({"hello_world": {"hello": {"world": "today"}, "test": "this"}, "hi": "there"}) == [ + ("hello_world[hello][world]", "today"), + ("hello_world[test]", "this"), + ("hi", "there"), + ] + + +def test_query_encoding_deep_object_arrays() -> None: + assert encode_query({"objects": [{"key": "hello", "value": "world"}, {"key": "foo", "value": "bar"}]}) == [ + ("objects[key]", "hello"), + ("objects[value]", "world"), + ("objects[key]", "foo"), + ("objects[value]", "bar"), + ] + assert encode_query( + {"users": [{"name": "string", "tags": ["string"]}, {"name": "string2", "tags": ["string2", "string3"]}]} + ) == [ + ("users[name]", "string"), + ("users[tags]", "string"), + ("users[name]", "string2"), + ("users[tags]", "string2"), + ("users[tags]", "string3"), + ] + + +def test_encode_query_with_none() -> None: + encoded = encode_query(None) + assert encoded == None diff --git a/seed/python-sdk/mixed-file-directory/tests/utils/test_serialization.py b/seed/python-sdk/mixed-file-directory/tests/utils/test_serialization.py new file mode 100644 index 00000000000..dd74836366c --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/tests/utils/test_serialization.py @@ -0,0 +1,72 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Any + +from seed.core.serialization import convert_and_respect_annotation_metadata +from .assets.models import ShapeParams, ObjectWithOptionalFieldParams + + +UNION_TEST: ShapeParams = {"radius_measurement": 1.0, "shape_type": "circle", "id": "1"} +UNION_TEST_CONVERTED = {"shapeType": "circle", "radiusMeasurement": 1.0, "id": "1"} + + +def test_convert_and_respect_annotation_metadata() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "bool_": True, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + assert converted == {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"} + + +def test_convert_and_respect_annotation_metadata_in_list() -> None: + data: List[ObjectWithOptionalFieldParams] = [ + {"string": "string", "long_": 12345, "bool_": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long_": 67890, "list_": [], "literal": "lit_one", "any": "any"}, + ] + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=List[ObjectWithOptionalFieldParams], direction="write" + ) + + assert converted == [ + {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long": 67890, "list": [], "literal": "lit_one", "any": "any"}, + ] + + +def test_convert_and_respect_annotation_metadata_in_nested_object() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "union": UNION_TEST, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + + assert converted == { + "string": "string", + "long": 12345, + "union": UNION_TEST_CONVERTED, + "literal": "lit_one", + "any": "any", + } + + +def test_convert_and_respect_annotation_metadata_in_union() -> None: + converted = convert_and_respect_annotation_metadata(object_=UNION_TEST, annotation=ShapeParams, direction="write") + + assert converted == UNION_TEST_CONVERTED + + +def test_convert_and_respect_annotation_metadata_with_empty_object() -> None: + data: Any = {} + converted = convert_and_respect_annotation_metadata(object_=data, annotation=ShapeParams, direction="write") + assert converted == data diff --git a/seed/ruby-model/mixed-file-directory/.mock/definition/__package__.yml b/seed/ruby-model/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/ruby-model/mixed-file-directory/.mock/definition/api.yml b/seed/ruby-model/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/ruby-model/mixed-file-directory/.mock/definition/organization.yml b/seed/ruby-model/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/ruby-model/mixed-file-directory/.mock/definition/user.yml b/seed/ruby-model/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/ruby-model/mixed-file-directory/.mock/definition/user/events.yml b/seed/ruby-model/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/ruby-model/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/ruby-model/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/ruby-model/mixed-file-directory/.mock/fern.config.json b/seed/ruby-model/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/ruby-model/mixed-file-directory/.mock/generators.yml b/seed/ruby-model/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/ruby-model/mixed-file-directory/.rubocop.yml b/seed/ruby-model/mixed-file-directory/.rubocop.yml new file mode 100644 index 00000000000..c1d2344d6e6 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/.rubocop.yml @@ -0,0 +1,36 @@ +AllCops: + TargetRubyVersion: 2.7 + +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + Enabled: true + EnforcedStyle: double_quotes + +Layout/FirstHashElementLineBreak: + Enabled: true + +Layout/MultilineHashKeyLineBreaks: + Enabled: true + +# Generated files may be more complex than standard, disable +# these rules for now as a known limitation. +Metrics/ParameterLists: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false diff --git a/seed/ruby-model/mixed-file-directory/Gemfile b/seed/ruby-model/mixed-file-directory/Gemfile new file mode 100644 index 00000000000..49bd9cd0173 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "minitest", "~> 5.0" +gem "rake", "~> 13.0" +gem "rubocop", "~> 1.21" diff --git a/seed/ruby-model/mixed-file-directory/Rakefile b/seed/ruby-model/mixed-file-directory/Rakefile new file mode 100644 index 00000000000..2bdbce0cf2c --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "rake/testtask" +require "rubocop/rake_task" + +task default: %i[test rubocop] + +Rake::TestTask.new do |t| + t.pattern = "./test/**/test_*.rb" +end + +RuboCop::RakeTask.new diff --git a/seed/ruby-model/mixed-file-directory/lib/gemconfig.rb b/seed/ruby-model/mixed-file-directory/lib/gemconfig.rb new file mode 100644 index 00000000000..ed3b846962d --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/lib/gemconfig.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module SeedMixedFileDirectoryClient + module Gemconfig + VERSION = "" + AUTHORS = [""].freeze + EMAIL = "" + SUMMARY = "" + DESCRIPTION = "" + HOMEPAGE = "https://github.com/REPO/URL" + SOURCE_CODE_URI = "https://github.com/REPO/URL" + CHANGELOG_URI = "https://github.com/REPO/URL/blob/master/CHANGELOG.md" + end +end diff --git a/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client.rb b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client.rb new file mode 100644 index 00000000000..843b682b991 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_relative "seed_mixed_file_directory_client/organization/types/organization" +require_relative "seed_mixed_file_directory_client/organization/types/create_organization_request" +require_relative "seed_mixed_file_directory_client/user/types/user" +require_relative "seed_mixed_file_directory_client/user/events/types/event" +require_relative "seed_mixed_file_directory_client/user/events/metadata/types/metadata" diff --git a/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/organization/types/create_organization_request.rb b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/organization/types/create_organization_request.rb new file mode 100644 index 00000000000..f9b7f9463af --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/organization/types/create_organization_request.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + class Organization + class CreateOrganizationRequest + # @return [String] + attr_reader :name + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param name [String] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::Organization::CreateOrganizationRequest] + def initialize(name:, additional_properties: nil) + @name = name + @additional_properties = additional_properties + @_field_set = { "name": name } + end + + # Deserialize a JSON object to an instance of CreateOrganizationRequest + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::Organization::CreateOrganizationRequest] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + name = parsed_json["name"] + new(name: name, additional_properties: struct) + end + + # Serialize an instance of CreateOrganizationRequest to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.name.is_a?(String) != false || raise("Passed value for field obj.name is not the expected type, validation failed.") + end + end + end +end diff --git a/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/organization/types/organization.rb b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/organization/types/organization.rb new file mode 100644 index 00000000000..59379842c25 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/organization/types/organization.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require_relative "../../user/types/user" +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + class Organization + class Organization + # @return [String] + attr_reader :id + # @return [String] + attr_reader :name + # @return [Array] + attr_reader :users + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param id [String] + # @param name [String] + # @param users [Array] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::Organization::Organization] + def initialize(id:, name:, users:, additional_properties: nil) + @id = id + @name = name + @users = users + @additional_properties = additional_properties + @_field_set = { "id": id, "name": name, "users": users } + end + + # Deserialize a JSON object to an instance of Organization + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::Organization::Organization] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + id = parsed_json["id"] + name = parsed_json["name"] + users = parsed_json["users"]&.map do |item| + item = item.to_json + SeedMixedFileDirectoryClient::User::User.from_json(json_object: item) + end + new( + id: id, + name: name, + users: users, + additional_properties: struct + ) + end + + # Serialize an instance of Organization to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.id.is_a?(String) != false || raise("Passed value for field obj.id is not the expected type, validation failed.") + obj.name.is_a?(String) != false || raise("Passed value for field obj.name is not the expected type, validation failed.") + obj.users.is_a?(Array) != false || raise("Passed value for field obj.users is not the expected type, validation failed.") + end + end + end +end diff --git a/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/events/metadata/types/metadata.rb b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/events/metadata/types/metadata.rb new file mode 100644 index 00000000000..e5378a17a60 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/events/metadata/types/metadata.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + module User + module Events + class Metadata + class Metadata + # @return [String] + attr_reader :id + # @return [Object] + attr_reader :value + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param id [String] + # @param value [Object] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::User::Events::Metadata::Metadata] + def initialize(id:, value:, additional_properties: nil) + @id = id + @value = value + @additional_properties = additional_properties + @_field_set = { "id": id, "value": value } + end + + # Deserialize a JSON object to an instance of Metadata + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::User::Events::Metadata::Metadata] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + id = parsed_json["id"] + value = parsed_json["value"] + new( + id: id, + value: value, + additional_properties: struct + ) + end + + # Serialize an instance of Metadata to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.id.is_a?(String) != false || raise("Passed value for field obj.id is not the expected type, validation failed.") + obj.value.is_a?(Object) != false || raise("Passed value for field obj.value is not the expected type, validation failed.") + end + end + end + end + end +end diff --git a/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/events/types/event.rb b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/events/types/event.rb new file mode 100644 index 00000000000..41105c14656 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/events/types/event.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + module User + class Events + class Event + # @return [String] + attr_reader :id + # @return [String] + attr_reader :name + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param id [String] + # @param name [String] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::User::Events::Event] + def initialize(id:, name:, additional_properties: nil) + @id = id + @name = name + @additional_properties = additional_properties + @_field_set = { "id": id, "name": name } + end + + # Deserialize a JSON object to an instance of Event + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::User::Events::Event] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + id = parsed_json["id"] + name = parsed_json["name"] + new( + id: id, + name: name, + additional_properties: struct + ) + end + + # Serialize an instance of Event to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.id.is_a?(String) != false || raise("Passed value for field obj.id is not the expected type, validation failed.") + obj.name.is_a?(String) != false || raise("Passed value for field obj.name is not the expected type, validation failed.") + end + end + end + end +end diff --git a/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/types/user.rb b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/types/user.rb new file mode 100644 index 00000000000..62c58baafb8 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/lib/seed_mixed_file_directory_client/user/types/user.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + class User + class User + # @return [String] + attr_reader :id + # @return [String] + attr_reader :name + # @return [Integer] + attr_reader :age + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param id [String] + # @param name [String] + # @param age [Integer] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::User::User] + def initialize(id:, name:, age:, additional_properties: nil) + @id = id + @name = name + @age = age + @additional_properties = additional_properties + @_field_set = { "id": id, "name": name, "age": age } + end + + # Deserialize a JSON object to an instance of User + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::User::User] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + id = parsed_json["id"] + name = parsed_json["name"] + age = parsed_json["age"] + new( + id: id, + name: name, + age: age, + additional_properties: struct + ) + end + + # Serialize an instance of User to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.id.is_a?(String) != false || raise("Passed value for field obj.id is not the expected type, validation failed.") + obj.name.is_a?(String) != false || raise("Passed value for field obj.name is not the expected type, validation failed.") + obj.age.is_a?(Integer) != false || raise("Passed value for field obj.age is not the expected type, validation failed.") + end + end + end +end diff --git a/seed/ruby-model/mixed-file-directory/seed_mixed_file_directory_client.gemspec b/seed/ruby-model/mixed-file-directory/seed_mixed_file_directory_client.gemspec new file mode 100644 index 00000000000..8f838635490 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/seed_mixed_file_directory_client.gemspec @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "lib/gemconfig" + +Gem::Specification.new do |spec| + spec.name = "seed_mixed_file_directory_client" + spec.version = SeedMixedFileDirectoryClient::Gemconfig::VERSION + spec.authors = SeedMixedFileDirectoryClient::Gemconfig::AUTHORS + spec.email = SeedMixedFileDirectoryClient::Gemconfig::EMAIL + spec.summary = SeedMixedFileDirectoryClient::Gemconfig::SUMMARY + spec.description = SeedMixedFileDirectoryClient::Gemconfig::DESCRIPTION + spec.homepage = SeedMixedFileDirectoryClient::Gemconfig::HOMEPAGE + spec.required_ruby_version = ">= 2.7.0" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = SeedMixedFileDirectoryClient::Gemconfig::SOURCE_CODE_URI + spec.metadata["changelog_uri"] = SeedMixedFileDirectoryClient::Gemconfig::CHANGELOG_URI + spec.files = Dir.glob("lib/**/*") + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] +end diff --git a/seed/ruby-model/mixed-file-directory/snippet-templates.json b/seed/ruby-model/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/ruby-model/mixed-file-directory/snippet.json b/seed/ruby-model/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/ruby-model/mixed-file-directory/test/test_helper.rb b/seed/ruby-model/mixed-file-directory/test/test_helper.rb new file mode 100644 index 00000000000..23a444d1d00 --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/test/test_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) + +require "minitest/autorun" +require "seed_mixed_file_directory_client" diff --git a/seed/ruby-model/mixed-file-directory/test/test_seed_mixed_file_directory_client.rb b/seed/ruby-model/mixed-file-directory/test/test_seed_mixed_file_directory_client.rb new file mode 100644 index 00000000000..278eedcaabe --- /dev/null +++ b/seed/ruby-model/mixed-file-directory/test/test_seed_mixed_file_directory_client.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "seed_mixed_file_directory_client" + +# Basic SeedMixedFileDirectoryClient tests +class TestSeedMixedFileDirectoryClient < Minitest::Test + def test_function + # SeedMixedFileDirectoryClient::Client.new + end +end diff --git a/seed/ruby-sdk/literal/fern_literal.gemspec b/seed/ruby-sdk/literal/fern_literal.gemspec index 4ea948062d8..4cc0a0bc1b0 100644 --- a/seed/ruby-sdk/literal/fern_literal.gemspec +++ b/seed/ruby-sdk/literal/fern_literal.gemspec @@ -18,4 +18,8 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] + spec.add_dependency "async-http-faraday", ">= 0.0", "< 1.0" + spec.add_dependency "faraday", ">= 1.10", "< 3.0" + spec.add_dependency "faraday-net_http", ">= 1.0", "< 4.0" + spec.add_dependency "faraday-retry", ">= 1.0", "< 3.0" end diff --git a/seed/ruby-sdk/literal/lib/fern_literal.rb b/seed/ruby-sdk/literal/lib/fern_literal.rb index 1f5189a4019..5f686514ee7 100644 --- a/seed/ruby-sdk/literal/lib/fern_literal.rb +++ b/seed/ruby-sdk/literal/lib/fern_literal.rb @@ -1,8 +1,79 @@ # frozen_string_literal: true -require_relative "fern_literal/types/send_response" -require_relative "fern_literal/inlined/types/some_aliased_literal" -require_relative "fern_literal/inlined/types/a_top_level_literal" -require_relative "fern_literal/inlined/types/a_nested_literal" -require_relative "fern_literal/reference/types/send_request" -require_relative "fern_literal/reference/types/some_literal" +require_relative "types_export" +require_relative "requests" +require_relative "fern_literal/headers/client" +require_relative "fern_literal/inlined/client" +require_relative "fern_literal/path/client" +require_relative "fern_literal/query/client" +require_relative "fern_literal/reference/client" + +module SeedLiteralClient + class Client + # @return [SeedLiteralClient::HeadersClient] + attr_reader :headers + # @return [SeedLiteralClient::InlinedClient] + attr_reader :inlined + # @return [SeedLiteralClient::PathClient] + attr_reader :path + # @return [SeedLiteralClient::QueryClient] + attr_reader :query + # @return [SeedLiteralClient::ReferenceClient] + attr_reader :reference + + # @param base_url [String] + # @param max_retries [Long] The number of times to retry a failed request, defaults to 2. + # @param timeout_in_seconds [Long] + # @param version [String] + # @param audit_logging [Boolean] + # @return [SeedLiteralClient::Client] + def initialize(version:, audit_logging:, base_url: nil, max_retries: nil, timeout_in_seconds: nil) + @request_client = SeedLiteralClient::RequestClient.new( + base_url: base_url, + max_retries: max_retries, + timeout_in_seconds: timeout_in_seconds, + version: version, + audit_logging: audit_logging + ) + @headers = SeedLiteralClient::HeadersClient.new(request_client: @request_client) + @inlined = SeedLiteralClient::InlinedClient.new(request_client: @request_client) + @path = SeedLiteralClient::PathClient.new(request_client: @request_client) + @query = SeedLiteralClient::QueryClient.new(request_client: @request_client) + @reference = SeedLiteralClient::ReferenceClient.new(request_client: @request_client) + end + end + + class AsyncClient + # @return [SeedLiteralClient::AsyncHeadersClient] + attr_reader :headers + # @return [SeedLiteralClient::AsyncInlinedClient] + attr_reader :inlined + # @return [SeedLiteralClient::AsyncPathClient] + attr_reader :path + # @return [SeedLiteralClient::AsyncQueryClient] + attr_reader :query + # @return [SeedLiteralClient::AsyncReferenceClient] + attr_reader :reference + + # @param base_url [String] + # @param max_retries [Long] The number of times to retry a failed request, defaults to 2. + # @param timeout_in_seconds [Long] + # @param version [String] + # @param audit_logging [Boolean] + # @return [SeedLiteralClient::AsyncClient] + def initialize(version:, audit_logging:, base_url: nil, max_retries: nil, timeout_in_seconds: nil) + @async_request_client = SeedLiteralClient::AsyncRequestClient.new( + base_url: base_url, + max_retries: max_retries, + timeout_in_seconds: timeout_in_seconds, + version: version, + audit_logging: audit_logging + ) + @headers = SeedLiteralClient::AsyncHeadersClient.new(request_client: @async_request_client) + @inlined = SeedLiteralClient::AsyncInlinedClient.new(request_client: @async_request_client) + @path = SeedLiteralClient::AsyncPathClient.new(request_client: @async_request_client) + @query = SeedLiteralClient::AsyncQueryClient.new(request_client: @async_request_client) + @reference = SeedLiteralClient::AsyncReferenceClient.new(request_client: @async_request_client) + end + end +end diff --git a/seed/ruby-sdk/literal/lib/fern_literal/headers/client.rb b/seed/ruby-sdk/literal/lib/fern_literal/headers/client.rb new file mode 100644 index 00000000000..7e85851d2aa --- /dev/null +++ b/seed/ruby-sdk/literal/lib/fern_literal/headers/client.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require_relative "../../requests" +require_relative "../types/send_response" +require "async" + +module SeedLiteralClient + class HeadersClient + # @return [SeedLiteralClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::RequestClient] + # @return [SeedLiteralClient::HeadersClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param query [String] + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.headers.send(query: "What is the weather today") + def send(query:, request_options: nil) + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}), + "X-Endpoint-Version": "02-12-2024", + "X-Async": true + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + req.body = { **(request_options&.additional_body_parameters || {}), query: query }.compact + req.url "#{@request_client.get_url(request_options: request_options)}/headers" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + + class AsyncHeadersClient + # @return [SeedLiteralClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::AsyncRequestClient] + # @return [SeedLiteralClient::AsyncHeadersClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param query [String] + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.headers.send(query: "What is the weather today") + def send(query:, request_options: nil) + Async do + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}), + "X-Endpoint-Version": "02-12-2024", + "X-Async": true + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + req.body = { **(request_options&.additional_body_parameters || {}), query: query }.compact + req.url "#{@request_client.get_url(request_options: request_options)}/headers" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + end +end diff --git a/seed/ruby-sdk/literal/lib/fern_literal/inlined/client.rb b/seed/ruby-sdk/literal/lib/fern_literal/inlined/client.rb new file mode 100644 index 00000000000..6fbd42f6e74 --- /dev/null +++ b/seed/ruby-sdk/literal/lib/fern_literal/inlined/client.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require_relative "../../requests" +require_relative "types/some_aliased_literal" +require_relative "types/a_top_level_literal" +require_relative "../types/send_response" +require "async" + +module SeedLiteralClient + class InlinedClient + # @return [SeedLiteralClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::RequestClient] + # @return [SeedLiteralClient::InlinedClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param query [String] + # @param temperature [Float] + # @param aliased_context [SeedLiteralClient::Inlined::SOME_ALIASED_LITERAL] + # @param maybe_context [SeedLiteralClient::Inlined::SOME_ALIASED_LITERAL] + # @param object_with_literal [Hash] Request of type SeedLiteralClient::Inlined::ATopLevelLiteral, as a Hash + # * :nested_literal (Hash) + # * :my_literal (String) + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.inlined.send( + # query: "What is the weather today", + # temperature: 10.1, + # aliased_context: "You're super wise", + # maybe_context: "You're super wise", + # object_with_literal: { nested_literal: { my_literal: "How super cool" } } + # ) + def send(query:, aliased_context:, object_with_literal:, temperature: nil, maybe_context: nil, request_options: nil) + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + req.body = { + **(request_options&.additional_body_parameters || {}), + "prompt": "You are a helpful assistant", + "context": "You're super wise", + "stream": false, + query: query, + temperature: temperature, + aliasedContext: aliased_context, + maybeContext: maybe_context, + objectWithLiteral: object_with_literal + }.compact + req.url "#{@request_client.get_url(request_options: request_options)}/inlined" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + + class AsyncInlinedClient + # @return [SeedLiteralClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::AsyncRequestClient] + # @return [SeedLiteralClient::AsyncInlinedClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param query [String] + # @param temperature [Float] + # @param aliased_context [SeedLiteralClient::Inlined::SOME_ALIASED_LITERAL] + # @param maybe_context [SeedLiteralClient::Inlined::SOME_ALIASED_LITERAL] + # @param object_with_literal [Hash] Request of type SeedLiteralClient::Inlined::ATopLevelLiteral, as a Hash + # * :nested_literal (Hash) + # * :my_literal (String) + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.inlined.send( + # query: "What is the weather today", + # temperature: 10.1, + # aliased_context: "You're super wise", + # maybe_context: "You're super wise", + # object_with_literal: { nested_literal: { my_literal: "How super cool" } } + # ) + def send(query:, aliased_context:, object_with_literal:, temperature: nil, maybe_context: nil, request_options: nil) + Async do + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + req.body = { + **(request_options&.additional_body_parameters || {}), + "prompt": "You are a helpful assistant", + "context": "You're super wise", + "stream": false, + query: query, + temperature: temperature, + aliasedContext: aliased_context, + maybeContext: maybe_context, + objectWithLiteral: object_with_literal + }.compact + req.url "#{@request_client.get_url(request_options: request_options)}/inlined" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + end +end diff --git a/seed/ruby-sdk/literal/lib/fern_literal/path/client.rb b/seed/ruby-sdk/literal/lib/fern_literal/path/client.rb new file mode 100644 index 00000000000..22be836cae7 --- /dev/null +++ b/seed/ruby-sdk/literal/lib/fern_literal/path/client.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require_relative "../../requests" +require_relative "../types/send_response" +require "async" + +module SeedLiteralClient + class PathClient + # @return [SeedLiteralClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::RequestClient] + # @return [SeedLiteralClient::PathClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.path.send + def send(request_options: nil) + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/path/#{id}" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + + class AsyncPathClient + # @return [SeedLiteralClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::AsyncRequestClient] + # @return [SeedLiteralClient::AsyncPathClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.path.send + def send(request_options: nil) + Async do + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/path/#{id}" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + end +end diff --git a/seed/ruby-sdk/literal/lib/fern_literal/query/client.rb b/seed/ruby-sdk/literal/lib/fern_literal/query/client.rb new file mode 100644 index 00000000000..90666d9c9db --- /dev/null +++ b/seed/ruby-sdk/literal/lib/fern_literal/query/client.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require_relative "../../requests" +require_relative "../types/send_response" +require "async" + +module SeedLiteralClient + class QueryClient + # @return [SeedLiteralClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::RequestClient] + # @return [SeedLiteralClient::QueryClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param query [String] + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.query.send(query: "What is the weather today") + def send(query:, request_options: nil) + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + req.params = { + **(request_options&.additional_query_parameters || {}), + "prompt": "You are a helpful assistant", + "stream": false, + "query": query + }.compact + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/query" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + + class AsyncQueryClient + # @return [SeedLiteralClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::AsyncRequestClient] + # @return [SeedLiteralClient::AsyncQueryClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param query [String] + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.query.send(query: "What is the weather today") + def send(query:, request_options: nil) + Async do + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + req.params = { + **(request_options&.additional_query_parameters || {}), + "prompt": "You are a helpful assistant", + "stream": false, + "query": query + }.compact + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/query" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + end +end diff --git a/seed/ruby-sdk/literal/lib/fern_literal/reference/client.rb b/seed/ruby-sdk/literal/lib/fern_literal/reference/client.rb new file mode 100644 index 00000000000..4edf25fad17 --- /dev/null +++ b/seed/ruby-sdk/literal/lib/fern_literal/reference/client.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require_relative "../../requests" +require_relative "types/send_request" +require_relative "../types/send_response" +require "async" + +module SeedLiteralClient + class ReferenceClient + # @return [SeedLiteralClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::RequestClient] + # @return [SeedLiteralClient::ReferenceClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param request [Hash] Request of type SeedLiteralClient::Reference::SendRequest, as a Hash + # * :prompt (String) + # * :query (String) + # * :stream (Boolean) + # * :context (SeedLiteralClient::Reference::SOME_LITERAL) + # * :maybe_context (SeedLiteralClient::Reference::SOME_LITERAL) + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.reference.send(request: { prompt: "You are a helpful assistant", stream: false, context: "You're super wise", query: "What is the weather today" }) + def send(request:, request_options: nil) + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + req.body = { **(request || {}), **(request_options&.additional_body_parameters || {}) }.compact + req.url "#{@request_client.get_url(request_options: request_options)}/reference" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + + class AsyncReferenceClient + # @return [SeedLiteralClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedLiteralClient::AsyncRequestClient] + # @return [SeedLiteralClient::AsyncReferenceClient] + def initialize(request_client:) + @request_client = request_client + end + + # @param request [Hash] Request of type SeedLiteralClient::Reference::SendRequest, as a Hash + # * :prompt (String) + # * :query (String) + # * :stream (Boolean) + # * :context (SeedLiteralClient::Reference::SOME_LITERAL) + # * :maybe_context (SeedLiteralClient::Reference::SOME_LITERAL) + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [SeedLiteralClient::SendResponse] + # @example + # literal = SeedLiteralClient::Client.new( + # base_url: "https://api.example.com", + # version: "Version", + # audit_logging: "AuditLogging" + # ) + # literal.reference.send(request: { prompt: "You are a helpful assistant", stream: false, context: "You're super wise", query: "What is the weather today" }) + def send(request:, request_options: nil) + Async do + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers["X-API-Version"] = request_options.version unless request_options&.version.nil? + unless request_options&.audit_logging.nil? + req.headers["X-API-Enable-Audit-Logging"] = + request_options.audit_logging + end + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + req.body = { **(request || {}), **(request_options&.additional_body_parameters || {}) }.compact + req.url "#{@request_client.get_url(request_options: request_options)}/reference" + end + SeedLiteralClient::SendResponse.from_json(json_object: response.body) + end + end + end +end diff --git a/seed/ruby-sdk/literal/lib/requests.rb b/seed/ruby-sdk/literal/lib/requests.rb new file mode 100644 index 00000000000..78bcb4f9a52 --- /dev/null +++ b/seed/ruby-sdk/literal/lib/requests.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require "faraday" +require "faraday/retry" +require "async/http/faraday" + +module SeedLiteralClient + class RequestClient + # @return [Faraday] + attr_reader :conn + # @return [String] + attr_reader :base_url + + # @param base_url [String] + # @param max_retries [Long] The number of times to retry a failed request, defaults to 2. + # @param timeout_in_seconds [Long] + # @param version [String] + # @param audit_logging [Boolean] + # @return [SeedLiteralClient::RequestClient] + def initialize(version:, audit_logging:, base_url: nil, max_retries: nil, timeout_in_seconds: nil) + @base_url = base_url + @headers = {} + @headers["X-API-Version"] = version unless version.nil? + @headers["X-API-Enable-Audit-Logging"] = audit_logging unless audit_logging.nil? + @conn = Faraday.new(headers: @headers) do |faraday| + faraday.request :json + faraday.response :raise_error, include_request: true + faraday.request :retry, { max: max_retries } unless max_retries.nil? + faraday.options.timeout = timeout_in_seconds unless timeout_in_seconds.nil? + end + end + + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [String] + def get_url(request_options: nil) + request_options&.base_url || @base_url + end + + # @return [Hash{String => String}] + def get_headers + { "X-Fern-Language": "Ruby", "X-Fern-SDK-Name": "fern_literal", "X-Fern-SDK-Version": "0.0.1" } + end + end + + class AsyncRequestClient + # @return [Faraday] + attr_reader :conn + # @return [String] + attr_reader :base_url + + # @param base_url [String] + # @param max_retries [Long] The number of times to retry a failed request, defaults to 2. + # @param timeout_in_seconds [Long] + # @param version [String] + # @param audit_logging [Boolean] + # @return [SeedLiteralClient::AsyncRequestClient] + def initialize(version:, audit_logging:, base_url: nil, max_retries: nil, timeout_in_seconds: nil) + @base_url = base_url + @headers = {} + @headers["X-API-Version"] = version unless version.nil? + @headers["X-API-Enable-Audit-Logging"] = audit_logging unless audit_logging.nil? + @conn = Faraday.new(headers: @headers) do |faraday| + faraday.request :json + faraday.response :raise_error, include_request: true + faraday.adapter :async_http + faraday.request :retry, { max: max_retries } unless max_retries.nil? + faraday.options.timeout = timeout_in_seconds unless timeout_in_seconds.nil? + end + end + + # @param request_options [SeedLiteralClient::RequestOptions] + # @return [String] + def get_url(request_options: nil) + request_options&.base_url || @base_url + end + + # @return [Hash{String => String}] + def get_headers + { "X-Fern-Language": "Ruby", "X-Fern-SDK-Name": "fern_literal", "X-Fern-SDK-Version": "0.0.1" } + end + end + + # Additional options for request-specific configuration when calling APIs via the + # SDK. + class RequestOptions + # @return [String] + attr_reader :base_url + # @return [String] + attr_reader :version + # @return [Boolean] + attr_reader :audit_logging + # @return [Hash{String => Object}] + attr_reader :additional_headers + # @return [Hash{String => Object}] + attr_reader :additional_query_parameters + # @return [Hash{String => Object}] + attr_reader :additional_body_parameters + # @return [Long] + attr_reader :timeout_in_seconds + + # @param base_url [String] + # @param version [String] + # @param audit_logging [Boolean] + # @param additional_headers [Hash{String => Object}] + # @param additional_query_parameters [Hash{String => Object}] + # @param additional_body_parameters [Hash{String => Object}] + # @param timeout_in_seconds [Long] + # @return [SeedLiteralClient::RequestOptions] + def initialize(base_url: nil, version: nil, audit_logging: nil, additional_headers: nil, + additional_query_parameters: nil, additional_body_parameters: nil, timeout_in_seconds: nil) + @base_url = base_url + @version = version + @audit_logging = audit_logging + @additional_headers = additional_headers + @additional_query_parameters = additional_query_parameters + @additional_body_parameters = additional_body_parameters + @timeout_in_seconds = timeout_in_seconds + end + end + + # Additional options for request-specific configuration when calling APIs via the + # SDK. + class IdempotencyRequestOptions + # @return [String] + attr_reader :base_url + # @return [String] + attr_reader :version + # @return [Boolean] + attr_reader :audit_logging + # @return [Hash{String => Object}] + attr_reader :additional_headers + # @return [Hash{String => Object}] + attr_reader :additional_query_parameters + # @return [Hash{String => Object}] + attr_reader :additional_body_parameters + # @return [Long] + attr_reader :timeout_in_seconds + + # @param base_url [String] + # @param version [String] + # @param audit_logging [Boolean] + # @param additional_headers [Hash{String => Object}] + # @param additional_query_parameters [Hash{String => Object}] + # @param additional_body_parameters [Hash{String => Object}] + # @param timeout_in_seconds [Long] + # @return [SeedLiteralClient::IdempotencyRequestOptions] + def initialize(base_url: nil, version: nil, audit_logging: nil, additional_headers: nil, + additional_query_parameters: nil, additional_body_parameters: nil, timeout_in_seconds: nil) + @base_url = base_url + @version = version + @audit_logging = audit_logging + @additional_headers = additional_headers + @additional_query_parameters = additional_query_parameters + @additional_body_parameters = additional_body_parameters + @timeout_in_seconds = timeout_in_seconds + end + end +end diff --git a/seed/ruby-sdk/literal/lib/types_export.rb b/seed/ruby-sdk/literal/lib/types_export.rb new file mode 100644 index 00000000000..1f5189a4019 --- /dev/null +++ b/seed/ruby-sdk/literal/lib/types_export.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require_relative "fern_literal/types/send_response" +require_relative "fern_literal/inlined/types/some_aliased_literal" +require_relative "fern_literal/inlined/types/a_top_level_literal" +require_relative "fern_literal/inlined/types/a_nested_literal" +require_relative "fern_literal/reference/types/send_request" +require_relative "fern_literal/reference/types/some_literal" diff --git a/seed/ruby-sdk/literal/snippet.json b/seed/ruby-sdk/literal/snippet.json index e69de29bb2d..4f2f80a6605 100644 --- a/seed/ruby-sdk/literal/snippet.json +++ b/seed/ruby-sdk/literal/snippet.json @@ -0,0 +1,115 @@ +{ + "endpoints": [ + { + "id": { + "path": "/headers", + "method": "POST", + "identifierOverride": "endpoint_headers.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.headers.send(query: \"What is the weather today\")", + "type": "ruby" + } + }, + { + "id": { + "path": "/headers", + "method": "POST", + "identifierOverride": "endpoint_headers.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.headers.send(query: \"What is the weather today\")", + "type": "ruby" + } + }, + { + "id": { + "path": "/inlined", + "method": "POST", + "identifierOverride": "endpoint_inlined.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.inlined.send(\n query: \"What is the weather today\",\n temperature: 10.1,\n aliased_context: \"You're super wise\",\n maybe_context: \"You're super wise\",\n object_with_literal: { nested_literal: { my_literal: \"How super cool\" } }\n)", + "type": "ruby" + } + }, + { + "id": { + "path": "/inlined", + "method": "POST", + "identifierOverride": "endpoint_inlined.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.inlined.send(\n query: \"What is the weather today\",\n temperature: 10.1,\n aliased_context: \"You're super wise\",\n maybe_context: \"You're super wise\",\n object_with_literal: { nested_literal: { my_literal: \"How super cool\" } }\n)", + "type": "ruby" + } + }, + { + "id": { + "path": "/path/{id}", + "method": "POST", + "identifierOverride": "endpoint_path.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.path.send", + "type": "ruby" + } + }, + { + "id": { + "path": "/path/{id}", + "method": "POST", + "identifierOverride": "endpoint_path.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.path.send", + "type": "ruby" + } + }, + { + "id": { + "path": "/query", + "method": "POST", + "identifierOverride": "endpoint_query.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.query.send(query: \"What is the weather today\")", + "type": "ruby" + } + }, + { + "id": { + "path": "/query", + "method": "POST", + "identifierOverride": "endpoint_query.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.query.send(query: \"What is the weather today\")", + "type": "ruby" + } + }, + { + "id": { + "path": "/reference", + "method": "POST", + "identifierOverride": "endpoint_reference.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.reference.send(request: { prompt: \"You are a helpful assistant\", stream: false, context: \"You're super wise\", query: \"What is the weather today\" })", + "type": "ruby" + } + }, + { + "id": { + "path": "/reference", + "method": "POST", + "identifierOverride": "endpoint_reference.send" + }, + "snippet": { + "client": "require \"fern_literal\"\n\nliteral = SeedLiteralClient::Client.new(\n base_url: \"https://api.example.com\",\n version: \"Version\",\n audit_logging: \"AuditLogging\"\n)\nliteral.reference.send(request: { prompt: \"You are a helpful assistant\", stream: false, context: \"You're super wise\", query: \"What is the weather today\" })", + "type": "ruby" + } + } + ], + "types": {} +} \ No newline at end of file diff --git a/seed/ruby-sdk/mixed-file-directory/.github/workflows/publish.yml b/seed/ruby-sdk/mixed-file-directory/.github/workflows/publish.yml new file mode 100644 index 00000000000..ad47258c129 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +name: Publish + +on: [push] +jobs: + publish: + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + + - name: Test gem + run: bundle install && bundle exec rake test + + - name: Build and Push Gem + env: + GEM_HOST_API_KEY: ${{ secrets. }} + run: | + gem build fern_mixed_file_directory.gemspec + + gem push fern_mixed_file_directory-*.gem --host diff --git a/seed/ruby-sdk/mixed-file-directory/.gitignore b/seed/ruby-sdk/mixed-file-directory/.gitignore new file mode 100644 index 00000000000..a97c182a2e1 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.gitignore @@ -0,0 +1,10 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.gem +.env diff --git a/seed/ruby-sdk/mixed-file-directory/.mock/definition/__package__.yml b/seed/ruby-sdk/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/ruby-sdk/mixed-file-directory/.mock/definition/api.yml b/seed/ruby-sdk/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/ruby-sdk/mixed-file-directory/.mock/definition/organization.yml b/seed/ruby-sdk/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/ruby-sdk/mixed-file-directory/.mock/definition/user.yml b/seed/ruby-sdk/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/ruby-sdk/mixed-file-directory/.mock/definition/user/events.yml b/seed/ruby-sdk/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/ruby-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/ruby-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/ruby-sdk/mixed-file-directory/.mock/fern.config.json b/seed/ruby-sdk/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/ruby-sdk/mixed-file-directory/.mock/generators.yml b/seed/ruby-sdk/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/ruby-sdk/mixed-file-directory/.rubocop.yml b/seed/ruby-sdk/mixed-file-directory/.rubocop.yml new file mode 100644 index 00000000000..c1d2344d6e6 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/.rubocop.yml @@ -0,0 +1,36 @@ +AllCops: + TargetRubyVersion: 2.7 + +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + Enabled: true + EnforcedStyle: double_quotes + +Layout/FirstHashElementLineBreak: + Enabled: true + +Layout/MultilineHashKeyLineBreaks: + Enabled: true + +# Generated files may be more complex than standard, disable +# these rules for now as a known limitation. +Metrics/ParameterLists: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false diff --git a/seed/ruby-sdk/mixed-file-directory/Gemfile b/seed/ruby-sdk/mixed-file-directory/Gemfile new file mode 100644 index 00000000000..49bd9cd0173 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "minitest", "~> 5.0" +gem "rake", "~> 13.0" +gem "rubocop", "~> 1.21" diff --git a/seed/ruby-sdk/mixed-file-directory/README.md b/seed/ruby-sdk/mixed-file-directory/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/ruby-sdk/mixed-file-directory/Rakefile b/seed/ruby-sdk/mixed-file-directory/Rakefile new file mode 100644 index 00000000000..2bdbce0cf2c --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "rake/testtask" +require "rubocop/rake_task" + +task default: %i[test rubocop] + +Rake::TestTask.new do |t| + t.pattern = "./test/**/test_*.rb" +end + +RuboCop::RakeTask.new diff --git a/seed/ruby-sdk/mixed-file-directory/fern_mixed_file_directory.gemspec b/seed/ruby-sdk/mixed-file-directory/fern_mixed_file_directory.gemspec new file mode 100644 index 00000000000..9d24116f187 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/fern_mixed_file_directory.gemspec @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative "lib/gemconfig" + +Gem::Specification.new do |spec| + spec.name = "fern_mixed_file_directory" + spec.version = "0.0.1" + spec.authors = SeedMixedFileDirectoryClient::Gemconfig::AUTHORS + spec.email = SeedMixedFileDirectoryClient::Gemconfig::EMAIL + spec.summary = SeedMixedFileDirectoryClient::Gemconfig::SUMMARY + spec.description = SeedMixedFileDirectoryClient::Gemconfig::DESCRIPTION + spec.homepage = SeedMixedFileDirectoryClient::Gemconfig::HOMEPAGE + spec.required_ruby_version = ">= 2.7.0" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = SeedMixedFileDirectoryClient::Gemconfig::SOURCE_CODE_URI + spec.metadata["changelog_uri"] = SeedMixedFileDirectoryClient::Gemconfig::CHANGELOG_URI + spec.files = Dir.glob("lib/**/*") + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + spec.add_dependency "async-http-faraday", ">= 0.0", "< 1.0" + spec.add_dependency "faraday", ">= 1.10", "< 3.0" + spec.add_dependency "faraday-net_http", ">= 1.0", "< 4.0" + spec.add_dependency "faraday-retry", ">= 1.0", "< 3.0" +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory.rb new file mode 100644 index 00000000000..02f3b8f2c9c --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require_relative "types_export" +require_relative "requests" +require_relative "fern_mixed_file_directory/organization/client" +require_relative "fern_mixed_file_directory/user/client" + +module SeedMixedFileDirectoryClient + class Client + # @return [SeedMixedFileDirectoryClient::OrganizationClient] + attr_reader :organization + # @return [SeedMixedFileDirectoryClient::UserClient] + attr_reader :user + + # @param base_url [String] + # @param max_retries [Long] The number of times to retry a failed request, defaults to 2. + # @param timeout_in_seconds [Long] + # @return [SeedMixedFileDirectoryClient::Client] + def initialize(base_url: nil, max_retries: nil, timeout_in_seconds: nil) + @request_client = SeedMixedFileDirectoryClient::RequestClient.new( + base_url: base_url, + max_retries: max_retries, + timeout_in_seconds: timeout_in_seconds + ) + @organization = SeedMixedFileDirectoryClient::OrganizationClient.new(request_client: @request_client) + @user = SeedMixedFileDirectoryClient::UserClient.new(request_client: @request_client) + end + end + + class AsyncClient + # @return [SeedMixedFileDirectoryClient::AsyncOrganizationClient] + attr_reader :organization + # @return [SeedMixedFileDirectoryClient::AsyncUserClient] + attr_reader :user + + # @param base_url [String] + # @param max_retries [Long] The number of times to retry a failed request, defaults to 2. + # @param timeout_in_seconds [Long] + # @return [SeedMixedFileDirectoryClient::AsyncClient] + def initialize(base_url: nil, max_retries: nil, timeout_in_seconds: nil) + @async_request_client = SeedMixedFileDirectoryClient::AsyncRequestClient.new( + base_url: base_url, + max_retries: max_retries, + timeout_in_seconds: timeout_in_seconds + ) + @organization = SeedMixedFileDirectoryClient::AsyncOrganizationClient.new(request_client: @async_request_client) + @user = SeedMixedFileDirectoryClient::AsyncUserClient.new(request_client: @async_request_client) + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/client.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/client.rb new file mode 100644 index 00000000000..b81476b354a --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/client.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require_relative "../../requests" +require_relative "types/create_organization_request" +require_relative "types/organization" +require "async" + +module SeedMixedFileDirectoryClient + class OrganizationClient + # @return [SeedMixedFileDirectoryClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedMixedFileDirectoryClient::RequestClient] + # @return [SeedMixedFileDirectoryClient::OrganizationClient] + def initialize(request_client:) + @request_client = request_client + end + + # Create a new organization. + # + # @param request [Hash] Request of type SeedMixedFileDirectoryClient::Organization::CreateOrganizationRequest, as a Hash + # * :name (String) + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [SeedMixedFileDirectoryClient::Organization::Organization] + # @example + # mixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: "https://api.example.com") + # mixed_file_directory.organization.create(request: { name: "string" }) + def create(request:, request_options: nil) + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + req.body = { **(request || {}), **(request_options&.additional_body_parameters || {}) }.compact + req.url "#{@request_client.get_url(request_options: request_options)}/organizations" + end + SeedMixedFileDirectoryClient::Organization::Organization.from_json(json_object: response.body) + end + end + + class AsyncOrganizationClient + # @return [SeedMixedFileDirectoryClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedMixedFileDirectoryClient::AsyncRequestClient] + # @return [SeedMixedFileDirectoryClient::AsyncOrganizationClient] + def initialize(request_client:) + @request_client = request_client + end + + # Create a new organization. + # + # @param request [Hash] Request of type SeedMixedFileDirectoryClient::Organization::CreateOrganizationRequest, as a Hash + # * :name (String) + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [SeedMixedFileDirectoryClient::Organization::Organization] + # @example + # mixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: "https://api.example.com") + # mixed_file_directory.organization.create(request: { name: "string" }) + def create(request:, request_options: nil) + Async do + response = @request_client.conn.post do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + unless request_options.nil? || request_options&.additional_query_parameters.nil? + req.params = { **(request_options&.additional_query_parameters || {}) }.compact + end + req.body = { **(request || {}), **(request_options&.additional_body_parameters || {}) }.compact + req.url "#{@request_client.get_url(request_options: request_options)}/organizations" + end + SeedMixedFileDirectoryClient::Organization::Organization.from_json(json_object: response.body) + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/types/create_organization_request.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/types/create_organization_request.rb new file mode 100644 index 00000000000..f9b7f9463af --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/types/create_organization_request.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + class Organization + class CreateOrganizationRequest + # @return [String] + attr_reader :name + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param name [String] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::Organization::CreateOrganizationRequest] + def initialize(name:, additional_properties: nil) + @name = name + @additional_properties = additional_properties + @_field_set = { "name": name } + end + + # Deserialize a JSON object to an instance of CreateOrganizationRequest + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::Organization::CreateOrganizationRequest] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + name = parsed_json["name"] + new(name: name, additional_properties: struct) + end + + # Serialize an instance of CreateOrganizationRequest to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.name.is_a?(String) != false || raise("Passed value for field obj.name is not the expected type, validation failed.") + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/types/organization.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/types/organization.rb new file mode 100644 index 00000000000..59379842c25 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/organization/types/organization.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require_relative "../../user/types/user" +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + class Organization + class Organization + # @return [String] + attr_reader :id + # @return [String] + attr_reader :name + # @return [Array] + attr_reader :users + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param id [String] + # @param name [String] + # @param users [Array] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::Organization::Organization] + def initialize(id:, name:, users:, additional_properties: nil) + @id = id + @name = name + @users = users + @additional_properties = additional_properties + @_field_set = { "id": id, "name": name, "users": users } + end + + # Deserialize a JSON object to an instance of Organization + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::Organization::Organization] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + id = parsed_json["id"] + name = parsed_json["name"] + users = parsed_json["users"]&.map do |item| + item = item.to_json + SeedMixedFileDirectoryClient::User::User.from_json(json_object: item) + end + new( + id: id, + name: name, + users: users, + additional_properties: struct + ) + end + + # Serialize an instance of Organization to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.id.is_a?(String) != false || raise("Passed value for field obj.id is not the expected type, validation failed.") + obj.name.is_a?(String) != false || raise("Passed value for field obj.name is not the expected type, validation failed.") + obj.users.is_a?(Array) != false || raise("Passed value for field obj.users is not the expected type, validation failed.") + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/client.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/client.rb new file mode 100644 index 00000000000..fe048410bc3 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/client.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require_relative "../../requests" +require_relative "types/user" +require "json" +require "async" + +module SeedMixedFileDirectoryClient + class UserClient + # @return [SeedMixedFileDirectoryClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedMixedFileDirectoryClient::RequestClient] + # @return [SeedMixedFileDirectoryClient::UserClient] + def initialize(request_client:) + @request_client = request_client + end + + # List all users. + # + # @param limit [Integer] The maximum number of results to return. + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [Array] + # @example + # mixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: "https://api.example.com") + # mixed_file_directory.user.list(limit: 1) + def list(limit: nil, request_options: nil) + response = @request_client.conn.get do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + req.params = { **(request_options&.additional_query_parameters || {}), "limit": limit }.compact + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/users" + end + parsed_json = JSON.parse(response.body) + parsed_json&.map do |item| + item = item.to_json + SeedMixedFileDirectoryClient::User::User.from_json(json_object: item) + end + end + end + + class AsyncUserClient + # @return [SeedMixedFileDirectoryClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedMixedFileDirectoryClient::AsyncRequestClient] + # @return [SeedMixedFileDirectoryClient::AsyncUserClient] + def initialize(request_client:) + @request_client = request_client + end + + # List all users. + # + # @param limit [Integer] The maximum number of results to return. + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [Array] + # @example + # mixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: "https://api.example.com") + # mixed_file_directory.user.list(limit: 1) + def list(limit: nil, request_options: nil) + Async do + response = @request_client.conn.get do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + req.params = { **(request_options&.additional_query_parameters || {}), "limit": limit }.compact + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/users" + end + parsed_json = JSON.parse(response.body) + parsed_json&.map do |item| + item = item.to_json + SeedMixedFileDirectoryClient::User::User.from_json(json_object: item) + end + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/client.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/client.rb new file mode 100644 index 00000000000..135ec6e229c --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/client.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require_relative "../../../requests" +require_relative "types/event" +require "json" +require "async" + +module SeedMixedFileDirectoryClient + module User + class EventsClient + # @return [SeedMixedFileDirectoryClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedMixedFileDirectoryClient::RequestClient] + # @return [SeedMixedFileDirectoryClient::User::EventsClient] + def initialize(request_client:) + @request_client = request_client + end + + # List all user events. + # + # @param limit [Integer] The maximum number of results to return. + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [Array] + # @example + # mixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: "https://api.example.com") + # mixed_file_directory.user.events.list_events(limit: 1) + def list_events(limit: nil, request_options: nil) + response = @request_client.conn.get do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + req.params = { **(request_options&.additional_query_parameters || {}), "limit": limit }.compact + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/users/events" + end + parsed_json = JSON.parse(response.body) + parsed_json&.map do |item| + item = item.to_json + SeedMixedFileDirectoryClient::User::Events::Event.from_json(json_object: item) + end + end + end + + class AsyncEventsClient + # @return [SeedMixedFileDirectoryClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedMixedFileDirectoryClient::AsyncRequestClient] + # @return [SeedMixedFileDirectoryClient::User::AsyncEventsClient] + def initialize(request_client:) + @request_client = request_client + end + + # List all user events. + # + # @param limit [Integer] The maximum number of results to return. + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [Array] + # @example + # mixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: "https://api.example.com") + # mixed_file_directory.user.events.list_events(limit: 1) + def list_events(limit: nil, request_options: nil) + Async do + response = @request_client.conn.get do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + req.params = { **(request_options&.additional_query_parameters || {}), "limit": limit }.compact + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/users/events" + end + parsed_json = JSON.parse(response.body) + parsed_json&.map do |item| + item = item.to_json + SeedMixedFileDirectoryClient::User::Events::Event.from_json(json_object: item) + end + end + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/metadata/client.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/metadata/client.rb new file mode 100644 index 00000000000..82fc8d568e0 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/metadata/client.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require_relative "../../../../requests" +require_relative "types/metadata" +require "async" + +module SeedMixedFileDirectoryClient + module User + module Events + class MetadataClient + # @return [SeedMixedFileDirectoryClient::RequestClient] + attr_reader :request_client + + # @param request_client [SeedMixedFileDirectoryClient::RequestClient] + # @return [SeedMixedFileDirectoryClient::User::Events::MetadataClient] + def initialize(request_client:) + @request_client = request_client + end + + # Get event metadata. + # + # @param id [String] + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [SeedMixedFileDirectoryClient::User::Events::Metadata::Metadata] + # @example + # mixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: "https://api.example.com") + # mixed_file_directory.user.events.metadata.get_metadata(id: "string") + def get_metadata(id:, request_options: nil) + response = @request_client.conn.get do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + req.params = { **(request_options&.additional_query_parameters || {}), "id": id }.compact + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/users/events/metadata" + end + SeedMixedFileDirectoryClient::User::Events::Metadata::Metadata.from_json(json_object: response.body) + end + end + + class AsyncMetadataClient + # @return [SeedMixedFileDirectoryClient::AsyncRequestClient] + attr_reader :request_client + + # @param request_client [SeedMixedFileDirectoryClient::AsyncRequestClient] + # @return [SeedMixedFileDirectoryClient::User::Events::AsyncMetadataClient] + def initialize(request_client:) + @request_client = request_client + end + + # Get event metadata. + # + # @param id [String] + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [SeedMixedFileDirectoryClient::User::Events::Metadata::Metadata] + # @example + # mixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: "https://api.example.com") + # mixed_file_directory.user.events.metadata.get_metadata(id: "string") + def get_metadata(id:, request_options: nil) + Async do + response = @request_client.conn.get do |req| + req.options.timeout = request_options.timeout_in_seconds unless request_options&.timeout_in_seconds.nil? + req.headers = { + **(req.headers || {}), + **@request_client.get_headers, + **(request_options&.additional_headers || {}) + }.compact + req.params = { **(request_options&.additional_query_parameters || {}), "id": id }.compact + unless request_options.nil? || request_options&.additional_body_parameters.nil? + req.body = { **(request_options&.additional_body_parameters || {}) }.compact + end + req.url "#{@request_client.get_url(request_options: request_options)}/users/events/metadata" + end + SeedMixedFileDirectoryClient::User::Events::Metadata::Metadata.from_json(json_object: response.body) + end + end + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/metadata/types/metadata.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/metadata/types/metadata.rb new file mode 100644 index 00000000000..e5378a17a60 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/metadata/types/metadata.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + module User + module Events + class Metadata + class Metadata + # @return [String] + attr_reader :id + # @return [Object] + attr_reader :value + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param id [String] + # @param value [Object] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::User::Events::Metadata::Metadata] + def initialize(id:, value:, additional_properties: nil) + @id = id + @value = value + @additional_properties = additional_properties + @_field_set = { "id": id, "value": value } + end + + # Deserialize a JSON object to an instance of Metadata + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::User::Events::Metadata::Metadata] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + id = parsed_json["id"] + value = parsed_json["value"] + new( + id: id, + value: value, + additional_properties: struct + ) + end + + # Serialize an instance of Metadata to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.id.is_a?(String) != false || raise("Passed value for field obj.id is not the expected type, validation failed.") + obj.value.is_a?(Object) != false || raise("Passed value for field obj.value is not the expected type, validation failed.") + end + end + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/types/event.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/types/event.rb new file mode 100644 index 00000000000..41105c14656 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/events/types/event.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + module User + class Events + class Event + # @return [String] + attr_reader :id + # @return [String] + attr_reader :name + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param id [String] + # @param name [String] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::User::Events::Event] + def initialize(id:, name:, additional_properties: nil) + @id = id + @name = name + @additional_properties = additional_properties + @_field_set = { "id": id, "name": name } + end + + # Deserialize a JSON object to an instance of Event + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::User::Events::Event] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + id = parsed_json["id"] + name = parsed_json["name"] + new( + id: id, + name: name, + additional_properties: struct + ) + end + + # Serialize an instance of Event to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.id.is_a?(String) != false || raise("Passed value for field obj.id is not the expected type, validation failed.") + obj.name.is_a?(String) != false || raise("Passed value for field obj.name is not the expected type, validation failed.") + end + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/types/user.rb b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/types/user.rb new file mode 100644 index 00000000000..62c58baafb8 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/fern_mixed_file_directory/user/types/user.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "ostruct" +require "json" + +module SeedMixedFileDirectoryClient + class User + class User + # @return [String] + attr_reader :id + # @return [String] + attr_reader :name + # @return [Integer] + attr_reader :age + # @return [OpenStruct] Additional properties unmapped to the current class definition + attr_reader :additional_properties + # @return [Object] + attr_reader :_field_set + protected :_field_set + + OMIT = Object.new + + # @param id [String] + # @param name [String] + # @param age [Integer] + # @param additional_properties [OpenStruct] Additional properties unmapped to the current class definition + # @return [SeedMixedFileDirectoryClient::User::User] + def initialize(id:, name:, age:, additional_properties: nil) + @id = id + @name = name + @age = age + @additional_properties = additional_properties + @_field_set = { "id": id, "name": name, "age": age } + end + + # Deserialize a JSON object to an instance of User + # + # @param json_object [String] + # @return [SeedMixedFileDirectoryClient::User::User] + def self.from_json(json_object:) + struct = JSON.parse(json_object, object_class: OpenStruct) + parsed_json = JSON.parse(json_object) + id = parsed_json["id"] + name = parsed_json["name"] + age = parsed_json["age"] + new( + id: id, + name: name, + age: age, + additional_properties: struct + ) + end + + # Serialize an instance of User to a JSON object + # + # @return [String] + def to_json(*_args) + @_field_set&.to_json + end + + # Leveraged for Union-type generation, validate_raw attempts to parse the given + # hash and check each fields type against the current object's property + # definitions. + # + # @param obj [Object] + # @return [Void] + def self.validate_raw(obj:) + obj.id.is_a?(String) != false || raise("Passed value for field obj.id is not the expected type, validation failed.") + obj.name.is_a?(String) != false || raise("Passed value for field obj.name is not the expected type, validation failed.") + obj.age.is_a?(Integer) != false || raise("Passed value for field obj.age is not the expected type, validation failed.") + end + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/gemconfig.rb b/seed/ruby-sdk/mixed-file-directory/lib/gemconfig.rb new file mode 100644 index 00000000000..5704c781aba --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/gemconfig.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module SeedMixedFileDirectoryClient + module Gemconfig + VERSION = "" + AUTHORS = [""].freeze + EMAIL = "" + SUMMARY = "" + DESCRIPTION = "" + HOMEPAGE = "https://github.com/mixed-file-directory/fern" + SOURCE_CODE_URI = "https://github.com/mixed-file-directory/fern" + CHANGELOG_URI = "https://github.com/mixed-file-directory/fern/blob/master/CHANGELOG.md" + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/requests.rb b/seed/ruby-sdk/mixed-file-directory/lib/requests.rb new file mode 100644 index 00000000000..ef0e9dce9a6 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/requests.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require "faraday" +require "faraday/retry" +require "async/http/faraday" + +module SeedMixedFileDirectoryClient + class RequestClient + # @return [Faraday] + attr_reader :conn + # @return [String] + attr_reader :base_url + + # @param base_url [String] + # @param max_retries [Long] The number of times to retry a failed request, defaults to 2. + # @param timeout_in_seconds [Long] + # @return [SeedMixedFileDirectoryClient::RequestClient] + def initialize(base_url: nil, max_retries: nil, timeout_in_seconds: nil) + @base_url = base_url + @conn = Faraday.new do |faraday| + faraday.request :json + faraday.response :raise_error, include_request: true + faraday.request :retry, { max: max_retries } unless max_retries.nil? + faraday.options.timeout = timeout_in_seconds unless timeout_in_seconds.nil? + end + end + + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [String] + def get_url(request_options: nil) + request_options&.base_url || @base_url + end + + # @return [Hash{String => String}] + def get_headers + { + "X-Fern-Language": "Ruby", + "X-Fern-SDK-Name": "fern_mixed_file_directory", + "X-Fern-SDK-Version": "0.0.1" + } + end + end + + class AsyncRequestClient + # @return [Faraday] + attr_reader :conn + # @return [String] + attr_reader :base_url + + # @param base_url [String] + # @param max_retries [Long] The number of times to retry a failed request, defaults to 2. + # @param timeout_in_seconds [Long] + # @return [SeedMixedFileDirectoryClient::AsyncRequestClient] + def initialize(base_url: nil, max_retries: nil, timeout_in_seconds: nil) + @base_url = base_url + @conn = Faraday.new do |faraday| + faraday.request :json + faraday.response :raise_error, include_request: true + faraday.adapter :async_http + faraday.request :retry, { max: max_retries } unless max_retries.nil? + faraday.options.timeout = timeout_in_seconds unless timeout_in_seconds.nil? + end + end + + # @param request_options [SeedMixedFileDirectoryClient::RequestOptions] + # @return [String] + def get_url(request_options: nil) + request_options&.base_url || @base_url + end + + # @return [Hash{String => String}] + def get_headers + { + "X-Fern-Language": "Ruby", + "X-Fern-SDK-Name": "fern_mixed_file_directory", + "X-Fern-SDK-Version": "0.0.1" + } + end + end + + # Additional options for request-specific configuration when calling APIs via the + # SDK. + class RequestOptions + # @return [String] + attr_reader :base_url + # @return [Hash{String => Object}] + attr_reader :additional_headers + # @return [Hash{String => Object}] + attr_reader :additional_query_parameters + # @return [Hash{String => Object}] + attr_reader :additional_body_parameters + # @return [Long] + attr_reader :timeout_in_seconds + + # @param base_url [String] + # @param additional_headers [Hash{String => Object}] + # @param additional_query_parameters [Hash{String => Object}] + # @param additional_body_parameters [Hash{String => Object}] + # @param timeout_in_seconds [Long] + # @return [SeedMixedFileDirectoryClient::RequestOptions] + def initialize(base_url: nil, additional_headers: nil, additional_query_parameters: nil, + additional_body_parameters: nil, timeout_in_seconds: nil) + @base_url = base_url + @additional_headers = additional_headers + @additional_query_parameters = additional_query_parameters + @additional_body_parameters = additional_body_parameters + @timeout_in_seconds = timeout_in_seconds + end + end + + # Additional options for request-specific configuration when calling APIs via the + # SDK. + class IdempotencyRequestOptions + # @return [String] + attr_reader :base_url + # @return [Hash{String => Object}] + attr_reader :additional_headers + # @return [Hash{String => Object}] + attr_reader :additional_query_parameters + # @return [Hash{String => Object}] + attr_reader :additional_body_parameters + # @return [Long] + attr_reader :timeout_in_seconds + + # @param base_url [String] + # @param additional_headers [Hash{String => Object}] + # @param additional_query_parameters [Hash{String => Object}] + # @param additional_body_parameters [Hash{String => Object}] + # @param timeout_in_seconds [Long] + # @return [SeedMixedFileDirectoryClient::IdempotencyRequestOptions] + def initialize(base_url: nil, additional_headers: nil, additional_query_parameters: nil, + additional_body_parameters: nil, timeout_in_seconds: nil) + @base_url = base_url + @additional_headers = additional_headers + @additional_query_parameters = additional_query_parameters + @additional_body_parameters = additional_body_parameters + @timeout_in_seconds = timeout_in_seconds + end + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/lib/types_export.rb b/seed/ruby-sdk/mixed-file-directory/lib/types_export.rb new file mode 100644 index 00000000000..82bd7b30bf9 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/lib/types_export.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_relative "fern_mixed_file_directory/organization/types/organization" +require_relative "fern_mixed_file_directory/organization/types/create_organization_request" +require_relative "fern_mixed_file_directory/user/types/user" +require_relative "fern_mixed_file_directory/user/events/types/event" +require_relative "fern_mixed_file_directory/user/events/metadata/types/metadata" diff --git a/seed/ruby-sdk/mixed-file-directory/snippet-templates.json b/seed/ruby-sdk/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/ruby-sdk/mixed-file-directory/snippet.json b/seed/ruby-sdk/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..7f605459db6 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/snippet.json @@ -0,0 +1,93 @@ +{ + "endpoints": [ + { + "id": { + "path": "/organizations/", + "method": "POST", + "identifierOverride": "endpoint_organization.create" + }, + "snippet": { + "client": "require \"fern_mixed_file_directory\"\n\nmixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: \"https://api.example.com\")\nmixed_file_directory.organization.create(request: { name: \"string\" })", + "type": "ruby" + } + }, + { + "id": { + "path": "/organizations/", + "method": "POST", + "identifierOverride": "endpoint_organization.create" + }, + "snippet": { + "client": "require \"fern_mixed_file_directory\"\n\nmixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: \"https://api.example.com\")\nmixed_file_directory.organization.create(request: { name: \"string\" })", + "type": "ruby" + } + }, + { + "id": { + "path": "/users/", + "method": "GET", + "identifierOverride": "endpoint_user.list" + }, + "snippet": { + "client": "require \"fern_mixed_file_directory\"\n\nmixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: \"https://api.example.com\")\nmixed_file_directory.user.list(limit: 1)", + "type": "ruby" + } + }, + { + "id": { + "path": "/users/", + "method": "GET", + "identifierOverride": "endpoint_user.list" + }, + "snippet": { + "client": "require \"fern_mixed_file_directory\"\n\nmixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: \"https://api.example.com\")\nmixed_file_directory.user.list(limit: 1)", + "type": "ruby" + } + }, + { + "id": { + "path": "/users/events/", + "method": "GET", + "identifierOverride": "endpoint_user/events.listEvents" + }, + "snippet": { + "client": "require \"fern_mixed_file_directory\"\n\nmixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: \"https://api.example.com\")\nmixed_file_directory.user.events.list_events(limit: 1)", + "type": "ruby" + } + }, + { + "id": { + "path": "/users/events/", + "method": "GET", + "identifierOverride": "endpoint_user/events.listEvents" + }, + "snippet": { + "client": "require \"fern_mixed_file_directory\"\n\nmixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: \"https://api.example.com\")\nmixed_file_directory.user.events.list_events(limit: 1)", + "type": "ruby" + } + }, + { + "id": { + "path": "/users/events/metadata/", + "method": "GET", + "identifierOverride": "endpoint_user/events/metadata.getMetadata" + }, + "snippet": { + "client": "require \"fern_mixed_file_directory\"\n\nmixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: \"https://api.example.com\")\nmixed_file_directory.user.events.metadata.get_metadata(id: \"string\")", + "type": "ruby" + } + }, + { + "id": { + "path": "/users/events/metadata/", + "method": "GET", + "identifierOverride": "endpoint_user/events/metadata.getMetadata" + }, + "snippet": { + "client": "require \"fern_mixed_file_directory\"\n\nmixed_file_directory = SeedMixedFileDirectoryClient::Client.new(base_url: \"https://api.example.com\")\nmixed_file_directory.user.events.metadata.get_metadata(id: \"string\")", + "type": "ruby" + } + } + ], + "types": {} +} \ No newline at end of file diff --git a/seed/ruby-sdk/mixed-file-directory/test/test_fern_mixed_file_directory.rb b/seed/ruby-sdk/mixed-file-directory/test/test_fern_mixed_file_directory.rb new file mode 100644 index 00000000000..12563582ce7 --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/test/test_fern_mixed_file_directory.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "fern_mixed_file_directory" + +# Basic SeedMixedFileDirectoryClient tests +class TestSeedMixedFileDirectoryClient < Minitest::Test + def test_function + # SeedMixedFileDirectoryClient::Client.new + end +end diff --git a/seed/ruby-sdk/mixed-file-directory/test/test_helper.rb b/seed/ruby-sdk/mixed-file-directory/test/test_helper.rb new file mode 100644 index 00000000000..7e8a3733fff --- /dev/null +++ b/seed/ruby-sdk/mixed-file-directory/test/test_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) + +require "minitest/autorun" +require "fern_mixed_file_directory" diff --git a/seed/ruby-sdk/seed.yml b/seed/ruby-sdk/seed.yml index ce19f6184a6..bfae57f3c78 100644 --- a/seed/ruby-sdk/seed.yml +++ b/seed/ruby-sdk/seed.yml @@ -41,12 +41,11 @@ fixtures: flattenModuleStructure: true outputFolder: flattened-module-structure allowedFailures: - - streaming - - objects-with-imports - - circular-references - # TODO: Add support for recursive undiscriminated unions. - - grpc - any-auth + - circular-references + - mixed-file-directory + - objects-with-imports + - streaming - trace features: requestOptions: true diff --git a/seed/ts-express/mixed-file-directory/.mock/definition/__package__.yml b/seed/ts-express/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/ts-express/mixed-file-directory/.mock/definition/api.yml b/seed/ts-express/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/ts-express/mixed-file-directory/.mock/definition/organization.yml b/seed/ts-express/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/ts-express/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/ts-express/mixed-file-directory/.mock/definition/user.yml b/seed/ts-express/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/ts-express/mixed-file-directory/.mock/definition/user/events.yml b/seed/ts-express/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/ts-express/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/ts-express/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/ts-express/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/ts-express/mixed-file-directory/.mock/fern.config.json b/seed/ts-express/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/ts-express/mixed-file-directory/.mock/generators.yml b/seed/ts-express/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/ts-express/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/ts-express/mixed-file-directory/api/index.ts b/seed/ts-express/mixed-file-directory/api/index.ts new file mode 100644 index 00000000000..3ce0a3e38e8 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./resources"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/index.ts b/seed/ts-express/mixed-file-directory/api/resources/index.ts new file mode 100644 index 00000000000..f281c7d14eb --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/index.ts @@ -0,0 +1,4 @@ +export * as organization from "./organization"; +export * from "./organization/types"; +export * as user from "./user"; +export * from "./user/types"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/organization/index.ts b/seed/ts-express/mixed-file-directory/api/resources/organization/index.ts new file mode 100644 index 00000000000..fcc81debec4 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/organization/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./service"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/organization/service/OrganizationService.ts b/seed/ts-express/mixed-file-directory/api/resources/organization/service/OrganizationService.ts new file mode 100644 index 00000000000..c2b5b7b283e --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/organization/service/OrganizationService.ts @@ -0,0 +1,90 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../index"; +import express from "express"; +import * as serializers from "../../../../serialization/index"; +import * as errors from "../../../../errors/index"; + +export interface OrganizationServiceMethods { + create( + req: express.Request< + never, + SeedMixedFileDirectory.Organization, + SeedMixedFileDirectory.CreateOrganizationRequest, + never + >, + res: { + send: (responseBody: SeedMixedFileDirectory.Organization) => Promise; + cookie: (cookie: string, value: string, options?: express.CookieOptions) => void; + locals: any; + }, + next: express.NextFunction + ): void | Promise; +} + +export class OrganizationService { + private router; + + constructor(private readonly methods: OrganizationServiceMethods, middleware: express.RequestHandler[] = []) { + this.router = express.Router({ mergeParams: true }).use( + express.json({ + strict: false, + }), + ...middleware + ); + } + + public addMiddleware(handler: express.RequestHandler): this { + this.router.use(handler); + return this; + } + + public toRouter(): express.Router { + this.router.post("/", async (req, res, next) => { + const request = serializers.CreateOrganizationRequest.parse(req.body); + if (request.ok) { + req.body = request.value; + try { + await this.methods.create( + req as any, + { + send: async (responseBody) => { + res.json( + serializers.Organization.jsonOrThrow(responseBody, { + unrecognizedObjectKeys: "strip", + }) + ); + }, + cookie: res.cookie.bind(res), + locals: res.locals, + }, + next + ); + next(); + } catch (error) { + if (error instanceof errors.SeedMixedFileDirectoryError) { + console.warn( + `Endpoint 'create' unexpectedly threw ${error.constructor.name}.` + + ` If this was intentional, please add ${error.constructor.name} to` + + " the endpoint's errors list in your Fern Definition." + ); + await error.send(res); + } else { + res.status(500).json("Internal Server Error"); + } + next(error); + } + } else { + res.status(422).json({ + errors: request.errors.map( + (error) => ["request", ...error.path].join(" -> ") + ": " + error.message + ), + }); + next(request.errors); + } + }); + return this.router; + } +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/organization/service/index.ts b/seed/ts-express/mixed-file-directory/api/resources/organization/service/index.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/organization/service/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/seed/ts-express/mixed-file-directory/api/resources/organization/types/CreateOrganizationRequest.ts b/seed/ts-express/mixed-file-directory/api/resources/organization/types/CreateOrganizationRequest.ts new file mode 100644 index 00000000000..8b5c5097ade --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/organization/types/CreateOrganizationRequest.ts @@ -0,0 +1,7 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface CreateOrganizationRequest { + name: string; +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/organization/types/Organization.ts b/seed/ts-express/mixed-file-directory/api/resources/organization/types/Organization.ts new file mode 100644 index 00000000000..7daf3aa7807 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/organization/types/Organization.ts @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../index"; + +export interface Organization { + id: SeedMixedFileDirectory.Id; + name: string; + users: SeedMixedFileDirectory.User[]; +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/organization/types/index.ts b/seed/ts-express/mixed-file-directory/api/resources/organization/types/index.ts new file mode 100644 index 00000000000..c4b0dd7c2d6 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/organization/types/index.ts @@ -0,0 +1,2 @@ +export * from "./Organization"; +export * from "./CreateOrganizationRequest"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/index.ts new file mode 100644 index 00000000000..5ddb983439b --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./resources"; +export * from "./service"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/index.ts new file mode 100644 index 00000000000..5ddb983439b --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./resources"; +export * from "./service"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/index.ts new file mode 100644 index 00000000000..20085af104d --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/index.ts @@ -0,0 +1,2 @@ +export * as metadata from "./metadata"; +export * from "./metadata/types"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/index.ts new file mode 100644 index 00000000000..fcc81debec4 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./service"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/service/MetadataService.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/service/MetadataService.ts new file mode 100644 index 00000000000..bbd4e6bdba3 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/service/MetadataService.ts @@ -0,0 +1,81 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../../../../../index"; +import express from "express"; +import * as serializers from "../../../../../../../../serialization/index"; +import * as errors from "../../../../../../../../errors/index"; + +export interface MetadataServiceMethods { + getMetadata( + req: express.Request< + never, + SeedMixedFileDirectory.user.events.Metadata, + never, + { + id: SeedMixedFileDirectory.Id; + } + >, + res: { + send: (responseBody: SeedMixedFileDirectory.user.events.Metadata) => Promise; + cookie: (cookie: string, value: string, options?: express.CookieOptions) => void; + locals: any; + }, + next: express.NextFunction + ): void | Promise; +} + +export class MetadataService { + private router; + + constructor(private readonly methods: MetadataServiceMethods, middleware: express.RequestHandler[] = []) { + this.router = express.Router({ mergeParams: true }).use( + express.json({ + strict: false, + }), + ...middleware + ); + } + + public addMiddleware(handler: express.RequestHandler): this { + this.router.use(handler); + return this; + } + + public toRouter(): express.Router { + this.router.get("/", async (req, res, next) => { + try { + await this.methods.getMetadata( + req as any, + { + send: async (responseBody) => { + res.json( + serializers.user.events.Metadata.jsonOrThrow(responseBody, { + unrecognizedObjectKeys: "strip", + }) + ); + }, + cookie: res.cookie.bind(res), + locals: res.locals, + }, + next + ); + next(); + } catch (error) { + if (error instanceof errors.SeedMixedFileDirectoryError) { + console.warn( + `Endpoint 'getMetadata' unexpectedly threw ${error.constructor.name}.` + + ` If this was intentional, please add ${error.constructor.name} to` + + " the endpoint's errors list in your Fern Definition." + ); + await error.send(res); + } else { + res.status(500).json("Internal Server Error"); + } + next(error); + } + }); + return this.router; + } +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/service/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/service/index.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/service/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/types/Metadata.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/types/Metadata.ts new file mode 100644 index 00000000000..a38ef1059ed --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/types/Metadata.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../../../../../index"; + +export interface Metadata { + id: SeedMixedFileDirectory.Id; + value?: unknown; +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/types/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/types/index.ts new file mode 100644 index 00000000000..8abb66966d0 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/resources/metadata/types/index.ts @@ -0,0 +1 @@ +export * from "./Metadata"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/service/EventsService.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/service/EventsService.ts new file mode 100644 index 00000000000..d5de5806344 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/service/EventsService.ts @@ -0,0 +1,81 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../../../index"; +import express from "express"; +import * as serializers from "../../../../../../serialization/index"; +import * as errors from "../../../../../../errors/index"; + +export interface EventsServiceMethods { + listEvents( + req: express.Request< + never, + SeedMixedFileDirectory.user.Event[], + never, + { + limit?: number; + } + >, + res: { + send: (responseBody: SeedMixedFileDirectory.user.Event[]) => Promise; + cookie: (cookie: string, value: string, options?: express.CookieOptions) => void; + locals: any; + }, + next: express.NextFunction + ): void | Promise; +} + +export class EventsService { + private router; + + constructor(private readonly methods: EventsServiceMethods, middleware: express.RequestHandler[] = []) { + this.router = express.Router({ mergeParams: true }).use( + express.json({ + strict: false, + }), + ...middleware + ); + } + + public addMiddleware(handler: express.RequestHandler): this { + this.router.use(handler); + return this; + } + + public toRouter(): express.Router { + this.router.get("/", async (req, res, next) => { + try { + await this.methods.listEvents( + req as any, + { + send: async (responseBody) => { + res.json( + serializers.user.events.listEvents.Response.jsonOrThrow(responseBody, { + unrecognizedObjectKeys: "strip", + }) + ); + }, + cookie: res.cookie.bind(res), + locals: res.locals, + }, + next + ); + next(); + } catch (error) { + if (error instanceof errors.SeedMixedFileDirectoryError) { + console.warn( + `Endpoint 'listEvents' unexpectedly threw ${error.constructor.name}.` + + ` If this was intentional, please add ${error.constructor.name} to` + + " the endpoint's errors list in your Fern Definition." + ); + await error.send(res); + } else { + res.status(500).json("Internal Server Error"); + } + next(error); + } + }); + return this.router; + } +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/service/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/service/index.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/service/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/types/Event.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/types/Event.ts new file mode 100644 index 00000000000..811dfcff1dd --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/types/Event.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../../../index"; + +export interface Event { + id: SeedMixedFileDirectory.Id; + name: string; +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/types/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/types/index.ts new file mode 100644 index 00000000000..6868d665e48 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/events/types/index.ts @@ -0,0 +1 @@ +export * from "./Event"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/resources/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/resources/index.ts new file mode 100644 index 00000000000..f8858c12a24 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/resources/index.ts @@ -0,0 +1,2 @@ +export * as events from "./events"; +export * from "./events/types"; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/service/UserService.ts b/seed/ts-express/mixed-file-directory/api/resources/user/service/UserService.ts new file mode 100644 index 00000000000..1712d27d915 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/service/UserService.ts @@ -0,0 +1,81 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../index"; +import express from "express"; +import * as serializers from "../../../../serialization/index"; +import * as errors from "../../../../errors/index"; + +export interface UserServiceMethods { + list( + req: express.Request< + never, + SeedMixedFileDirectory.User[], + never, + { + limit?: number; + } + >, + res: { + send: (responseBody: SeedMixedFileDirectory.User[]) => Promise; + cookie: (cookie: string, value: string, options?: express.CookieOptions) => void; + locals: any; + }, + next: express.NextFunction + ): void | Promise; +} + +export class UserService { + private router; + + constructor(private readonly methods: UserServiceMethods, middleware: express.RequestHandler[] = []) { + this.router = express.Router({ mergeParams: true }).use( + express.json({ + strict: false, + }), + ...middleware + ); + } + + public addMiddleware(handler: express.RequestHandler): this { + this.router.use(handler); + return this; + } + + public toRouter(): express.Router { + this.router.get("/", async (req, res, next) => { + try { + await this.methods.list( + req as any, + { + send: async (responseBody) => { + res.json( + serializers.user.list.Response.jsonOrThrow(responseBody, { + unrecognizedObjectKeys: "strip", + }) + ); + }, + cookie: res.cookie.bind(res), + locals: res.locals, + }, + next + ); + next(); + } catch (error) { + if (error instanceof errors.SeedMixedFileDirectoryError) { + console.warn( + `Endpoint 'list' unexpectedly threw ${error.constructor.name}.` + + ` If this was intentional, please add ${error.constructor.name} to` + + " the endpoint's errors list in your Fern Definition." + ); + await error.send(res); + } else { + res.status(500).json("Internal Server Error"); + } + next(error); + } + }); + return this.router; + } +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/service/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/service/index.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/service/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/types/User.ts b/seed/ts-express/mixed-file-directory/api/resources/user/types/User.ts new file mode 100644 index 00000000000..6f6ec03a072 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/types/User.ts @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../index"; + +export interface User { + id: SeedMixedFileDirectory.Id; + name: string; + age: number; +} diff --git a/seed/ts-express/mixed-file-directory/api/resources/user/types/index.ts b/seed/ts-express/mixed-file-directory/api/resources/user/types/index.ts new file mode 100644 index 00000000000..3ce758c1197 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/resources/user/types/index.ts @@ -0,0 +1 @@ +export * from "./User"; diff --git a/seed/ts-express/mixed-file-directory/api/types/Id.ts b/seed/ts-express/mixed-file-directory/api/types/Id.ts new file mode 100644 index 00000000000..f96abc0c746 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/types/Id.ts @@ -0,0 +1,5 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export type Id = string; diff --git a/seed/ts-express/mixed-file-directory/api/types/index.ts b/seed/ts-express/mixed-file-directory/api/types/index.ts new file mode 100644 index 00000000000..6823c3ab871 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/api/types/index.ts @@ -0,0 +1 @@ +export * from "./Id"; diff --git a/seed/ts-express/mixed-file-directory/core/index.ts b/seed/ts-express/mixed-file-directory/core/index.ts new file mode 100644 index 00000000000..3ae53c06d38 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/index.ts @@ -0,0 +1 @@ +export * as serialization from "./schemas"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/Schema.ts b/seed/ts-express/mixed-file-directory/core/schemas/Schema.ts new file mode 100644 index 00000000000..19acc5dc44b --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/Schema.ts @@ -0,0 +1,98 @@ +import { SchemaUtils } from "./builders"; + +export type Schema = BaseSchema & SchemaUtils; + +export type inferRaw = S extends Schema ? Raw : never; +export type inferParsed = S extends Schema ? Parsed : never; + +export interface BaseSchema { + parse: (raw: unknown, opts?: SchemaOptions) => MaybeValid; + json: (parsed: unknown, opts?: SchemaOptions) => MaybeValid; + getType: () => SchemaType | SchemaType; +} + +export const SchemaType = { + DATE: "date", + ENUM: "enum", + LIST: "list", + STRING_LITERAL: "stringLiteral", + BOOLEAN_LITERAL: "booleanLiteral", + OBJECT: "object", + ANY: "any", + BOOLEAN: "boolean", + NUMBER: "number", + STRING: "string", + UNKNOWN: "unknown", + RECORD: "record", + SET: "set", + UNION: "union", + UNDISCRIMINATED_UNION: "undiscriminatedUnion", + OPTIONAL: "optional", +} as const; +export type SchemaType = typeof SchemaType[keyof typeof SchemaType]; + +export type MaybeValid = Valid | Invalid; + +export interface Valid { + ok: true; + value: T; +} + +export interface Invalid { + ok: false; + errors: ValidationError[]; +} + +export interface ValidationError { + path: string[]; + message: string; +} + +export interface SchemaOptions { + /** + * how to handle unrecognized keys in objects + * + * @default "fail" + */ + unrecognizedObjectKeys?: "fail" | "passthrough" | "strip"; + + /** + * whether to fail when an unrecognized discriminant value is + * encountered in a union + * + * @default false + */ + allowUnrecognizedUnionMembers?: boolean; + + /** + * whether to fail when an unrecognized enum value is encountered + * + * @default false + */ + allowUnrecognizedEnumValues?: boolean; + + /** + * whether to allow data that doesn't conform to the schema. + * invalid data is passed through without transformation. + * + * when this is enabled, .parse() and .json() will always + * return `ok: true`. `.parseOrThrow()` and `.jsonOrThrow()` + * will never fail. + * + * @default false + */ + skipValidation?: boolean; + + /** + * each validation failure contains a "path" property, which is + * the breadcrumbs to the offending node in the JSON. you can supply + * a prefix that is prepended to all the errors' paths. this can be + * helpful for zurg's internal debug logging. + */ + breadcrumbsPrefix?: string[]; + + /** + * whether to send 'null' for optional properties explicitly set to 'undefined'. + */ + omitUndefined?: boolean; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/date/date.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/date/date.ts new file mode 100644 index 00000000000..b70f24b045a --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/date/date.ts @@ -0,0 +1,65 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +// https://stackoverflow.com/questions/12756159/regex-and-iso8601-formatted-datetime +const ISO_8601_REGEX = + /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; + +export function date(): Schema { + const baseSchema: BaseSchema = { + parse: (raw, { breadcrumbsPrefix = [] } = {}) => { + if (typeof raw !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "string"), + }, + ], + }; + } + if (!ISO_8601_REGEX.test(raw)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "ISO 8601 date string"), + }, + ], + }; + } + return { + ok: true, + value: new Date(raw), + }; + }, + json: (date, { breadcrumbsPrefix = [] } = {}) => { + if (date instanceof Date) { + return { + ok: true, + value: date.toISOString(), + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(date, "Date object"), + }, + ], + }; + } + }, + getType: () => SchemaType.DATE, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/date/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/date/index.ts new file mode 100644 index 00000000000..187b29040f6 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/date/index.ts @@ -0,0 +1 @@ +export { date } from "./date"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/enum/enum.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/enum/enum.ts new file mode 100644 index 00000000000..c1e24d69dec --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/enum/enum.ts @@ -0,0 +1,43 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function enum_(values: E): Schema { + const validValues = new Set(values); + + const schemaCreator = createIdentitySchemaCreator( + SchemaType.ENUM, + (value, { allowUnrecognizedEnumValues, breadcrumbsPrefix = [] } = {}) => { + if (typeof value !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "string"), + }, + ], + }; + } + + if (!validValues.has(value) && !allowUnrecognizedEnumValues) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "enum"), + }, + ], + }; + } + + return { + ok: true, + value: value as U, + }; + } + ); + + return schemaCreator(); +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/enum/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/enum/index.ts new file mode 100644 index 00000000000..fe6faed93e3 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/enum/index.ts @@ -0,0 +1 @@ +export { enum_ } from "./enum"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/index.ts new file mode 100644 index 00000000000..050cd2c4efb --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/index.ts @@ -0,0 +1,13 @@ +export * from "./date"; +export * from "./enum"; +export * from "./lazy"; +export * from "./list"; +export * from "./literals"; +export * from "./object"; +export * from "./object-like"; +export * from "./primitives"; +export * from "./record"; +export * from "./schema-utils"; +export * from "./set"; +export * from "./undiscriminated-union"; +export * from "./union"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/index.ts new file mode 100644 index 00000000000..77420fb031c --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/index.ts @@ -0,0 +1,3 @@ +export { lazy } from "./lazy"; +export type { SchemaGetter } from "./lazy"; +export { lazyObject } from "./lazyObject"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/lazy.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/lazy.ts new file mode 100644 index 00000000000..835c61f8a56 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/lazy.ts @@ -0,0 +1,32 @@ +import { BaseSchema, Schema } from "../../Schema"; +import { getSchemaUtils } from "../schema-utils"; + +export type SchemaGetter> = () => SchemaType; + +export function lazy(getter: SchemaGetter>): Schema { + const baseSchema = constructLazyBaseSchema(getter); + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} + +export function constructLazyBaseSchema( + getter: SchemaGetter> +): BaseSchema { + return { + parse: (raw, opts) => getMemoizedSchema(getter).parse(raw, opts), + json: (parsed, opts) => getMemoizedSchema(getter).json(parsed, opts), + getType: () => getMemoizedSchema(getter).getType(), + }; +} + +type MemoizedGetter> = SchemaGetter & { __zurg_memoized?: SchemaType }; + +export function getMemoizedSchema>(getter: SchemaGetter): SchemaType { + const castedGetter = getter as MemoizedGetter; + if (castedGetter.__zurg_memoized == null) { + castedGetter.__zurg_memoized = getter(); + } + return castedGetter.__zurg_memoized; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/lazyObject.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/lazyObject.ts new file mode 100644 index 00000000000..38c9e28404b --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/lazy/lazyObject.ts @@ -0,0 +1,20 @@ +import { getObjectUtils } from "../object"; +import { getObjectLikeUtils } from "../object-like"; +import { BaseObjectSchema, ObjectSchema } from "../object/types"; +import { getSchemaUtils } from "../schema-utils"; +import { constructLazyBaseSchema, getMemoizedSchema, SchemaGetter } from "./lazy"; + +export function lazyObject(getter: SchemaGetter>): ObjectSchema { + const baseSchema: BaseObjectSchema = { + ...constructLazyBaseSchema(getter), + _getRawProperties: () => getMemoizedSchema(getter)._getRawProperties(), + _getParsedProperties: () => getMemoizedSchema(getter)._getParsedProperties(), + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/list/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/list/index.ts new file mode 100644 index 00000000000..25f4bcc1737 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/list/index.ts @@ -0,0 +1 @@ +export { list } from "./list"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts new file mode 100644 index 00000000000..e4c5c4a4a99 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts @@ -0,0 +1,73 @@ +import { BaseSchema, MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +export function list(schema: Schema): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => + validateAndTransformArray(raw, (item, index) => + schema.parse(item, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `[${index}]`], + }) + ), + json: (parsed, opts) => + validateAndTransformArray(parsed, (item, index) => + schema.json(item, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `[${index}]`], + }) + ), + getType: () => SchemaType.LIST, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformArray( + value: unknown, + transformItem: (item: Raw, index: number) => MaybeValid +): MaybeValid { + if (!Array.isArray(value)) { + return { + ok: false, + errors: [ + { + message: getErrorMessageForIncorrectType(value, "list"), + path: [], + }, + ], + }; + } + + const maybeValidItems = value.map((item, index) => transformItem(item, index)); + + return maybeValidItems.reduce>( + (acc, item) => { + if (acc.ok && item.ok) { + return { + ok: true, + value: [...acc.value, item.value], + }; + } + + const errors: ValidationError[] = []; + if (!acc.ok) { + errors.push(...acc.errors); + } + if (!item.ok) { + errors.push(...item.errors); + } + + return { + ok: false, + errors, + }; + }, + { ok: true, value: [] } + ); +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/booleanLiteral.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/booleanLiteral.ts new file mode 100644 index 00000000000..a83d22cd48a --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/booleanLiteral.ts @@ -0,0 +1,29 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function booleanLiteral(literal: V): Schema { + const schemaCreator = createIdentitySchemaCreator( + SchemaType.BOOLEAN_LITERAL, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (value === literal) { + return { + ok: true, + value: literal, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, `${literal.toString()}`), + }, + ], + }; + } + } + ); + + return schemaCreator(); +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/index.ts new file mode 100644 index 00000000000..d2bf08fc6ca --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/index.ts @@ -0,0 +1,2 @@ +export { stringLiteral } from "./stringLiteral"; +export { booleanLiteral } from "./booleanLiteral"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/stringLiteral.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/stringLiteral.ts new file mode 100644 index 00000000000..3939b76b48d --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/literals/stringLiteral.ts @@ -0,0 +1,29 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function stringLiteral(literal: V): Schema { + const schemaCreator = createIdentitySchemaCreator( + SchemaType.STRING_LITERAL, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (value === literal) { + return { + ok: true, + value: literal, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, `"${literal}"`), + }, + ], + }; + } + } + ); + + return schemaCreator(); +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts new file mode 100644 index 00000000000..8331d08da89 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -0,0 +1,79 @@ +import { BaseSchema } from "../../Schema"; +import { filterObject } from "../../utils/filterObject"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { getSchemaUtils } from "../schema-utils"; +import { ObjectLikeSchema, ObjectLikeUtils } from "./types"; + +export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { + return { + withParsedProperties: (properties) => withParsedProperties(schema, properties), + }; +} + +/** + * object-like utils are defined in one file to resolve issues with circular imports + */ + +export function withParsedProperties( + objectLike: BaseSchema, + properties: { [K in keyof Properties]: Properties[K] | ((parsed: ParsedObjectShape) => Properties[K]) } +): ObjectLikeSchema { + const objectSchema: BaseSchema = { + parse: (raw, opts) => { + const parsedObject = objectLike.parse(raw, opts); + if (!parsedObject.ok) { + return parsedObject; + } + + const additionalProperties = Object.entries(properties).reduce>( + (processed, [key, value]) => { + return { + ...processed, + [key]: typeof value === "function" ? value(parsedObject.value) : value, + }; + }, + {} + ); + + return { + ok: true, + value: { + ...parsedObject.value, + ...(additionalProperties as Properties), + }, + }; + }, + + json: (parsed, opts) => { + if (!isPlainObject(parsed)) { + return { + ok: false, + errors: [ + { + path: opts?.breadcrumbsPrefix ?? [], + message: getErrorMessageForIncorrectType(parsed, "object"), + }, + ], + }; + } + + // strip out added properties + const addedPropertyKeys = new Set(Object.keys(properties)); + const parsedWithoutAddedProperties = filterObject( + parsed, + Object.keys(parsed).filter((key) => !addedPropertyKeys.has(key)) + ); + + return objectLike.json(parsedWithoutAddedProperties as ParsedObjectShape, opts); + }, + + getType: () => objectLike.getType(), + }; + + return { + ...objectSchema, + ...getSchemaUtils(objectSchema), + ...getObjectLikeUtils(objectSchema), + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/index.ts new file mode 100644 index 00000000000..c342e72cf9d --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/index.ts @@ -0,0 +1,2 @@ +export { getObjectLikeUtils, withParsedProperties } from "./getObjectLikeUtils"; +export type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/types.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/types.ts new file mode 100644 index 00000000000..75b3698729c --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/types.ts @@ -0,0 +1,11 @@ +import { BaseSchema, Schema } from "../../Schema"; + +export type ObjectLikeSchema = Schema & + BaseSchema & + ObjectLikeUtils; + +export interface ObjectLikeUtils { + withParsedProperties: >(properties: { + [K in keyof T]: T[K] | ((parsed: Parsed) => T[K]); + }) => ObjectLikeSchema; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/index.ts new file mode 100644 index 00000000000..e3f4388db28 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/index.ts @@ -0,0 +1,22 @@ +export { getObjectUtils, object } from "./object"; +export { objectWithoutOptionalProperties } from "./objectWithoutOptionalProperties"; +export type { + inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas, + inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas, +} from "./objectWithoutOptionalProperties"; +export { isProperty, property } from "./property"; +export type { Property } from "./property"; +export type { + BaseObjectSchema, + inferObjectSchemaFromPropertySchemas, + inferParsedObject, + inferParsedObjectFromPropertySchemas, + inferParsedPropertySchema, + inferRawKey, + inferRawObject, + inferRawObjectFromPropertySchemas, + inferRawPropertySchema, + ObjectSchema, + ObjectUtils, + PropertySchemas, +} from "./types"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts new file mode 100644 index 00000000000..e00136d72fc --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts @@ -0,0 +1,324 @@ +import { MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { entries } from "../../utils/entries"; +import { filterObject } from "../../utils/filterObject"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { keys } from "../../utils/keys"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { partition } from "../../utils/partition"; +import { getObjectLikeUtils } from "../object-like"; +import { getSchemaUtils } from "../schema-utils"; +import { isProperty } from "./property"; +import { + BaseObjectSchema, + inferObjectSchemaFromPropertySchemas, + inferParsedObjectFromPropertySchemas, + inferRawObjectFromPropertySchemas, + ObjectSchema, + ObjectUtils, + PropertySchemas, +} from "./types"; + +interface ObjectPropertyWithRawKey { + rawKey: string; + parsedKey: string; + valueSchema: Schema; +} + +export function object>( + schemas: T +): inferObjectSchemaFromPropertySchemas { + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const rawKeyToProperty: Record = {}; + const requiredKeys: string[] = []; + + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; + + const property: ObjectPropertyWithRawKey = { + rawKey, + parsedKey: parsedKey as string, + valueSchema, + }; + + rawKeyToProperty[rawKey] = property; + + if (isSchemaRequired(valueSchema)) { + requiredKeys.push(rawKey); + } + } + + return validateAndTransformObject({ + value: raw, + requiredKeys, + getProperty: (rawKey) => { + const property = rawKeyToProperty[rawKey]; + if (property == null) { + return undefined; + } + return { + transformedKey: property.parsedKey, + transform: (propertyValue) => + property.valueSchema.parse(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], + }), + }; + }, + unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, + skipValidation: opts?.skipValidation, + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + omitUndefined: opts?.omitUndefined, + }); + }, + + json: (parsed, opts) => { + const requiredKeys: string[] = []; + + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; + + if (isSchemaRequired(valueSchema)) { + requiredKeys.push(parsedKey as string); + } + } + + return validateAndTransformObject({ + value: parsed, + requiredKeys, + getProperty: ( + parsedKey + ): { transformedKey: string; transform: (propertyValue: unknown) => MaybeValid } | undefined => { + const property = schemas[parsedKey as keyof T]; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (property == null) { + return undefined; + } + + if (isProperty(property)) { + return { + transformedKey: property.rawKey, + transform: (propertyValue) => + property.valueSchema.json(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], + }), + }; + } else { + return { + transformedKey: parsedKey, + transform: (propertyValue) => + property.json(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], + }), + }; + } + }, + unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, + skipValidation: opts?.skipValidation, + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + omitUndefined: opts?.omitUndefined, + }); + }, + + getType: () => SchemaType.OBJECT, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; +} + +function validateAndTransformObject({ + value, + requiredKeys, + getProperty, + unrecognizedObjectKeys = "fail", + skipValidation = false, + breadcrumbsPrefix = [], +}: { + value: unknown; + requiredKeys: string[]; + getProperty: ( + preTransformedKey: string + ) => { transformedKey: string; transform: (propertyValue: unknown) => MaybeValid } | undefined; + unrecognizedObjectKeys: "fail" | "passthrough" | "strip" | undefined; + skipValidation: boolean | undefined; + breadcrumbsPrefix: string[] | undefined; + omitUndefined: boolean | undefined; +}): MaybeValid { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + const missingRequiredKeys = new Set(requiredKeys); + const errors: ValidationError[] = []; + const transformed: Record = {}; + + for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + const property = getProperty(preTransformedKey); + + if (property != null) { + missingRequiredKeys.delete(preTransformedKey); + + const value = property.transform(preTransformedItemValue); + if (value.ok) { + transformed[property.transformedKey] = value.value; + } else { + transformed[preTransformedKey] = preTransformedItemValue; + errors.push(...value.errors); + } + } else { + switch (unrecognizedObjectKeys) { + case "fail": + errors.push({ + path: [...breadcrumbsPrefix, preTransformedKey], + message: `Unexpected key "${preTransformedKey}"`, + }); + break; + case "strip": + break; + case "passthrough": + transformed[preTransformedKey] = preTransformedItemValue; + break; + } + } + } + + errors.push( + ...requiredKeys + .filter((key) => missingRequiredKeys.has(key)) + .map((key) => ({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + })) + ); + + if (errors.length === 0 || skipValidation) { + return { + ok: true, + value: transformed as Transformed, + }; + } else { + return { + ok: false, + errors, + }; + } +} + +export function getObjectUtils(schema: BaseObjectSchema): ObjectUtils { + return { + extend: (extension: ObjectSchema) => { + const baseSchema: BaseObjectSchema = { + _getParsedProperties: () => [...schema._getParsedProperties(), ...extension._getParsedProperties()], + _getRawProperties: () => [...schema._getRawProperties(), ...extension._getRawProperties()], + parse: (raw, opts) => { + return validateAndTransformExtendedObject({ + extensionKeys: extension._getRawProperties(), + value: raw, + transformBase: (rawBase) => schema.parse(rawBase, opts), + transformExtension: (rawExtension) => extension.parse(rawExtension, opts), + }); + }, + json: (parsed, opts) => { + return validateAndTransformExtendedObject({ + extensionKeys: extension._getParsedProperties(), + value: parsed, + transformBase: (parsedBase) => schema.json(parsedBase, opts), + transformExtension: (parsedExtension) => extension.json(parsedExtension, opts), + }); + }, + getType: () => SchemaType.OBJECT, + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; + }, + }; +} + +function validateAndTransformExtendedObject({ + extensionKeys, + value, + transformBase, + transformExtension, +}: { + extensionKeys: (keyof PreTransformedExtension)[]; + value: unknown; + transformBase: (value: unknown) => MaybeValid; + transformExtension: (value: unknown) => MaybeValid; +}): MaybeValid { + const extensionPropertiesSet = new Set(extensionKeys); + const [extensionProperties, baseProperties] = partition(keys(value), (key) => + extensionPropertiesSet.has(key as keyof PreTransformedExtension) + ); + + const transformedBase = transformBase(filterObject(value, baseProperties)); + const transformedExtension = transformExtension(filterObject(value, extensionProperties)); + + if (transformedBase.ok && transformedExtension.ok) { + return { + ok: true, + value: { + ...transformedBase.value, + ...transformedExtension.value, + }, + }; + } else { + return { + ok: false, + errors: [ + ...(transformedBase.ok ? [] : transformedBase.errors), + ...(transformedExtension.ok ? [] : transformedExtension.errors), + ], + }; + } +} + +function isSchemaRequired(schema: Schema): boolean { + return !isSchemaOptional(schema); +} + +function isSchemaOptional(schema: Schema): boolean { + switch (schema.getType()) { + case SchemaType.ANY: + case SchemaType.UNKNOWN: + case SchemaType.OPTIONAL: + return true; + default: + return false; + } +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/objectWithoutOptionalProperties.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/objectWithoutOptionalProperties.ts new file mode 100644 index 00000000000..a0951f48efc --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/objectWithoutOptionalProperties.ts @@ -0,0 +1,18 @@ +import { object } from "./object"; +import { inferParsedPropertySchema, inferRawObjectFromPropertySchemas, ObjectSchema, PropertySchemas } from "./types"; + +export function objectWithoutOptionalProperties>( + schemas: T +): inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas { + return object(schemas) as unknown as inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas; +} + +export type inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas> = + ObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas + >; + +export type inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas> = { + [K in keyof T]: inferParsedPropertySchema; +}; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/property.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/property.ts new file mode 100644 index 00000000000..d245c4b193a --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/property.ts @@ -0,0 +1,23 @@ +import { Schema } from "../../Schema"; + +export function property( + rawKey: RawKey, + valueSchema: Schema +): Property { + return { + rawKey, + valueSchema, + isProperty: true, + }; +} + +export interface Property { + rawKey: RawKey; + valueSchema: Schema; + isProperty: true; +} + +export function isProperty>(maybeProperty: unknown): maybeProperty is O { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return (maybeProperty as O).isProperty; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/types.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/types.ts new file mode 100644 index 00000000000..de9bb4074e5 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/types.ts @@ -0,0 +1,72 @@ +import { BaseSchema, inferParsed, inferRaw, Schema } from "../../Schema"; +import { addQuestionMarksToNullableProperties } from "../../utils/addQuestionMarksToNullableProperties"; +import { ObjectLikeUtils } from "../object-like"; +import { SchemaUtils } from "../schema-utils"; +import { Property } from "./property"; + +export type ObjectSchema = BaseObjectSchema & + ObjectLikeUtils & + ObjectUtils & + SchemaUtils; + +export interface BaseObjectSchema extends BaseSchema { + _getRawProperties: () => (keyof Raw)[]; + _getParsedProperties: () => (keyof Parsed)[]; +} + +export interface ObjectUtils { + extend: ( + schemas: ObjectSchema + ) => ObjectSchema; +} + +export type inferRawObject> = O extends ObjectSchema ? Raw : never; + +export type inferParsedObject> = O extends ObjectSchema + ? Parsed + : never; + +export type inferObjectSchemaFromPropertySchemas> = ObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas +>; + +export type inferRawObjectFromPropertySchemas> = + addQuestionMarksToNullableProperties<{ + [ParsedKey in keyof T as inferRawKey]: inferRawPropertySchema; + }>; + +export type inferParsedObjectFromPropertySchemas> = + addQuestionMarksToNullableProperties<{ + [K in keyof T]: inferParsedPropertySchema; + }>; + +export type PropertySchemas = Record< + ParsedKeys, + Property | Schema +>; + +export type inferRawPropertySchema

| Schema> = P extends Property< + any, + infer Raw, + any +> + ? Raw + : P extends Schema + ? inferRaw

+ : never; + +export type inferParsedPropertySchema

| Schema> = P extends Property< + any, + any, + infer Parsed +> + ? Parsed + : P extends Schema + ? inferParsed

+ : never; + +export type inferRawKey< + ParsedKey extends string | number | symbol, + P extends Property | Schema +> = P extends Property ? Raw : ParsedKey; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/any.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/any.ts new file mode 100644 index 00000000000..fcaeb04255a --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/any.ts @@ -0,0 +1,4 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; + +export const any = createIdentitySchemaCreator(SchemaType.ANY, (value) => ({ ok: true, value })); diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/boolean.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/boolean.ts new file mode 100644 index 00000000000..fad60562120 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/boolean.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const boolean = createIdentitySchemaCreator( + SchemaType.BOOLEAN, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "boolean") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "boolean"), + }, + ], + }; + } + } +); diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/index.ts new file mode 100644 index 00000000000..788f9416bfe --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/index.ts @@ -0,0 +1,5 @@ +export { any } from "./any"; +export { boolean } from "./boolean"; +export { number } from "./number"; +export { string } from "./string"; +export { unknown } from "./unknown"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/number.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/number.ts new file mode 100644 index 00000000000..c2689456936 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/number.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const number = createIdentitySchemaCreator( + SchemaType.NUMBER, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "number") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "number"), + }, + ], + }; + } + } +); diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/string.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/string.ts new file mode 100644 index 00000000000..949f1f2a630 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/string.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const string = createIdentitySchemaCreator( + SchemaType.STRING, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "string") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "string"), + }, + ], + }; + } + } +); diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/unknown.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/unknown.ts new file mode 100644 index 00000000000..4d5249571f5 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/primitives/unknown.ts @@ -0,0 +1,4 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; + +export const unknown = createIdentitySchemaCreator(SchemaType.UNKNOWN, (value) => ({ ok: true, value })); diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/record/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/index.ts new file mode 100644 index 00000000000..82e25c5c2af --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/index.ts @@ -0,0 +1,2 @@ +export { record } from "./record"; +export type { BaseRecordSchema, RecordSchema } from "./types"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts new file mode 100644 index 00000000000..6683ac3609f --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts @@ -0,0 +1,130 @@ +import { MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { entries } from "../../utils/entries"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; +import { BaseRecordSchema, RecordSchema } from "./types"; + +export function record( + keySchema: Schema, + valueSchema: Schema +): RecordSchema { + const baseSchema: BaseRecordSchema = { + parse: (raw, opts) => { + return validateAndTransformRecord({ + value: raw, + isKeyNumeric: keySchema.getType() === SchemaType.NUMBER, + transformKey: (key) => + keySchema.parse(key, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key} (key)`], + }), + transformValue: (value, key) => + valueSchema.parse(value, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key}`], + }), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + json: (parsed, opts) => { + return validateAndTransformRecord({ + value: parsed, + isKeyNumeric: keySchema.getType() === SchemaType.NUMBER, + transformKey: (key) => + keySchema.json(key, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key} (key)`], + }), + transformValue: (value, key) => + valueSchema.json(value, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key}`], + }), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + getType: () => SchemaType.RECORD, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformRecord({ + value, + isKeyNumeric, + transformKey, + transformValue, + breadcrumbsPrefix = [], +}: { + value: unknown; + isKeyNumeric: boolean; + transformKey: (key: string | number) => MaybeValid; + transformValue: (value: unknown, key: string | number) => MaybeValid; + breadcrumbsPrefix: string[] | undefined; +}): MaybeValid> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + return entries(value).reduce>>( + (accPromise, [stringKey, value]) => { + // skip nullish keys + if (value == null) { + return accPromise; + } + + const acc = accPromise; + + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!isNaN(numberKey)) { + key = numberKey; + } + } + const transformedKey = transformKey(key); + + const transformedValue = transformValue(value, key); + + if (acc.ok && transformedKey.ok && transformedValue.ok) { + return { + ok: true, + value: { + ...acc.value, + [transformedKey.value]: transformedValue.value, + }, + }; + } + + const errors: ValidationError[] = []; + if (!acc.ok) { + errors.push(...acc.errors); + } + if (!transformedKey.ok) { + errors.push(...transformedKey.errors); + } + if (!transformedValue.ok) { + errors.push(...transformedValue.errors); + } + + return { + ok: false, + errors, + }; + }, + { ok: true, value: {} as Record } + ); +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/record/types.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/types.ts new file mode 100644 index 00000000000..eb82cc7f65c --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/types.ts @@ -0,0 +1,17 @@ +import { BaseSchema } from "../../Schema"; +import { SchemaUtils } from "../schema-utils"; + +export type RecordSchema< + RawKey extends string | number, + RawValue, + ParsedKey extends string | number, + ParsedValue +> = BaseRecordSchema & + SchemaUtils, Record>; + +export type BaseRecordSchema< + RawKey extends string | number, + RawValue, + ParsedKey extends string | number, + ParsedValue +> = BaseSchema, Record>; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/JsonError.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/JsonError.ts new file mode 100644 index 00000000000..2b89ca0e7ad --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/JsonError.ts @@ -0,0 +1,9 @@ +import { ValidationError } from "../../Schema"; +import { stringifyValidationError } from "./stringifyValidationErrors"; + +export class JsonError extends Error { + constructor(public readonly errors: ValidationError[]) { + super(errors.map(stringifyValidationError).join("; ")); + Object.setPrototypeOf(this, JsonError.prototype); + } +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/ParseError.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/ParseError.ts new file mode 100644 index 00000000000..d056eb45cf7 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/ParseError.ts @@ -0,0 +1,9 @@ +import { ValidationError } from "../../Schema"; +import { stringifyValidationError } from "./stringifyValidationErrors"; + +export class ParseError extends Error { + constructor(public readonly errors: ValidationError[]) { + super(errors.map(stringifyValidationError).join("; ")); + Object.setPrototypeOf(this, ParseError.prototype); + } +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/getSchemaUtils.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/getSchemaUtils.ts new file mode 100644 index 00000000000..79ecad92132 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/getSchemaUtils.ts @@ -0,0 +1,105 @@ +import { BaseSchema, Schema, SchemaOptions, SchemaType } from "../../Schema"; +import { JsonError } from "./JsonError"; +import { ParseError } from "./ParseError"; + +export interface SchemaUtils { + optional: () => Schema; + transform: (transformer: SchemaTransformer) => Schema; + parseOrThrow: (raw: unknown, opts?: SchemaOptions) => Parsed; + jsonOrThrow: (raw: unknown, opts?: SchemaOptions) => Raw; +} + +export interface SchemaTransformer { + transform: (parsed: Parsed) => Transformed; + untransform: (transformed: any) => Parsed; +} + +export function getSchemaUtils(schema: BaseSchema): SchemaUtils { + return { + optional: () => optional(schema), + transform: (transformer) => transform(schema, transformer), + parseOrThrow: (raw, opts) => { + const parsed = schema.parse(raw, opts); + if (parsed.ok) { + return parsed.value; + } + throw new ParseError(parsed.errors); + }, + jsonOrThrow: (parsed, opts) => { + const raw = schema.json(parsed, opts); + if (raw.ok) { + return raw.value; + } + throw new JsonError(raw.errors); + }, + }; +} + +/** + * schema utils are defined in one file to resolve issues with circular imports + */ + +export function optional( + schema: BaseSchema +): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => { + if (raw == null) { + return { + ok: true, + value: undefined, + }; + } + return schema.parse(raw, opts); + }, + json: (parsed, opts) => { + if (opts?.omitUndefined && parsed === undefined) { + return { + ok: true, + value: undefined, + }; + } + if (parsed == null) { + return { + ok: true, + value: null, + }; + } + return schema.json(parsed, opts); + }, + getType: () => SchemaType.OPTIONAL, + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} + +export function transform( + schema: BaseSchema, + transformer: SchemaTransformer +): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => { + const parsed = schema.parse(raw, opts); + if (!parsed.ok) { + return parsed; + } + return { + ok: true, + value: transformer.transform(parsed.value), + }; + }, + json: (transformed, opts) => { + const parsed = transformer.untransform(transformed); + return schema.json(parsed, opts); + }, + getType: () => schema.getType(), + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/index.ts new file mode 100644 index 00000000000..aa04e051dfa --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/index.ts @@ -0,0 +1,4 @@ +export { getSchemaUtils, optional, transform } from "./getSchemaUtils"; +export type { SchemaUtils } from "./getSchemaUtils"; +export { JsonError } from "./JsonError"; +export { ParseError } from "./ParseError"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/stringifyValidationErrors.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/stringifyValidationErrors.ts new file mode 100644 index 00000000000..4160f0a2617 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/schema-utils/stringifyValidationErrors.ts @@ -0,0 +1,8 @@ +import { ValidationError } from "../../Schema"; + +export function stringifyValidationError(error: ValidationError): string { + if (error.path.length === 0) { + return error.message; + } + return `${error.path.join(" -> ")}: ${error.message}`; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/set/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/set/index.ts new file mode 100644 index 00000000000..f3310e8bdad --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/set/index.ts @@ -0,0 +1 @@ +export { set } from "./set"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/set/set.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/set/set.ts new file mode 100644 index 00000000000..e9e6bb7e539 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/set/set.ts @@ -0,0 +1,43 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { list } from "../list"; +import { getSchemaUtils } from "../schema-utils"; + +export function set(schema: Schema): Schema> { + const listSchema = list(schema); + const baseSchema: BaseSchema> = { + parse: (raw, opts) => { + const parsedList = listSchema.parse(raw, opts); + if (parsedList.ok) { + return { + ok: true, + value: new Set(parsedList.value), + }; + } else { + return parsedList; + } + }, + json: (parsed, opts) => { + if (!(parsed instanceof Set)) { + return { + ok: false, + errors: [ + { + path: opts?.breadcrumbsPrefix ?? [], + message: getErrorMessageForIncorrectType(parsed, "Set"), + }, + ], + }; + } + const jsonList = listSchema.json([...parsed], opts); + return jsonList; + }, + getType: () => SchemaType.SET, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/index.ts new file mode 100644 index 00000000000..75b71cb3565 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/index.ts @@ -0,0 +1,6 @@ +export type { + inferParsedUnidiscriminatedUnionSchema, + inferRawUnidiscriminatedUnionSchema, + UndiscriminatedUnionSchema, +} from "./types"; +export { undiscriminatedUnion } from "./undiscriminatedUnion"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/types.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/types.ts new file mode 100644 index 00000000000..43e7108a060 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/types.ts @@ -0,0 +1,10 @@ +import { inferParsed, inferRaw, Schema } from "../../Schema"; + +export type UndiscriminatedUnionSchema = Schema< + inferRawUnidiscriminatedUnionSchema, + inferParsedUnidiscriminatedUnionSchema +>; + +export type inferRawUnidiscriminatedUnionSchema = inferRaw; + +export type inferParsedUnidiscriminatedUnionSchema = inferParsed; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts new file mode 100644 index 00000000000..21ed3df0f40 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts @@ -0,0 +1,60 @@ +import { BaseSchema, MaybeValid, Schema, SchemaOptions, SchemaType, ValidationError } from "../../Schema"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; +import { inferParsedUnidiscriminatedUnionSchema, inferRawUnidiscriminatedUnionSchema } from "./types"; + +export function undiscriminatedUnion, ...Schema[]]>( + schemas: Schemas +): Schema, inferParsedUnidiscriminatedUnionSchema> { + const baseSchema: BaseSchema< + inferRawUnidiscriminatedUnionSchema, + inferParsedUnidiscriminatedUnionSchema + > = { + parse: (raw, opts) => { + return validateAndTransformUndiscriminatedUnion>( + (schema, opts) => schema.parse(raw, opts), + schemas, + opts + ); + }, + json: (parsed, opts) => { + return validateAndTransformUndiscriminatedUnion>( + (schema, opts) => schema.json(parsed, opts), + schemas, + opts + ); + }, + getType: () => SchemaType.UNDISCRIMINATED_UNION, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformUndiscriminatedUnion( + transform: (schema: Schema, opts: SchemaOptions) => MaybeValid, + schemas: Schema[], + opts: SchemaOptions | undefined +): MaybeValid { + const errors: ValidationError[] = []; + for (const [index, schema] of schemas.entries()) { + const transformed = transform(schema, { ...opts, skipValidation: false }); + if (transformed.ok) { + return transformed; + } else { + for (const error of transformed.errors) { + errors.push({ + path: error.path, + message: `[Variant ${index}] ${error.message}`, + }); + } + } + } + + return { + ok: false, + errors, + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/union/discriminant.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/discriminant.ts new file mode 100644 index 00000000000..55065bc8946 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/discriminant.ts @@ -0,0 +1,14 @@ +export function discriminant( + parsedDiscriminant: ParsedDiscriminant, + rawDiscriminant: RawDiscriminant +): Discriminant { + return { + parsedDiscriminant, + rawDiscriminant, + }; +} + +export interface Discriminant { + parsedDiscriminant: ParsedDiscriminant; + rawDiscriminant: RawDiscriminant; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/union/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/index.ts new file mode 100644 index 00000000000..85fc008a2d8 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/index.ts @@ -0,0 +1,10 @@ +export { discriminant } from "./discriminant"; +export type { Discriminant } from "./discriminant"; +export type { + inferParsedDiscriminant, + inferParsedUnion, + inferRawDiscriminant, + inferRawUnion, + UnionSubtypes, +} from "./types"; +export { union } from "./union"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/union/types.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/types.ts new file mode 100644 index 00000000000..6f82c868b2d --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/types.ts @@ -0,0 +1,26 @@ +import { inferParsedObject, inferRawObject, ObjectSchema } from "../object"; +import { Discriminant } from "./discriminant"; + +export type UnionSubtypes = { + [K in DiscriminantValues]: ObjectSchema; +}; + +export type inferRawUnion, U extends UnionSubtypes> = { + [K in keyof U]: Record, K> & inferRawObject; +}[keyof U]; + +export type inferParsedUnion, U extends UnionSubtypes> = { + [K in keyof U]: Record, K> & inferParsedObject; +}[keyof U]; + +export type inferRawDiscriminant> = D extends string + ? D + : D extends Discriminant + ? Raw + : never; + +export type inferParsedDiscriminant> = D extends string + ? D + : D extends Discriminant + ? Parsed + : never; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts new file mode 100644 index 00000000000..ab61475a572 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts @@ -0,0 +1,170 @@ +import { BaseSchema, MaybeValid, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { keys } from "../../utils/keys"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { enum_ } from "../enum"; +import { ObjectSchema } from "../object"; +import { getObjectLikeUtils, ObjectLikeSchema } from "../object-like"; +import { getSchemaUtils } from "../schema-utils"; +import { Discriminant } from "./discriminant"; +import { inferParsedDiscriminant, inferParsedUnion, inferRawDiscriminant, inferRawUnion, UnionSubtypes } from "./types"; + +export function union, U extends UnionSubtypes>( + discriminant: D, + union: U +): ObjectLikeSchema, inferParsedUnion> { + const rawDiscriminant = + typeof discriminant === "string" ? discriminant : (discriminant.rawDiscriminant as inferRawDiscriminant); + const parsedDiscriminant = + typeof discriminant === "string" + ? discriminant + : (discriminant.parsedDiscriminant as inferParsedDiscriminant); + + const discriminantValueSchema = enum_(keys(union) as string[]); + + const baseSchema: BaseSchema, inferParsedUnion> = { + parse: (raw, opts) => { + return transformAndValidateUnion({ + value: raw, + discriminant: rawDiscriminant, + transformedDiscriminant: parsedDiscriminant, + transformDiscriminantValue: (discriminantValue) => + discriminantValueSchema.parse(discriminantValue, { + allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawDiscriminant], + }), + getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], + allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, + transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => + additionalPropertiesSchema.parse(additionalProperties, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + json: (parsed, opts) => { + return transformAndValidateUnion({ + value: parsed, + discriminant: parsedDiscriminant, + transformedDiscriminant: rawDiscriminant, + transformDiscriminantValue: (discriminantValue) => + discriminantValueSchema.json(discriminantValue, { + allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedDiscriminant], + }), + getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], + allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, + transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => + additionalPropertiesSchema.json(additionalProperties, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + getType: () => SchemaType.UNION, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + }; +} + +function transformAndValidateUnion< + TransformedDiscriminant extends string, + TransformedDiscriminantValue extends string, + TransformedAdditionalProperties +>({ + value, + discriminant, + transformedDiscriminant, + transformDiscriminantValue, + getAdditionalPropertiesSchema, + allowUnrecognizedUnionMembers = false, + transformAdditionalProperties, + breadcrumbsPrefix = [], +}: { + value: unknown; + discriminant: string; + transformedDiscriminant: TransformedDiscriminant; + transformDiscriminantValue: (discriminantValue: unknown) => MaybeValid; + getAdditionalPropertiesSchema: (discriminantValue: string) => ObjectSchema | undefined; + allowUnrecognizedUnionMembers: boolean | undefined; + transformAdditionalProperties: ( + additionalProperties: unknown, + additionalPropertiesSchema: ObjectSchema + ) => MaybeValid; + breadcrumbsPrefix: string[] | undefined; +}): MaybeValid & TransformedAdditionalProperties> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + const { [discriminant]: discriminantValue, ...additionalProperties } = value; + + if (discriminantValue == null) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: `Missing discriminant ("${discriminant}")`, + }, + ], + }; + } + + const transformedDiscriminantValue = transformDiscriminantValue(discriminantValue); + if (!transformedDiscriminantValue.ok) { + return { + ok: false, + errors: transformedDiscriminantValue.errors, + }; + } + + const additionalPropertiesSchema = getAdditionalPropertiesSchema(transformedDiscriminantValue.value); + + if (additionalPropertiesSchema == null) { + if (allowUnrecognizedUnionMembers) { + return { + ok: true, + value: { + [transformedDiscriminant]: transformedDiscriminantValue.value, + ...additionalProperties, + } as Record & TransformedAdditionalProperties, + }; + } else { + return { + ok: false, + errors: [ + { + path: [...breadcrumbsPrefix, discriminant], + message: "Unexpected discriminant value", + }, + ], + }; + } + } + + const transformedAdditionalProperties = transformAdditionalProperties( + additionalProperties, + additionalPropertiesSchema + ); + if (!transformedAdditionalProperties.ok) { + return transformedAdditionalProperties; + } + + return { + ok: true, + value: { + [transformedDiscriminant]: discriminantValue, + ...transformedAdditionalProperties.value, + } as Record & TransformedAdditionalProperties, + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/index.ts b/seed/ts-express/mixed-file-directory/core/schemas/index.ts new file mode 100644 index 00000000000..5429d8b43eb --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/index.ts @@ -0,0 +1,2 @@ +export * from "./builders"; +export type { inferParsed, inferRaw, Schema, SchemaOptions } from "./Schema"; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/MaybePromise.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/MaybePromise.ts new file mode 100644 index 00000000000..9cd354b3418 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/MaybePromise.ts @@ -0,0 +1 @@ +export type MaybePromise = T | Promise; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/addQuestionMarksToNullableProperties.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/addQuestionMarksToNullableProperties.ts new file mode 100644 index 00000000000..4111d703cd0 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/addQuestionMarksToNullableProperties.ts @@ -0,0 +1,15 @@ +export type addQuestionMarksToNullableProperties = { + [K in OptionalKeys]?: T[K]; +} & Pick>; + +export type OptionalKeys = { + [K in keyof T]-?: undefined extends T[K] + ? K + : null extends T[K] + ? K + : 1 extends (any extends T[K] ? 0 : 1) + ? never + : K; +}[keyof T]; + +export type RequiredKeys = Exclude>; diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/createIdentitySchemaCreator.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/createIdentitySchemaCreator.ts new file mode 100644 index 00000000000..de107cf5ee1 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/createIdentitySchemaCreator.ts @@ -0,0 +1,21 @@ +import { getSchemaUtils } from "../builders/schema-utils"; +import { BaseSchema, MaybeValid, Schema, SchemaOptions, SchemaType } from "../Schema"; +import { maybeSkipValidation } from "./maybeSkipValidation"; + +export function createIdentitySchemaCreator( + schemaType: SchemaType, + validate: (value: unknown, opts?: SchemaOptions) => MaybeValid +): () => Schema { + return () => { + const baseSchema: BaseSchema = { + parse: validate, + json: validate, + getType: () => schemaType, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/entries.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/entries.ts new file mode 100644 index 00000000000..e122952137d --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/entries.ts @@ -0,0 +1,3 @@ +export function entries(object: T): [keyof T, T[keyof T]][] { + return Object.entries(object) as [keyof T, T[keyof T]][]; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/filterObject.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/filterObject.ts new file mode 100644 index 00000000000..2c25a3455bc --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/filterObject.ts @@ -0,0 +1,10 @@ +export function filterObject(obj: T, keysToInclude: K[]): Pick { + const keysToIncludeSet = new Set(keysToInclude); + return Object.entries(obj).reduce((acc, [key, value]) => { + if (keysToIncludeSet.has(key as K)) { + acc[key as K] = value; + } + return acc; + // eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter + }, {} as Pick); +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/getErrorMessageForIncorrectType.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/getErrorMessageForIncorrectType.ts new file mode 100644 index 00000000000..438012df418 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/getErrorMessageForIncorrectType.ts @@ -0,0 +1,21 @@ +export function getErrorMessageForIncorrectType(value: unknown, expectedType: string): string { + return `Expected ${expectedType}. Received ${getTypeAsString(value)}.`; +} + +function getTypeAsString(value: unknown): string { + if (Array.isArray(value)) { + return "list"; + } + if (value === null) { + return "null"; + } + switch (typeof value) { + case "string": + return `"${value}"`; + case "number": + case "boolean": + case "undefined": + return `${value}`; + } + return typeof value; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts new file mode 100644 index 00000000000..db82a722c35 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts @@ -0,0 +1,17 @@ +// borrowed from https://github.com/lodash/lodash/blob/master/isPlainObject.js +export function isPlainObject(value: unknown): value is Record { + if (typeof value !== "object" || value === null) { + return false; + } + + if (Object.getPrototypeOf(value) === null) { + return true; + } + + let proto = value; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + + return Object.getPrototypeOf(value) === proto; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/keys.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/keys.ts new file mode 100644 index 00000000000..01867098287 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/keys.ts @@ -0,0 +1,3 @@ +export function keys(object: T): (keyof T)[] { + return Object.keys(object) as (keyof T)[]; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/maybeSkipValidation.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/maybeSkipValidation.ts new file mode 100644 index 00000000000..86c07abf2b4 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/maybeSkipValidation.ts @@ -0,0 +1,38 @@ +import { BaseSchema, MaybeValid, SchemaOptions } from "../Schema"; + +export function maybeSkipValidation, Raw, Parsed>(schema: S): S { + return { + ...schema, + json: transformAndMaybeSkipValidation(schema.json), + parse: transformAndMaybeSkipValidation(schema.parse), + }; +} + +function transformAndMaybeSkipValidation( + transform: (value: unknown, opts?: SchemaOptions) => MaybeValid +): (value: unknown, opts?: SchemaOptions) => MaybeValid { + return (value, opts): MaybeValid => { + const transformed = transform(value, opts); + const { skipValidation = false } = opts ?? {}; + if (!transformed.ok && skipValidation) { + // eslint-disable-next-line no-console + console.warn( + [ + "Failed to validate.", + ...transformed.errors.map( + (error) => + " - " + + (error.path.length > 0 ? `${error.path.join(".")}: ${error.message}` : error.message) + ), + ].join("\n") + ); + + return { + ok: true, + value: value as T, + }; + } else { + return transformed; + } + }; +} diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/partition.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/partition.ts new file mode 100644 index 00000000000..f58d6f3d35f --- /dev/null +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/partition.ts @@ -0,0 +1,12 @@ +export function partition(items: readonly T[], predicate: (item: T) => boolean): [T[], T[]] { + const trueItems: T[] = [], + falseItems: T[] = []; + for (const item of items) { + if (predicate(item)) { + trueItems.push(item); + } else { + falseItems.push(item); + } + } + return [trueItems, falseItems]; +} diff --git a/seed/ts-express/mixed-file-directory/errors/SeedMixedFileDirectoryError.ts b/seed/ts-express/mixed-file-directory/errors/SeedMixedFileDirectoryError.ts new file mode 100644 index 00000000000..abd8e4b4da1 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/errors/SeedMixedFileDirectoryError.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import express from "express"; + +export abstract class SeedMixedFileDirectoryError extends Error { + constructor(public readonly errorName?: string) { + super(); + Object.setPrototypeOf(this, SeedMixedFileDirectoryError.prototype); + } + + public abstract send(res: express.Response): Promise; +} diff --git a/seed/ts-express/mixed-file-directory/errors/index.ts b/seed/ts-express/mixed-file-directory/errors/index.ts new file mode 100644 index 00000000000..6c5dfece4f3 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/errors/index.ts @@ -0,0 +1 @@ +export { SeedMixedFileDirectoryError } from "./SeedMixedFileDirectoryError"; diff --git a/seed/ts-express/mixed-file-directory/index.ts b/seed/ts-express/mixed-file-directory/index.ts new file mode 100644 index 00000000000..bc75b9c820d --- /dev/null +++ b/seed/ts-express/mixed-file-directory/index.ts @@ -0,0 +1,3 @@ +export * as SeedMixedFileDirectory from "./api"; +export { register } from "./register"; +export { SeedMixedFileDirectoryError } from "./errors"; diff --git a/seed/ts-express/mixed-file-directory/register.ts b/seed/ts-express/mixed-file-directory/register.ts new file mode 100644 index 00000000000..6658a100849 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/register.ts @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import express from "express"; +import { OrganizationService } from "./api/resources/organization/service/OrganizationService"; +import { UserService } from "./api/resources/user/service/UserService"; + +export function register( + expressApp: express.Express | express.Router, + services: { + organization: OrganizationService; + user: UserService; + } +): void { + (expressApp as any).use("/organizations", services.organization.toRouter()); + (expressApp as any).use("/users", services.user.toRouter()); + (expressApp as any).use("/users/events", services.user.events.toRouter()); + (expressApp as any).use("/users/events/metadata", services.user.events.metadata.toRouter()); +} diff --git a/seed/ts-express/mixed-file-directory/serialization/index.ts b/seed/ts-express/mixed-file-directory/serialization/index.ts new file mode 100644 index 00000000000..3ce0a3e38e8 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./resources"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/index.ts new file mode 100644 index 00000000000..f281c7d14eb --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/index.ts @@ -0,0 +1,4 @@ +export * as organization from "./organization"; +export * from "./organization/types"; +export * as user from "./user"; +export * from "./user/types"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/organization/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/organization/index.ts new file mode 100644 index 00000000000..eea524d6557 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/organization/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/CreateOrganizationRequest.ts b/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/CreateOrganizationRequest.ts new file mode 100644 index 00000000000..7dd01a9fbf2 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/CreateOrganizationRequest.ts @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as SeedMixedFileDirectory from "../../../../api/index"; +import * as core from "../../../../core"; + +export const CreateOrganizationRequest: core.serialization.ObjectSchema< + serializers.CreateOrganizationRequest.Raw, + SeedMixedFileDirectory.CreateOrganizationRequest +> = core.serialization.object({ + name: core.serialization.string(), +}); + +export declare namespace CreateOrganizationRequest { + interface Raw { + name: string; + } +} diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/Organization.ts b/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/Organization.ts new file mode 100644 index 00000000000..88e5b965186 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/Organization.ts @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as SeedMixedFileDirectory from "../../../../api/index"; +import * as core from "../../../../core"; + +export const Organization: core.serialization.ObjectSchema< + serializers.Organization.Raw, + SeedMixedFileDirectory.Organization +> = core.serialization.object({ + id: core.serialization.lazy(() => serializers.Id), + name: core.serialization.string(), + users: core.serialization.list(core.serialization.lazyObject(() => serializers.User)), +}); + +export declare namespace Organization { + interface Raw { + id: serializers.Id.Raw; + name: string; + users: serializers.User.Raw[]; + } +} diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/index.ts new file mode 100644 index 00000000000..c4b0dd7c2d6 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/organization/types/index.ts @@ -0,0 +1,2 @@ +export * from "./Organization"; +export * from "./CreateOrganizationRequest"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/index.ts new file mode 100644 index 00000000000..5ddb983439b --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./resources"; +export * from "./service"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/index.ts new file mode 100644 index 00000000000..5ddb983439b --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./resources"; +export * from "./service"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/index.ts new file mode 100644 index 00000000000..20085af104d --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/index.ts @@ -0,0 +1,2 @@ +export * as metadata from "./metadata"; +export * from "./metadata/types"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/index.ts new file mode 100644 index 00000000000..eea524d6557 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/types/Metadata.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/types/Metadata.ts new file mode 100644 index 00000000000..f44c8002a59 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/types/Metadata.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../../../../../index"; +import * as SeedMixedFileDirectory from "../../../../../../../../api/index"; +import * as core from "../../../../../../../../core"; + +export const Metadata: core.serialization.ObjectSchema< + serializers.user.events.Metadata.Raw, + SeedMixedFileDirectory.user.events.Metadata +> = core.serialization.object({ + id: core.serialization.lazy(() => serializers.Id), + value: core.serialization.unknown(), +}); + +export declare namespace Metadata { + interface Raw { + id: serializers.Id.Raw; + value?: unknown; + } +} diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/types/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/types/index.ts new file mode 100644 index 00000000000..8abb66966d0 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/resources/metadata/types/index.ts @@ -0,0 +1 @@ +export * from "./Metadata"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/service/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/service/index.ts new file mode 100644 index 00000000000..ad7c868f2b7 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/service/index.ts @@ -0,0 +1 @@ +export * as listEvents from "./listEvents"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/service/listEvents.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/service/listEvents.ts new file mode 100644 index 00000000000..3e63d7781ef --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/service/listEvents.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../../../index"; +import * as SeedMixedFileDirectory from "../../../../../../api/index"; +import * as core from "../../../../../../core"; + +export const Response: core.serialization.Schema< + serializers.user.events.listEvents.Response.Raw, + SeedMixedFileDirectory.user.Event[] +> = core.serialization.list(core.serialization.lazyObject(() => serializers.user.Event)); + +export declare namespace Response { + type Raw = serializers.user.Event.Raw[]; +} diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/types/Event.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/types/Event.ts new file mode 100644 index 00000000000..9ff9d957f90 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/types/Event.ts @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../../../index"; +import * as SeedMixedFileDirectory from "../../../../../../api/index"; +import * as core from "../../../../../../core"; + +export const Event: core.serialization.ObjectSchema = + core.serialization.object({ + id: core.serialization.lazy(() => serializers.Id), + name: core.serialization.string(), + }); + +export declare namespace Event { + interface Raw { + id: serializers.Id.Raw; + name: string; + } +} diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/types/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/types/index.ts new file mode 100644 index 00000000000..6868d665e48 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/events/types/index.ts @@ -0,0 +1 @@ +export * from "./Event"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/index.ts new file mode 100644 index 00000000000..f8858c12a24 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/resources/index.ts @@ -0,0 +1,2 @@ +export * as events from "./events"; +export * from "./events/types"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/service/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/service/index.ts new file mode 100644 index 00000000000..abbe30ae7ad --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/service/index.ts @@ -0,0 +1 @@ +export * as list from "./list"; diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/service/list.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/service/list.ts new file mode 100644 index 00000000000..70c5b6ff1bf --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/service/list.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as SeedMixedFileDirectory from "../../../../api/index"; +import * as core from "../../../../core"; + +export const Response: core.serialization.Schema = + core.serialization.list(core.serialization.lazyObject(() => serializers.User)); + +export declare namespace Response { + type Raw = serializers.User.Raw[]; +} diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/types/User.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/types/User.ts new file mode 100644 index 00000000000..e6df171ee8f --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/types/User.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as SeedMixedFileDirectory from "../../../../api/index"; +import * as core from "../../../../core"; + +export const User: core.serialization.ObjectSchema = + core.serialization.object({ + id: core.serialization.lazy(() => serializers.Id), + name: core.serialization.string(), + age: core.serialization.number(), + }); + +export declare namespace User { + interface Raw { + id: serializers.Id.Raw; + name: string; + age: number; + } +} diff --git a/seed/ts-express/mixed-file-directory/serialization/resources/user/types/index.ts b/seed/ts-express/mixed-file-directory/serialization/resources/user/types/index.ts new file mode 100644 index 00000000000..3ce758c1197 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/resources/user/types/index.ts @@ -0,0 +1 @@ +export * from "./User"; diff --git a/seed/ts-express/mixed-file-directory/serialization/types/Id.ts b/seed/ts-express/mixed-file-directory/serialization/types/Id.ts new file mode 100644 index 00000000000..51c5b259c49 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/types/Id.ts @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../index"; +import * as SeedMixedFileDirectory from "../../api/index"; +import * as core from "../../core"; + +export const Id: core.serialization.Schema = core.serialization.string(); + +export declare namespace Id { + type Raw = string; +} diff --git a/seed/ts-express/mixed-file-directory/serialization/types/index.ts b/seed/ts-express/mixed-file-directory/serialization/types/index.ts new file mode 100644 index 00000000000..6823c3ab871 --- /dev/null +++ b/seed/ts-express/mixed-file-directory/serialization/types/index.ts @@ -0,0 +1 @@ +export * from "./Id"; diff --git a/seed/ts-express/mixed-file-directory/snippet-templates.json b/seed/ts-express/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/ts-express/mixed-file-directory/snippet.json b/seed/ts-express/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/ts-sdk/mixed-file-directory/.github/workflows/ci.yml b/seed/ts-sdk/mixed-file-directory/.github/workflows/ci.yml new file mode 100644 index 00000000000..b64a6cbbb4a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up node + uses: actions/setup-node@v3 + + - name: Compile + run: yarn && yarn build + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up node + uses: actions/setup-node@v3 + + - name: Compile + run: yarn && yarn test + + publish: + needs: [ compile, test ] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up node + uses: actions/setup-node@v3 + - name: Install dependencies + run: yarn install + - name: Build + run: yarn build + + - name: Publish to npm + run: | + npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} + if [[ ${GITHUB_REF} == *alpha* ]]; then + npm publish --access public --tag alpha + elif [[ ${GITHUB_REF} == *beta* ]]; then + npm publish --access public --tag beta + else + npm publish --access public + fi + env: + NPM_TOKEN: ${{ secrets. }} \ No newline at end of file diff --git a/seed/ts-sdk/mixed-file-directory/.gitignore b/seed/ts-sdk/mixed-file-directory/.gitignore new file mode 100644 index 00000000000..72271e049c0 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +/dist \ No newline at end of file diff --git a/seed/ts-sdk/mixed-file-directory/.mock/definition/__package__.yml b/seed/ts-sdk/mixed-file-directory/.mock/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.mock/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/seed/ts-sdk/mixed-file-directory/.mock/definition/api.yml b/seed/ts-sdk/mixed-file-directory/.mock/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.mock/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/seed/ts-sdk/mixed-file-directory/.mock/definition/organization.yml b/seed/ts-sdk/mixed-file-directory/.mock/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.mock/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/seed/ts-sdk/mixed-file-directory/.mock/definition/user.yml b/seed/ts-sdk/mixed-file-directory/.mock/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.mock/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/ts-sdk/mixed-file-directory/.mock/definition/user/events.yml b/seed/ts-sdk/mixed-file-directory/.mock/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.mock/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/seed/ts-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml b/seed/ts-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.mock/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/seed/ts-sdk/mixed-file-directory/.mock/fern.config.json b/seed/ts-sdk/mixed-file-directory/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/ts-sdk/mixed-file-directory/.mock/generators.yml b/seed/ts-sdk/mixed-file-directory/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/ts-sdk/mixed-file-directory/.npmignore b/seed/ts-sdk/mixed-file-directory/.npmignore new file mode 100644 index 00000000000..6db0876c41c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.npmignore @@ -0,0 +1,9 @@ +node_modules +src +tests +.gitignore +.github +.fernignore +.prettierrc.yml +tsconfig.json +yarn.lock \ No newline at end of file diff --git a/seed/ts-sdk/mixed-file-directory/.prettierrc.yml b/seed/ts-sdk/mixed-file-directory/.prettierrc.yml new file mode 100644 index 00000000000..0c06786bf53 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/.prettierrc.yml @@ -0,0 +1,2 @@ +tabWidth: 4 +printWidth: 120 diff --git a/seed/ts-sdk/mixed-file-directory/README.md b/seed/ts-sdk/mixed-file-directory/README.md new file mode 100644 index 00000000000..f9f203d5fbd --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/README.md @@ -0,0 +1,137 @@ +# Seed TypeScript Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-SDK%20generated%20by%20Fern-brightgreen)](https://github.com/fern-api/fern) +[![npm shield](https://img.shields.io/npm/v/@fern/mixed-file-directory)](https://www.npmjs.com/package/@fern/mixed-file-directory) + +The Seed TypeScript library provides convenient access to the Seed API from TypeScript. + +## Installation + +```sh +npm i -s @fern/mixed-file-directory +``` + +## Usage + +Instantiate and use the client with the following: + +```typescript +import { SeedMixedFileDirectoryClient } from "@fern/mixed-file-directory"; + +const client = new SeedMixedFileDirectoryClient({ environment: "YOUR_BASE_URL" }); +await client.organization.create({ + name: "string", +}); +``` + +## Request And Response Types + +The SDK exports all request and response types as TypeScript interfaces. Simply import them with the +following namespace: + +```typescript +import { SeedMixedFileDirectory } from "@fern/mixed-file-directory"; + +const request: SeedMixedFileDirectory.ListUsersRequest = { + ... +}; +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```typescript +import { SeedMixedFileDirectoryError } from "@fern/mixed-file-directory"; + +try { + await client.organization.create(...); +} catch (err) { + if (err instanceof SeedMixedFileDirectoryError) { + console.log(err.statusCode); + console.log(err.message); + console.log(err.body); + } +} +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retriable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retriable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `maxRetries` request option to configure this behavior. + +```typescript +const response = await client.organization.create(..., { + maxRetries: 0 // override maxRetries at the request level +}); +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. Use the `timeoutInSeconds` option to configure this behavior. + +```typescript +const response = await client.organization.create(..., { + timeoutInSeconds: 30 // override timeout to 30s +}); +``` + +### Aborting Requests + +The SDK allows users to abort requests at any point by passing in an abort signal. + +```typescript +const controller = new AbortController(); +const response = await client.organization.create(..., { + abortSignal: controller.signal +}); +controller.abort(); // aborts the request +``` + +### Runtime Compatibility + +The SDK defaults to `node-fetch` but will use the global fetch client if present. The SDK works in the following +runtimes: + +- Node.js 18+ +- Vercel +- Cloudflare Workers +- Deno v1.25+ +- Bun 1.0+ +- React Native + +### Customizing Fetch Client + +The SDK provides a way for your to customize the underlying HTTP client / Fetch function. If you're running in an +unsupported environment, this provides a way for you to break glass and ensure the SDK works. + +```typescript +import { SeedMixedFileDirectoryClient } from "@fern/mixed-file-directory"; + +const client = new SeedMixedFileDirectoryClient({ + ... + fetcher: // provide your implementation here +}); +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/ts-sdk/mixed-file-directory/jest.config.js b/seed/ts-sdk/mixed-file-directory/jest.config.js new file mode 100644 index 00000000000..35d6e65bf93 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", +}; diff --git a/seed/ts-sdk/mixed-file-directory/package.json b/seed/ts-sdk/mixed-file-directory/package.json new file mode 100644 index 00000000000..cf66f252a1a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/package.json @@ -0,0 +1,43 @@ +{ + "name": "@fern/mixed-file-directory", + "version": "0.0.1", + "private": false, + "repository": "https://github.com/mixed-file-directory/fern", + "main": "./index.js", + "types": "./index.d.ts", + "scripts": { + "format": "prettier . --write --ignore-unknown", + "build": "tsc", + "prepack": "cp -rv dist/. .", + "test": "jest" + }, + "dependencies": { + "url-join": "4.0.1", + "form-data": "^4.0.0", + "formdata-node": "^6.0.3", + "node-fetch": "2.7.0", + "qs": "6.11.2", + "readable-stream": "^4.5.2" + }, + "devDependencies": { + "@types/url-join": "4.0.1", + "@types/qs": "6.9.8", + "@types/node-fetch": "2.6.9", + "@types/readable-stream": "^4.0.15", + "fetch-mock-jest": "^1.5.1", + "webpack": "^5.94.0", + "ts-loader": "^9.3.1", + "jest": "29.7.0", + "@types/jest": "29.5.5", + "ts-jest": "29.1.1", + "jest-environment-jsdom": "29.7.0", + "@types/node": "17.0.33", + "prettier": "2.7.1", + "typescript": "4.6.4" + }, + "browser": { + "fs": false, + "os": false, + "path": false + } +} diff --git a/seed/ts-sdk/mixed-file-directory/reference.md b/seed/ts-sdk/mixed-file-directory/reference.md new file mode 100644 index 00000000000..ed550a9a713 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/reference.md @@ -0,0 +1,269 @@ +# Reference + +## Organization + +

client.organization.create({ ...params }) -> SeedMixedFileDirectory.Organization +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a new organization. + +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.organization.create({ + name: "string", +}); +``` + +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `SeedMixedFileDirectory.CreateOrganizationRequest` + +
+
+ +
+
+ +**requestOptions:** `Organization.RequestOptions` + +
+
+
+
+ +
+
+
+ +## User + +
client.user.list({ ...params }) -> SeedMixedFileDirectory.User[] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List all users. + +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.user.list({ + limit: 1, +}); +``` + +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `SeedMixedFileDirectory.ListUsersRequest` + +
+
+ +
+
+ +**requestOptions:** `User.RequestOptions` + +
+
+
+
+ +
+
+
+ +## User Events + +
client.user.events.listEvents({ ...params }) -> SeedMixedFileDirectory.Event[] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List all user events. + +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.user.events.listEvents({ + limit: 1, +}); +``` + +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `SeedMixedFileDirectory.user.ListUserEventsRequest` + +
+
+ +
+
+ +**requestOptions:** `Events.RequestOptions` + +
+
+
+
+ +
+
+
+ +## User Events Metadata + +
client.user.events.metadata.getMetadata({ ...params }) -> SeedMixedFileDirectory.Metadata +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get event metadata. + +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.user.events.metadata.getMetadata({ + id: "string", +}); +``` + +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `SeedMixedFileDirectory.user.events.GetEventMetadataRequest` + +
+
+ +
+
+ +**requestOptions:** `Metadata.RequestOptions` + +
+
+
+
+ +
+
+
diff --git a/seed/ts-sdk/mixed-file-directory/snippet-templates.json b/seed/ts-sdk/mixed-file-directory/snippet-templates.json new file mode 100644 index 00000000000..024792ab79a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/snippet-templates.json @@ -0,0 +1,350 @@ +[ + { + "sdk": { + "package": "@fern/mixed-file-directory", + "version": "0.0.1", + "type": "typescript" + }, + "endpointId": { + "path": "/organizations/", + "method": "POST", + "identifierOverride": "endpoint_organization.create" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "import { SeedMixedFileDirectoryClient } from \"@fern/mixed-file-directory\";" + ], + "templateString": "const client = new SeedMixedFileDirectoryClient($FERN_INPUT);", + "isOptional": false, + "inputDelimiter": ",", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{ $FERN_INPUT }", + "isOptional": true, + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "environment: \"YOUR_BASE_URL\"", + "isOptional": false, + "templateInputs": [], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "templateString": "await client.organization.create(\n\t$FERN_INPUT\n)", + "isOptional": false, + "inputDelimiter": ",\n\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{\n\t\t$FERN_INPUT\n\t}", + "isOptional": true, + "inputDelimiter": ",\n\t\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{\n\t\t\t$FERN_INPUT\n\t\t}", + "isOptional": true, + "inputDelimiter": ",\n\t\t\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "name: $FERN_INPUT", + "isOptional": true, + "templateInputs": [ + { + "location": "BODY", + "path": "name", + "type": "payload" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "v1" + } + }, + { + "sdk": { + "package": "@fern/mixed-file-directory", + "version": "0.0.1", + "type": "typescript" + }, + "endpointId": { + "path": "/users/", + "method": "GET", + "identifierOverride": "endpoint_user.list" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "import { SeedMixedFileDirectoryClient } from \"@fern/mixed-file-directory\";" + ], + "templateString": "const client = new SeedMixedFileDirectoryClient($FERN_INPUT);", + "isOptional": false, + "inputDelimiter": ",", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{ $FERN_INPUT }", + "isOptional": true, + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "environment: \"YOUR_BASE_URL\"", + "isOptional": false, + "templateInputs": [], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "templateString": "await client.user.list(\n\t$FERN_INPUT\n)", + "isOptional": false, + "inputDelimiter": ",\n\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{\n\t\t$FERN_INPUT\n\t}", + "isOptional": true, + "inputDelimiter": ",\n\t\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "limit: $FERN_INPUT", + "isOptional": true, + "templateInputs": [ + { + "location": "QUERY", + "path": "limit", + "type": "payload" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "v1" + } + }, + { + "sdk": { + "package": "@fern/mixed-file-directory", + "version": "0.0.1", + "type": "typescript" + }, + "endpointId": { + "path": "/users/events/", + "method": "GET", + "identifierOverride": "endpoint_user/events.listEvents" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "import { SeedMixedFileDirectoryClient } from \"@fern/mixed-file-directory\";" + ], + "templateString": "const client = new SeedMixedFileDirectoryClient($FERN_INPUT);", + "isOptional": false, + "inputDelimiter": ",", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{ $FERN_INPUT }", + "isOptional": true, + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "environment: \"YOUR_BASE_URL\"", + "isOptional": false, + "templateInputs": [], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "templateString": "await client.user.events.listEvents(\n\t$FERN_INPUT\n)", + "isOptional": false, + "inputDelimiter": ",\n\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{\n\t\t$FERN_INPUT\n\t}", + "isOptional": true, + "inputDelimiter": ",\n\t\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "limit: $FERN_INPUT", + "isOptional": true, + "templateInputs": [ + { + "location": "QUERY", + "path": "limit", + "type": "payload" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "v1" + } + }, + { + "sdk": { + "package": "@fern/mixed-file-directory", + "version": "0.0.1", + "type": "typescript" + }, + "endpointId": { + "path": "/users/events/metadata/", + "method": "GET", + "identifierOverride": "endpoint_user/events/metadata.getMetadata" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "import { SeedMixedFileDirectoryClient } from \"@fern/mixed-file-directory\";" + ], + "templateString": "const client = new SeedMixedFileDirectoryClient($FERN_INPUT);", + "isOptional": false, + "inputDelimiter": ",", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{ $FERN_INPUT }", + "isOptional": true, + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "environment: \"YOUR_BASE_URL\"", + "isOptional": false, + "templateInputs": [], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "templateString": "await client.user.events.metadata.getMetadata(\n\t$FERN_INPUT\n)", + "isOptional": false, + "inputDelimiter": ",\n\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "{\n\t\t$FERN_INPUT\n\t}", + "isOptional": true, + "inputDelimiter": ",\n\t\t", + "templateInputs": [ + { + "value": { + "imports": [], + "templateString": "id: $FERN_INPUT", + "isOptional": true, + "templateInputs": [ + { + "location": "QUERY", + "path": "id", + "type": "payload" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "template" + } + ], + "type": "generic" + }, + "type": "v1" + } + } +] \ No newline at end of file diff --git a/seed/ts-sdk/mixed-file-directory/snippet.json b/seed/ts-sdk/mixed-file-directory/snippet.json new file mode 100644 index 00000000000..a98bcbd47e8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/snippet.json @@ -0,0 +1,49 @@ +{ + "endpoints": [ + { + "id": { + "path": "/organizations/", + "method": "POST", + "identifier_override": "endpoint_organization.create" + }, + "snippet": { + "type": "typescript", + "client": "import { SeedMixedFileDirectoryClient } from \"@fern/mixed-file-directory\";\n\nconst client = new SeedMixedFileDirectoryClient({ environment: \"YOUR_BASE_URL\" });\nawait client.organization.create({\n name: \"string\"\n});\n" + } + }, + { + "id": { + "path": "/users/", + "method": "GET", + "identifier_override": "endpoint_user.list" + }, + "snippet": { + "type": "typescript", + "client": "import { SeedMixedFileDirectoryClient } from \"@fern/mixed-file-directory\";\n\nconst client = new SeedMixedFileDirectoryClient({ environment: \"YOUR_BASE_URL\" });\nawait client.user.list({\n limit: 1\n});\n" + } + }, + { + "id": { + "path": "/users/events/", + "method": "GET", + "identifier_override": "endpoint_user/events.listEvents" + }, + "snippet": { + "type": "typescript", + "client": "import { SeedMixedFileDirectoryClient } from \"@fern/mixed-file-directory\";\n\nconst client = new SeedMixedFileDirectoryClient({ environment: \"YOUR_BASE_URL\" });\nawait client.user.events.listEvents({\n limit: 1\n});\n" + } + }, + { + "id": { + "path": "/users/events/metadata/", + "method": "GET", + "identifier_override": "endpoint_user/events/metadata.getMetadata" + }, + "snippet": { + "type": "typescript", + "client": "import { SeedMixedFileDirectoryClient } from \"@fern/mixed-file-directory\";\n\nconst client = new SeedMixedFileDirectoryClient({ environment: \"YOUR_BASE_URL\" });\nawait client.user.events.metadata.getMetadata({\n id: \"string\"\n});\n" + } + } + ], + "types": {} +} \ No newline at end of file diff --git a/seed/ts-sdk/mixed-file-directory/src/Client.ts b/seed/ts-sdk/mixed-file-directory/src/Client.ts new file mode 100644 index 00000000000..43b10e98105 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/Client.ts @@ -0,0 +1,38 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as core from "./core"; +import { Organization } from "./api/resources/organization/client/Client"; +import { User } from "./api/resources/user/client/Client"; + +export declare namespace SeedMixedFileDirectoryClient { + interface Options { + environment: core.Supplier; + } + + interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + } +} + +export class SeedMixedFileDirectoryClient { + constructor(protected readonly _options: SeedMixedFileDirectoryClient.Options) {} + + protected _organization: Organization | undefined; + + public get organization(): Organization { + return (this._organization ??= new Organization(this._options)); + } + + protected _user: User | undefined; + + public get user(): User { + return (this._user ??= new User(this._options)); + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/index.ts new file mode 100644 index 00000000000..3ce0a3e38e8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./resources"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/index.ts new file mode 100644 index 00000000000..46f06f4a4c5 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/index.ts @@ -0,0 +1,5 @@ +export * as organization from "./organization"; +export * from "./organization/types"; +export * as user from "./user"; +export * from "./user/types"; +export * from "./user/client/requests"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/client/Client.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/client/Client.ts new file mode 100644 index 00000000000..0fbebc0aa26 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/client/Client.ts @@ -0,0 +1,92 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as core from "../../../../core"; +import * as SeedMixedFileDirectory from "../../../index"; +import * as serializers from "../../../../serialization/index"; +import urlJoin from "url-join"; +import * as errors from "../../../../errors/index"; + +export declare namespace Organization { + interface Options { + environment: core.Supplier; + } + + interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + } +} + +export class Organization { + constructor(protected readonly _options: Organization.Options) {} + + /** + * Create a new organization. + * + * @param {SeedMixedFileDirectory.CreateOrganizationRequest} request + * @param {Organization.RequestOptions} requestOptions - Request-specific configuration. + * + * @example + * await client.organization.create({ + * name: "string" + * }) + */ + public async create( + request: SeedMixedFileDirectory.CreateOrganizationRequest, + requestOptions?: Organization.RequestOptions + ): Promise { + const _response = await core.fetcher({ + url: urlJoin(await core.Supplier.get(this._options.environment), "/organizations/"), + method: "POST", + headers: { + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@fern/mixed-file-directory", + "X-Fern-SDK-Version": "0.0.1", + "User-Agent": "@fern/mixed-file-directory/0.0.1", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + }, + contentType: "application/json", + requestType: "json", + body: serializers.CreateOrganizationRequest.jsonOrThrow(request, { unrecognizedObjectKeys: "strip" }), + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + maxRetries: requestOptions?.maxRetries, + abortSignal: requestOptions?.abortSignal, + }); + if (_response.ok) { + return serializers.Organization.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + throw new errors.SeedMixedFileDirectoryError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SeedMixedFileDirectoryError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SeedMixedFileDirectoryTimeoutError(); + case "unknown": + throw new errors.SeedMixedFileDirectoryError({ + message: _response.error.errorMessage, + }); + } + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/client/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/client/index.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/client/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/index.ts new file mode 100644 index 00000000000..c9240f83b48 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./client"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/CreateOrganizationRequest.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/CreateOrganizationRequest.ts new file mode 100644 index 00000000000..8b5c5097ade --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/CreateOrganizationRequest.ts @@ -0,0 +1,7 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface CreateOrganizationRequest { + name: string; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/Organization.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/Organization.ts new file mode 100644 index 00000000000..7daf3aa7807 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/Organization.ts @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../index"; + +export interface Organization { + id: SeedMixedFileDirectory.Id; + name: string; + users: SeedMixedFileDirectory.User[]; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/index.ts new file mode 100644 index 00000000000..c4b0dd7c2d6 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/organization/types/index.ts @@ -0,0 +1,2 @@ +export * from "./Organization"; +export * from "./CreateOrganizationRequest"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/Client.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/Client.ts new file mode 100644 index 00000000000..1e495fb9834 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/Client.ts @@ -0,0 +1,105 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as core from "../../../../core"; +import * as SeedMixedFileDirectory from "../../../index"; +import urlJoin from "url-join"; +import * as serializers from "../../../../serialization/index"; +import * as errors from "../../../../errors/index"; +import { Events } from "../resources/events/client/Client"; + +export declare namespace User { + interface Options { + environment: core.Supplier; + } + + interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + } +} + +export class User { + constructor(protected readonly _options: User.Options) {} + + /** + * List all users. + * + * @param {SeedMixedFileDirectory.ListUsersRequest} request + * @param {User.RequestOptions} requestOptions - Request-specific configuration. + * + * @example + * await client.user.list({ + * limit: 1 + * }) + */ + public async list( + request: SeedMixedFileDirectory.ListUsersRequest = {}, + requestOptions?: User.RequestOptions + ): Promise { + const { limit } = request; + const _queryParams: Record = {}; + if (limit != null) { + _queryParams["limit"] = limit.toString(); + } + + const _response = await core.fetcher({ + url: urlJoin(await core.Supplier.get(this._options.environment), "/users/"), + method: "GET", + headers: { + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@fern/mixed-file-directory", + "X-Fern-SDK-Version": "0.0.1", + "User-Agent": "@fern/mixed-file-directory/0.0.1", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + }, + contentType: "application/json", + queryParameters: _queryParams, + requestType: "json", + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + maxRetries: requestOptions?.maxRetries, + abortSignal: requestOptions?.abortSignal, + }); + if (_response.ok) { + return serializers.user.list.Response.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + throw new errors.SeedMixedFileDirectoryError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SeedMixedFileDirectoryError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SeedMixedFileDirectoryTimeoutError(); + case "unknown": + throw new errors.SeedMixedFileDirectoryError({ + message: _response.error.errorMessage, + }); + } + } + + protected _events: Events | undefined; + + public get events(): Events { + return (this._events ??= new Events(this._options)); + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/index.ts new file mode 100644 index 00000000000..415726b7fea --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/index.ts @@ -0,0 +1 @@ +export * from "./requests"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/requests/ListUsersRequest.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/requests/ListUsersRequest.ts new file mode 100644 index 00000000000..fc8ff4332f6 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/requests/ListUsersRequest.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +/** + * @example + * { + * limit: 1 + * } + */ +export interface ListUsersRequest { + /** + * The maximum number of results to return. + */ + limit?: number; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/requests/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/requests/index.ts new file mode 100644 index 00000000000..0c3357f7dfb --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/client/requests/index.ts @@ -0,0 +1 @@ +export { type ListUsersRequest } from "./ListUsersRequest"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/index.ts new file mode 100644 index 00000000000..a931b36375c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./resources"; +export * from "./client"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/Client.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/Client.ts new file mode 100644 index 00000000000..e8e1e20f56f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/Client.ts @@ -0,0 +1,105 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as core from "../../../../../../core"; +import * as SeedMixedFileDirectory from "../../../../../index"; +import urlJoin from "url-join"; +import * as serializers from "../../../../../../serialization/index"; +import * as errors from "../../../../../../errors/index"; +import { Metadata } from "../resources/metadata/client/Client"; + +export declare namespace Events { + interface Options { + environment: core.Supplier; + } + + interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + } +} + +export class Events { + constructor(protected readonly _options: Events.Options) {} + + /** + * List all user events. + * + * @param {SeedMixedFileDirectory.user.ListUserEventsRequest} request + * @param {Events.RequestOptions} requestOptions - Request-specific configuration. + * + * @example + * await client.user.events.listEvents({ + * limit: 1 + * }) + */ + public async listEvents( + request: SeedMixedFileDirectory.user.ListUserEventsRequest = {}, + requestOptions?: Events.RequestOptions + ): Promise { + const { limit } = request; + const _queryParams: Record = {}; + if (limit != null) { + _queryParams["limit"] = limit.toString(); + } + + const _response = await core.fetcher({ + url: urlJoin(await core.Supplier.get(this._options.environment), "/users/events/"), + method: "GET", + headers: { + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@fern/mixed-file-directory", + "X-Fern-SDK-Version": "0.0.1", + "User-Agent": "@fern/mixed-file-directory/0.0.1", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + }, + contentType: "application/json", + queryParameters: _queryParams, + requestType: "json", + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + maxRetries: requestOptions?.maxRetries, + abortSignal: requestOptions?.abortSignal, + }); + if (_response.ok) { + return serializers.user.events.listEvents.Response.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + throw new errors.SeedMixedFileDirectoryError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SeedMixedFileDirectoryError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SeedMixedFileDirectoryTimeoutError(); + case "unknown": + throw new errors.SeedMixedFileDirectoryError({ + message: _response.error.errorMessage, + }); + } + } + + protected _metadata: Metadata | undefined; + + public get metadata(): Metadata { + return (this._metadata ??= new Metadata(this._options)); + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/index.ts new file mode 100644 index 00000000000..415726b7fea --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/index.ts @@ -0,0 +1 @@ +export * from "./requests"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/requests/ListUserEventsRequest.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/requests/ListUserEventsRequest.ts new file mode 100644 index 00000000000..fcadb44981a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/requests/ListUserEventsRequest.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +/** + * @example + * { + * limit: 1 + * } + */ +export interface ListUserEventsRequest { + /** + * The maximum number of results to return. + */ + limit?: number; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/requests/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/requests/index.ts new file mode 100644 index 00000000000..8f7b333102e --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/client/requests/index.ts @@ -0,0 +1 @@ +export { type ListUserEventsRequest } from "./ListUserEventsRequest"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/index.ts new file mode 100644 index 00000000000..a931b36375c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./resources"; +export * from "./client"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/index.ts new file mode 100644 index 00000000000..2bc01d30d67 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/index.ts @@ -0,0 +1,3 @@ +export * as metadata from "./metadata"; +export * from "./metadata/types"; +export * from "./metadata/client/requests"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/Client.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/Client.ts new file mode 100644 index 00000000000..1fb76cb63a2 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/Client.ts @@ -0,0 +1,95 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as core from "../../../../../../../../core"; +import * as SeedMixedFileDirectory from "../../../../../../../index"; +import urlJoin from "url-join"; +import * as serializers from "../../../../../../../../serialization/index"; +import * as errors from "../../../../../../../../errors/index"; + +export declare namespace Metadata { + interface Options { + environment: core.Supplier; + } + + interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + } +} + +export class Metadata { + constructor(protected readonly _options: Metadata.Options) {} + + /** + * Get event metadata. + * + * @param {SeedMixedFileDirectory.user.events.GetEventMetadataRequest} request + * @param {Metadata.RequestOptions} requestOptions - Request-specific configuration. + * + * @example + * await client.user.events.metadata.getMetadata({ + * id: "string" + * }) + */ + public async getMetadata( + request: SeedMixedFileDirectory.user.events.GetEventMetadataRequest, + requestOptions?: Metadata.RequestOptions + ): Promise { + const { id } = request; + const _queryParams: Record = {}; + _queryParams["id"] = id; + const _response = await core.fetcher({ + url: urlJoin(await core.Supplier.get(this._options.environment), "/users/events/metadata/"), + method: "GET", + headers: { + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@fern/mixed-file-directory", + "X-Fern-SDK-Version": "0.0.1", + "User-Agent": "@fern/mixed-file-directory/0.0.1", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + }, + contentType: "application/json", + queryParameters: _queryParams, + requestType: "json", + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + maxRetries: requestOptions?.maxRetries, + abortSignal: requestOptions?.abortSignal, + }); + if (_response.ok) { + return serializers.user.events.Metadata.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + throw new errors.SeedMixedFileDirectoryError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SeedMixedFileDirectoryError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SeedMixedFileDirectoryTimeoutError(); + case "unknown": + throw new errors.SeedMixedFileDirectoryError({ + message: _response.error.errorMessage, + }); + } + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/index.ts new file mode 100644 index 00000000000..415726b7fea --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/index.ts @@ -0,0 +1 @@ +export * from "./requests"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/requests/GetEventMetadataRequest.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/requests/GetEventMetadataRequest.ts new file mode 100644 index 00000000000..904df15bac8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/requests/GetEventMetadataRequest.ts @@ -0,0 +1,15 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../../../../../../index"; + +/** + * @example + * { + * id: "string" + * } + */ +export interface GetEventMetadataRequest { + id: SeedMixedFileDirectory.Id; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/requests/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/requests/index.ts new file mode 100644 index 00000000000..c4e2f5a60c0 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/client/requests/index.ts @@ -0,0 +1 @@ +export { type GetEventMetadataRequest } from "./GetEventMetadataRequest"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/index.ts new file mode 100644 index 00000000000..c9240f83b48 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./client"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/types/Metadata.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/types/Metadata.ts new file mode 100644 index 00000000000..a38ef1059ed --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/types/Metadata.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../../../../../index"; + +export interface Metadata { + id: SeedMixedFileDirectory.Id; + value?: unknown; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/types/index.ts new file mode 100644 index 00000000000..8abb66966d0 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/resources/metadata/types/index.ts @@ -0,0 +1 @@ +export * from "./Metadata"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/types/Event.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/types/Event.ts new file mode 100644 index 00000000000..811dfcff1dd --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/types/Event.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../../../index"; + +export interface Event { + id: SeedMixedFileDirectory.Id; + name: string; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/types/index.ts new file mode 100644 index 00000000000..6868d665e48 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/events/types/index.ts @@ -0,0 +1 @@ +export * from "./Event"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/index.ts new file mode 100644 index 00000000000..111471c9a53 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/resources/index.ts @@ -0,0 +1,3 @@ +export * as events from "./events"; +export * from "./events/types"; +export * from "./events/client/requests"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/types/User.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/types/User.ts new file mode 100644 index 00000000000..6f6ec03a072 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/types/User.ts @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as SeedMixedFileDirectory from "../../../index"; + +export interface User { + id: SeedMixedFileDirectory.Id; + name: string; + age: number; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/api/resources/user/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/types/index.ts new file mode 100644 index 00000000000..3ce758c1197 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/resources/user/types/index.ts @@ -0,0 +1 @@ +export * from "./User"; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/types/Id.ts b/seed/ts-sdk/mixed-file-directory/src/api/types/Id.ts new file mode 100644 index 00000000000..f96abc0c746 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/types/Id.ts @@ -0,0 +1,5 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export type Id = string; diff --git a/seed/ts-sdk/mixed-file-directory/src/api/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/api/types/index.ts new file mode 100644 index 00000000000..6823c3ab871 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/api/types/index.ts @@ -0,0 +1 @@ +export * from "./Id"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/APIResponse.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/APIResponse.ts new file mode 100644 index 00000000000..3664d09e168 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/APIResponse.ts @@ -0,0 +1,12 @@ +export type APIResponse = SuccessfulResponse | FailedResponse; + +export interface SuccessfulResponse { + ok: true; + body: T; + headers?: Record; +} + +export interface FailedResponse { + ok: false; + error: T; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Fetcher.ts new file mode 100644 index 00000000000..d67bc042107 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Fetcher.ts @@ -0,0 +1,143 @@ +import { APIResponse } from "./APIResponse"; +import { createRequestUrl } from "./createRequestUrl"; +import { getFetchFn } from "./getFetchFn"; +import { getRequestBody } from "./getRequestBody"; +import { getResponseBody } from "./getResponseBody"; +import { makeRequest } from "./makeRequest"; +import { requestWithRetries } from "./requestWithRetries"; + +export type FetchFunction = (args: Fetcher.Args) => Promise>; + +export declare namespace Fetcher { + export interface Args { + url: string; + method: string; + contentType?: string; + headers?: Record; + queryParameters?: Record; + body?: unknown; + timeoutMs?: number; + maxRetries?: number; + withCredentials?: boolean; + abortSignal?: AbortSignal; + requestType?: "json" | "file" | "bytes"; + responseType?: "json" | "blob" | "sse" | "streaming" | "text"; + duplex?: "half"; + } + + export type Error = FailedStatusCodeError | NonJsonError | TimeoutError | UnknownError; + + export interface FailedStatusCodeError { + reason: "status-code"; + statusCode: number; + body: unknown; + } + + export interface NonJsonError { + reason: "non-json"; + statusCode: number; + rawBody: string; + } + + export interface TimeoutError { + reason: "timeout"; + } + + export interface UnknownError { + reason: "unknown"; + errorMessage: string; + } +} + +export async function fetcherImpl(args: Fetcher.Args): Promise> { + const headers: Record = {}; + if (args.body !== undefined && args.contentType != null) { + headers["Content-Type"] = args.contentType; + } + + if (args.headers != null) { + for (const [key, value] of Object.entries(args.headers)) { + if (value != null) { + headers[key] = value; + } + } + } + + const url = createRequestUrl(args.url, args.queryParameters); + let requestBody: BodyInit | undefined = await getRequestBody({ + body: args.body, + type: args.requestType === "json" ? "json" : "other", + }); + const fetchFn = await getFetchFn(); + + try { + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + url, + args.method, + headers, + requestBody, + args.timeoutMs, + args.abortSignal, + args.withCredentials, + args.duplex + ), + args.maxRetries + ); + let responseBody = await getResponseBody(response, args.responseType); + + if (response.status >= 200 && response.status < 400) { + return { + ok: true, + body: responseBody as R, + headers: response.headers, + }; + } else { + return { + ok: false, + error: { + reason: "status-code", + statusCode: response.status, + body: responseBody, + }, + }; + } + } catch (error) { + if (args.abortSignal != null && args.abortSignal.aborted) { + return { + ok: false, + error: { + reason: "unknown", + errorMessage: "The user aborted a request", + }, + }; + } else if (error instanceof Error && error.name === "AbortError") { + return { + ok: false, + error: { + reason: "timeout", + }, + }; + } else if (error instanceof Error) { + return { + ok: false, + error: { + reason: "unknown", + errorMessage: error.message, + }, + }; + } + + return { + ok: false, + error: { + reason: "unknown", + errorMessage: JSON.stringify(error), + }, + }; + } +} + +export const fetcher: FetchFunction = fetcherImpl; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Supplier.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Supplier.ts new file mode 100644 index 00000000000..867c931c02f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Supplier.ts @@ -0,0 +1,11 @@ +export type Supplier = T | Promise | (() => T | Promise); + +export const Supplier = { + get: async (supplier: Supplier): Promise => { + if (typeof supplier === "function") { + return (supplier as () => T)(); + } else { + return supplier; + } + }, +}; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/createRequestUrl.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/createRequestUrl.ts new file mode 100644 index 00000000000..9288a99bb22 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/createRequestUrl.ts @@ -0,0 +1,10 @@ +import qs from "qs"; + +export function createRequestUrl( + baseUrl: string, + queryParameters?: Record +): string { + return Object.keys(queryParameters ?? {}).length > 0 + ? `${baseUrl}?${qs.stringify(queryParameters, { arrayFormat: "repeat" })}` + : baseUrl; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getFetchFn.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getFetchFn.ts new file mode 100644 index 00000000000..9fd9bfc42bd --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getFetchFn.ts @@ -0,0 +1,25 @@ +import { RUNTIME } from "../runtime"; + +/** + * Returns a fetch function based on the runtime + */ +export async function getFetchFn(): Promise { + // In Node.js 18+ environments, use native fetch + if (RUNTIME.type === "node" && RUNTIME.parsedVersion != null && RUNTIME.parsedVersion >= 18) { + return fetch; + } + + // In Node.js 18 or lower environments, the SDK always uses`node-fetch`. + if (RUNTIME.type === "node") { + return (await import("node-fetch")).default as any; + } + + // Otherwise the SDK uses global fetch if available, + // and falls back to node-fetch. + if (typeof fetch == "function") { + return fetch; + } + + // Defaults to node `node-fetch` if global fetch isn't available + return (await import("node-fetch")).default as any; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getHeader.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getHeader.ts new file mode 100644 index 00000000000..50f922b0e87 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getHeader.ts @@ -0,0 +1,8 @@ +export function getHeader(headers: Record, header: string): string | undefined { + for (const [headerKey, headerValue] of Object.entries(headers)) { + if (headerKey.toLowerCase() === header.toLowerCase()) { + return headerValue; + } + } + return undefined; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getRequestBody.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getRequestBody.ts new file mode 100644 index 00000000000..1138414b1c2 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getRequestBody.ts @@ -0,0 +1,14 @@ +export declare namespace GetRequestBody { + interface Args { + body: unknown; + type: "json" | "file" | "bytes" | "other"; + } +} + +export async function getRequestBody({ body, type }: GetRequestBody.Args): Promise { + if (type.includes("json")) { + return JSON.stringify(body); + } else { + return body as BodyInit; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getResponseBody.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getResponseBody.ts new file mode 100644 index 00000000000..a7a9c508777 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/getResponseBody.ts @@ -0,0 +1,32 @@ +import { chooseStreamWrapper } from "./stream-wrappers/chooseStreamWrapper"; + +export async function getResponseBody(response: Response, responseType?: string): Promise { + if (response.body != null && responseType === "blob") { + return await response.blob(); + } else if (response.body != null && responseType === "sse") { + return response.body; + } else if (response.body != null && responseType === "streaming") { + return chooseStreamWrapper(response.body); + } else if (response.body != null && responseType === "text") { + return await response.text(); + } else { + const text = await response.text(); + if (text.length > 0) { + try { + let responseBody = JSON.parse(text); + return responseBody; + } catch (err) { + return { + ok: false, + error: { + reason: "non-json", + statusCode: response.status, + rawBody: text, + }, + }; + } + } else { + return undefined; + } + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/index.ts new file mode 100644 index 00000000000..2d658ca48f9 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/index.ts @@ -0,0 +1,5 @@ +export type { APIResponse } from "./APIResponse"; +export { fetcher } from "./Fetcher"; +export type { Fetcher, FetchFunction } from "./Fetcher"; +export { getHeader } from "./getHeader"; +export { Supplier } from "./Supplier"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/makeRequest.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/makeRequest.ts new file mode 100644 index 00000000000..8fb4bace466 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/makeRequest.ts @@ -0,0 +1,44 @@ +import { anySignal, getTimeoutSignal } from "./signals"; + +export const makeRequest = async ( + fetchFn: (url: string, init: RequestInit) => Promise, + url: string, + method: string, + headers: Record, + requestBody: BodyInit | undefined, + timeoutMs?: number, + abortSignal?: AbortSignal, + withCredentials?: boolean, + duplex?: "half" +): Promise => { + const signals: AbortSignal[] = []; + + // Add timeout signal + let timeoutAbortId: NodeJS.Timeout | undefined = undefined; + if (timeoutMs != null) { + const { signal, abortId } = getTimeoutSignal(timeoutMs); + timeoutAbortId = abortId; + signals.push(signal); + } + + // Add arbitrary signal + if (abortSignal != null) { + signals.push(abortSignal); + } + let newSignals = anySignal(signals); + const response = await fetchFn(url, { + method: method, + headers, + body: requestBody, + signal: newSignals, + credentials: withCredentials ? "include" : undefined, + // @ts-ignore + duplex, + }); + + if (timeoutAbortId != null) { + clearTimeout(timeoutAbortId); + } + + return response; +}; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/requestWithRetries.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/requestWithRetries.ts new file mode 100644 index 00000000000..ff5dc3bbabc --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/requestWithRetries.ts @@ -0,0 +1,21 @@ +const INITIAL_RETRY_DELAY = 1; +const MAX_RETRY_DELAY = 60; +const DEFAULT_MAX_RETRIES = 2; + +export async function requestWithRetries( + requestFn: () => Promise, + maxRetries: number = DEFAULT_MAX_RETRIES +): Promise { + let response: Response = await requestFn(); + + for (let i = 0; i < maxRetries; ++i) { + if ([408, 409, 429].includes(response.status) || response.status >= 500) { + const delay = Math.min(INITIAL_RETRY_DELAY * Math.pow(2, i), MAX_RETRY_DELAY); + await new Promise((resolve) => setTimeout(resolve, delay)); + response = await requestFn(); + } else { + break; + } + } + return response!; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/signals.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/signals.ts new file mode 100644 index 00000000000..6c124ff7985 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/signals.ts @@ -0,0 +1,38 @@ +const TIMEOUT = "timeout"; + +export function getTimeoutSignal(timeoutMs: number): { signal: AbortSignal; abortId: NodeJS.Timeout } { + const controller = new AbortController(); + const abortId = setTimeout(() => controller.abort(TIMEOUT), timeoutMs); + return { signal: controller.signal, abortId }; +} + +/** + * Returns an abort signal that is getting aborted when + * at least one of the specified abort signals is aborted. + * + * Requires at least node.js 18. + */ +export function anySignal(...args: AbortSignal[] | [AbortSignal[]]): AbortSignal { + // Allowing signals to be passed either as array + // of signals or as multiple arguments. + const signals = (args.length === 1 && Array.isArray(args[0]) ? args[0] : args); + + const controller = new AbortController(); + + for (const signal of signals) { + if (signal.aborted) { + // Exiting early if one of the signals + // is already aborted. + controller.abort((signal as any)?.reason); + break; + } + + // Listening for signals and removing the listeners + // when at least one symbol is aborted. + signal.addEventListener("abort", () => controller.abort((signal as any)?.reason), { + signal: controller.signal, + }); + } + + return controller.signal; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper.ts new file mode 100644 index 00000000000..4d7b7d52e8f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper.ts @@ -0,0 +1,256 @@ +import type { Writable } from "readable-stream"; +import { EventCallback, StreamWrapper } from "./chooseStreamWrapper"; + +export class Node18UniversalStreamWrapper + implements + StreamWrapper | Writable | WritableStream, ReadFormat> +{ + private readableStream: ReadableStream; + private reader: ReadableStreamDefaultReader; + private events: Record; + private paused: boolean; + private resumeCallback: ((value?: unknown) => void) | null; + private encoding: string | null; + + constructor(readableStream: ReadableStream) { + this.readableStream = readableStream; + this.reader = this.readableStream.getReader(); + this.events = { + data: [], + end: [], + error: [], + readable: [], + close: [], + pause: [], + resume: [], + }; + this.paused = false; + this.resumeCallback = null; + this.encoding = null; + } + + public on(event: string, callback: EventCallback): void { + this.events[event]?.push(callback); + } + + public off(event: string, callback: EventCallback): void { + this.events[event] = this.events[event]?.filter((cb) => cb !== callback); + } + + public pipe( + dest: Node18UniversalStreamWrapper | Writable | WritableStream + ): Node18UniversalStreamWrapper | Writable | WritableStream { + this.on("data", async (chunk) => { + if (dest instanceof Node18UniversalStreamWrapper) { + dest._write(chunk); + } else if (dest instanceof WritableStream) { + const writer = dest.getWriter(); + writer.write(chunk).then(() => writer.releaseLock()); + } else { + dest.write(chunk); + } + }); + + this.on("end", async () => { + if (dest instanceof Node18UniversalStreamWrapper) { + dest._end(); + } else if (dest instanceof WritableStream) { + const writer = dest.getWriter(); + writer.close(); + } else { + dest.end(); + } + }); + + this.on("error", async (error) => { + if (dest instanceof Node18UniversalStreamWrapper) { + dest._error(error); + } else if (dest instanceof WritableStream) { + const writer = dest.getWriter(); + writer.abort(error); + } else { + dest.destroy(error); + } + }); + + this._startReading(); + + return dest; + } + + public pipeTo( + dest: Node18UniversalStreamWrapper | Writable | WritableStream + ): Node18UniversalStreamWrapper | Writable | WritableStream { + return this.pipe(dest); + } + + public unpipe(dest: Node18UniversalStreamWrapper | Writable | WritableStream): void { + this.off("data", async (chunk) => { + if (dest instanceof Node18UniversalStreamWrapper) { + dest._write(chunk); + } else if (dest instanceof WritableStream) { + const writer = dest.getWriter(); + writer.write(chunk).then(() => writer.releaseLock()); + } else { + dest.write(chunk); + } + }); + + this.off("end", async () => { + if (dest instanceof Node18UniversalStreamWrapper) { + dest._end(); + } else if (dest instanceof WritableStream) { + const writer = dest.getWriter(); + writer.close(); + } else { + dest.end(); + } + }); + + this.off("error", async (error) => { + if (dest instanceof Node18UniversalStreamWrapper) { + dest._error(error); + } else if (dest instanceof WritableStream) { + const writer = dest.getWriter(); + writer.abort(error); + } else { + dest.destroy(error); + } + }); + } + + public destroy(error?: Error): void { + this.reader + .cancel(error) + .then(() => { + this._emit("close"); + }) + .catch((err) => { + this._emit("error", err); + }); + } + + public pause(): void { + this.paused = true; + this._emit("pause"); + } + + public resume(): void { + if (this.paused) { + this.paused = false; + this._emit("resume"); + if (this.resumeCallback) { + this.resumeCallback(); + this.resumeCallback = null; + } + } + } + + public get isPaused(): boolean { + return this.paused; + } + + public async read(): Promise { + if (this.paused) { + await new Promise((resolve) => { + this.resumeCallback = resolve; + }); + } + const { done, value } = await this.reader.read(); + + if (done) { + return undefined; + } + return value; + } + + public setEncoding(encoding: string): void { + this.encoding = encoding; + } + + public async text(): Promise { + const chunks: ReadFormat[] = []; + + while (true) { + const { done, value } = await this.reader.read(); + if (done) { + break; + } + if (value) { + chunks.push(value); + } + } + + const decoder = new TextDecoder(this.encoding || "utf-8"); + return decoder.decode(await new Blob(chunks).arrayBuffer()); + } + + public async json(): Promise { + const text = await this.text(); + return JSON.parse(text); + } + + private _write(chunk: ReadFormat): void { + this._emit("data", chunk); + } + + private _end(): void { + this._emit("end"); + } + + private _error(error: any): void { + this._emit("error", error); + } + + private _emit(event: string, data?: any): void { + if (this.events[event]) { + for (const callback of this.events[event] || []) { + callback(data); + } + } + } + + private async _startReading(): Promise { + try { + this._emit("readable"); + while (true) { + if (this.paused) { + await new Promise((resolve) => { + this.resumeCallback = resolve; + }); + } + const { done, value } = await this.reader.read(); + if (done) { + this._emit("end"); + this._emit("close"); + break; + } + if (value) { + this._emit("data", value); + } + } + } catch (error) { + this._emit("error", error); + } + } + + [Symbol.asyncIterator](): AsyncIterableIterator { + return { + next: async () => { + if (this.paused) { + await new Promise((resolve) => { + this.resumeCallback = resolve; + }); + } + const { done, value } = await this.reader.read(); + if (done) { + return { done: true, value: undefined }; + } + return { done: false, value }; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/NodePre18StreamWrapper.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/NodePre18StreamWrapper.ts new file mode 100644 index 00000000000..ba5f7276750 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/NodePre18StreamWrapper.ts @@ -0,0 +1,106 @@ +import type { Readable, Writable } from "readable-stream"; +import { EventCallback, StreamWrapper } from "./chooseStreamWrapper"; + +export class NodePre18StreamWrapper implements StreamWrapper { + private readableStream: Readable; + private encoding: string | undefined; + + constructor(readableStream: Readable) { + this.readableStream = readableStream; + } + + public on(event: string, callback: EventCallback): void { + this.readableStream.on(event, callback); + } + + public off(event: string, callback: EventCallback): void { + this.readableStream.off(event, callback); + } + + public pipe(dest: Writable): Writable { + this.readableStream.pipe(dest); + return dest; + } + + public pipeTo(dest: Writable): Writable { + return this.pipe(dest); + } + + public unpipe(dest?: Writable): void { + if (dest) { + this.readableStream.unpipe(dest); + } else { + this.readableStream.unpipe(); + } + } + + public destroy(error?: Error): void { + this.readableStream.destroy(error); + } + + public pause(): void { + this.readableStream.pause(); + } + + public resume(): void { + this.readableStream.resume(); + } + + public get isPaused(): boolean { + return this.readableStream.isPaused(); + } + + public async read(): Promise { + return new Promise((resolve, reject) => { + const chunk = this.readableStream.read(); + if (chunk) { + resolve(chunk); + } else { + this.readableStream.once("readable", () => { + const chunk = this.readableStream.read(); + resolve(chunk); + }); + this.readableStream.once("error", reject); + } + }); + } + + public setEncoding(encoding?: string): void { + this.readableStream.setEncoding(encoding as BufferEncoding); + this.encoding = encoding; + } + + public async text(): Promise { + const chunks: Uint8Array[] = []; + const encoder = new TextEncoder(); + this.readableStream.setEncoding((this.encoding || "utf-8") as BufferEncoding); + + for await (const chunk of this.readableStream) { + chunks.push(encoder.encode(chunk)); + } + + const decoder = new TextDecoder(this.encoding || "utf-8"); + return decoder.decode(Buffer.concat(chunks)); + } + + public async json(): Promise { + const text = await this.text(); + return JSON.parse(text); + } + + public [Symbol.asyncIterator](): AsyncIterableIterator { + const readableStream = this.readableStream; + const iterator = readableStream[Symbol.asyncIterator](); + + // Create and return an async iterator that yields buffers + return { + async next(): Promise> { + const { value, done } = await iterator.next(); + return { value: value as Buffer, done }; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/UndiciStreamWrapper.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/UndiciStreamWrapper.ts new file mode 100644 index 00000000000..263af00911f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/UndiciStreamWrapper.ts @@ -0,0 +1,243 @@ +import { StreamWrapper } from "./chooseStreamWrapper"; + +type EventCallback = (data?: any) => void; + +export class UndiciStreamWrapper + implements StreamWrapper | WritableStream, ReadFormat> +{ + private readableStream: ReadableStream; + private reader: ReadableStreamDefaultReader; + private events: Record; + private paused: boolean; + private resumeCallback: ((value?: unknown) => void) | null; + private encoding: string | null; + + constructor(readableStream: ReadableStream) { + this.readableStream = readableStream; + this.reader = this.readableStream.getReader(); + this.events = { + data: [], + end: [], + error: [], + readable: [], + close: [], + pause: [], + resume: [], + }; + this.paused = false; + this.resumeCallback = null; + this.encoding = null; + } + + public on(event: string, callback: EventCallback): void { + this.events[event]?.push(callback); + } + + public off(event: string, callback: EventCallback): void { + this.events[event] = this.events[event]?.filter((cb) => cb !== callback); + } + + public pipe( + dest: UndiciStreamWrapper | WritableStream + ): UndiciStreamWrapper | WritableStream { + this.on("data", (chunk) => { + if (dest instanceof UndiciStreamWrapper) { + dest._write(chunk); + } else { + const writer = dest.getWriter(); + writer.write(chunk).then(() => writer.releaseLock()); + } + }); + + this.on("end", () => { + if (dest instanceof UndiciStreamWrapper) { + dest._end(); + } else { + const writer = dest.getWriter(); + writer.close(); + } + }); + + this.on("error", (error) => { + if (dest instanceof UndiciStreamWrapper) { + dest._error(error); + } else { + const writer = dest.getWriter(); + writer.abort(error); + } + }); + + this._startReading(); + + return dest; + } + + public pipeTo( + dest: UndiciStreamWrapper | WritableStream + ): UndiciStreamWrapper | WritableStream { + return this.pipe(dest); + } + + public unpipe(dest: UndiciStreamWrapper | WritableStream): void { + this.off("data", (chunk) => { + if (dest instanceof UndiciStreamWrapper) { + dest._write(chunk); + } else { + const writer = dest.getWriter(); + writer.write(chunk).then(() => writer.releaseLock()); + } + }); + + this.off("end", () => { + if (dest instanceof UndiciStreamWrapper) { + dest._end(); + } else { + const writer = dest.getWriter(); + writer.close(); + } + }); + + this.off("error", (error) => { + if (dest instanceof UndiciStreamWrapper) { + dest._error(error); + } else { + const writer = dest.getWriter(); + writer.abort(error); + } + }); + } + + public destroy(error?: Error): void { + this.reader + .cancel(error) + .then(() => { + this._emit("close"); + }) + .catch((err) => { + this._emit("error", err); + }); + } + + public pause(): void { + this.paused = true; + this._emit("pause"); + } + + public resume(): void { + if (this.paused) { + this.paused = false; + this._emit("resume"); + if (this.resumeCallback) { + this.resumeCallback(); + this.resumeCallback = null; + } + } + } + + public get isPaused(): boolean { + return this.paused; + } + + public async read(): Promise { + if (this.paused) { + await new Promise((resolve) => { + this.resumeCallback = resolve; + }); + } + const { done, value } = await this.reader.read(); + if (done) { + return undefined; + } + return value; + } + + public setEncoding(encoding: string): void { + this.encoding = encoding; + } + + public async text(): Promise { + const chunks: BlobPart[] = []; + + while (true) { + const { done, value } = await this.reader.read(); + if (done) { + break; + } + if (value) { + chunks.push(value); + } + } + + const decoder = new TextDecoder(this.encoding || "utf-8"); + return decoder.decode(await new Blob(chunks).arrayBuffer()); + } + + public async json(): Promise { + const text = await this.text(); + return JSON.parse(text); + } + + private _write(chunk: ReadFormat): void { + this._emit("data", chunk); + } + + private _end(): void { + this._emit("end"); + } + + private _error(error: any): void { + this._emit("error", error); + } + + private _emit(event: string, data?: any): void { + if (this.events[event]) { + for (const callback of this.events[event] || []) { + callback(data); + } + } + } + + private async _startReading(): Promise { + try { + this._emit("readable"); + while (true) { + if (this.paused) { + await new Promise((resolve) => { + this.resumeCallback = resolve; + }); + } + const { done, value } = await this.reader.read(); + if (done) { + this._emit("end"); + this._emit("close"); + break; + } + if (value) { + this._emit("data", value); + } + } + } catch (error) { + this._emit("error", error); + } + } + + [Symbol.asyncIterator](): AsyncIterableIterator { + return { + next: async () => { + if (this.paused) { + await new Promise((resolve) => { + this.resumeCallback = resolve; + }); + } + const { done, value } = await this.reader.read(); + if (done) { + return { done: true, value: undefined }; + } + return { done: false, value }; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/chooseStreamWrapper.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/chooseStreamWrapper.ts new file mode 100644 index 00000000000..2abd6b2ba1c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/stream-wrappers/chooseStreamWrapper.ts @@ -0,0 +1,33 @@ +import type { Readable } from "readable-stream"; +import { RUNTIME } from "../../runtime"; + +export type EventCallback = (data?: any) => void; + +export interface StreamWrapper { + setEncoding(encoding?: string): void; + on(event: string, callback: EventCallback): void; + off(event: string, callback: EventCallback): void; + pipe(dest: WritableStream): WritableStream; + pipeTo(dest: WritableStream): WritableStream; + unpipe(dest?: WritableStream): void; + destroy(error?: Error): void; + pause(): void; + resume(): void; + get isPaused(): boolean; + read(): Promise; + text(): Promise; + json(): Promise; + [Symbol.asyncIterator](): AsyncIterableIterator; +} + +export async function chooseStreamWrapper(responseBody: any): Promise>> { + if (RUNTIME.type === "node" && RUNTIME.parsedVersion != null && RUNTIME.parsedVersion >= 18) { + return new (await import("./Node18UniversalStreamWrapper")).Node18UniversalStreamWrapper( + responseBody as ReadableStream + ); + } else if (RUNTIME.type !== "node" && typeof fetch === "function") { + return new (await import("./UndiciStreamWrapper")).UndiciStreamWrapper(responseBody as ReadableStream); + } else { + return new (await import("./NodePre18StreamWrapper")).NodePre18StreamWrapper(responseBody as Readable); + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/index.ts new file mode 100644 index 00000000000..e3006860f4d --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/index.ts @@ -0,0 +1,3 @@ +export * from "./fetcher"; +export * from "./runtime"; +export * as serialization from "./schemas"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/runtime/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/runtime/index.ts new file mode 100644 index 00000000000..5c76dbb133f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/runtime/index.ts @@ -0,0 +1 @@ +export { RUNTIME } from "./runtime"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/runtime/runtime.ts b/seed/ts-sdk/mixed-file-directory/src/core/runtime/runtime.ts new file mode 100644 index 00000000000..4d0687e8eb4 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/runtime/runtime.ts @@ -0,0 +1,126 @@ +interface DenoGlobal { + version: { + deno: string; + }; +} + +interface BunGlobal { + version: string; +} + +declare const Deno: DenoGlobal; +declare const Bun: BunGlobal; + +/** + * A constant that indicates whether the environment the code is running is a Web Browser. + */ +const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; + +/** + * A constant that indicates whether the environment the code is running is a Web Worker. + */ +const isWebWorker = + typeof self === "object" && + // @ts-ignore + typeof self?.importScripts === "function" && + (self.constructor?.name === "DedicatedWorkerGlobalScope" || + self.constructor?.name === "ServiceWorkerGlobalScope" || + self.constructor?.name === "SharedWorkerGlobalScope"); + +/** + * A constant that indicates whether the environment the code is running is Deno. + */ +const isDeno = + typeof Deno !== "undefined" && typeof Deno.version !== "undefined" && typeof Deno.version.deno !== "undefined"; + +/** + * A constant that indicates whether the environment the code is running is Bun.sh. + */ +const isBun = typeof Bun !== "undefined" && typeof Bun.version !== "undefined"; + +/** + * A constant that indicates whether the environment the code is running is Node.JS. + */ +const isNode = + typeof process !== "undefined" && + Boolean(process.version) && + Boolean(process.versions?.node) && + // Deno spoofs process.versions.node, see https://deno.land/std@0.177.0/node/process.ts?s=versions + !isDeno && + !isBun; + +/** + * A constant that indicates whether the environment the code is running is in React-Native. + * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/setUpNavigator.js + */ +const isReactNative = typeof navigator !== "undefined" && navigator?.product === "ReactNative"; + +/** + * A constant that indicates whether the environment the code is running is Cloudflare. + * https://developers.cloudflare.com/workers/runtime-apis/web-standards/#navigatoruseragent + */ +const isCloudflare = typeof globalThis !== "undefined" && globalThis?.navigator?.userAgent === "Cloudflare-Workers"; + +/** + * A constant that indicates which environment and version the SDK is running in. + */ +export const RUNTIME: Runtime = evaluateRuntime(); + +export interface Runtime { + type: "browser" | "web-worker" | "deno" | "bun" | "node" | "react-native" | "unknown" | "workerd"; + version?: string; + parsedVersion?: number; +} + +function evaluateRuntime(): Runtime { + if (isBrowser) { + return { + type: "browser", + version: window.navigator.userAgent, + }; + } + + if (isCloudflare) { + return { + type: "workerd", + }; + } + + if (isWebWorker) { + return { + type: "web-worker", + }; + } + + if (isDeno) { + return { + type: "deno", + version: Deno.version.deno, + }; + } + + if (isBun) { + return { + type: "bun", + version: Bun.version, + }; + } + + if (isNode) { + return { + type: "node", + version: process.versions.node, + parsedVersion: Number(process.versions.node.split(".")[0]), + }; + } + + if (isReactNative) { + return { + type: "react-native", + }; + } + + return { + type: "unknown", + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/Schema.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/Schema.ts new file mode 100644 index 00000000000..19acc5dc44b --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/Schema.ts @@ -0,0 +1,98 @@ +import { SchemaUtils } from "./builders"; + +export type Schema = BaseSchema & SchemaUtils; + +export type inferRaw = S extends Schema ? Raw : never; +export type inferParsed = S extends Schema ? Parsed : never; + +export interface BaseSchema { + parse: (raw: unknown, opts?: SchemaOptions) => MaybeValid; + json: (parsed: unknown, opts?: SchemaOptions) => MaybeValid; + getType: () => SchemaType | SchemaType; +} + +export const SchemaType = { + DATE: "date", + ENUM: "enum", + LIST: "list", + STRING_LITERAL: "stringLiteral", + BOOLEAN_LITERAL: "booleanLiteral", + OBJECT: "object", + ANY: "any", + BOOLEAN: "boolean", + NUMBER: "number", + STRING: "string", + UNKNOWN: "unknown", + RECORD: "record", + SET: "set", + UNION: "union", + UNDISCRIMINATED_UNION: "undiscriminatedUnion", + OPTIONAL: "optional", +} as const; +export type SchemaType = typeof SchemaType[keyof typeof SchemaType]; + +export type MaybeValid = Valid | Invalid; + +export interface Valid { + ok: true; + value: T; +} + +export interface Invalid { + ok: false; + errors: ValidationError[]; +} + +export interface ValidationError { + path: string[]; + message: string; +} + +export interface SchemaOptions { + /** + * how to handle unrecognized keys in objects + * + * @default "fail" + */ + unrecognizedObjectKeys?: "fail" | "passthrough" | "strip"; + + /** + * whether to fail when an unrecognized discriminant value is + * encountered in a union + * + * @default false + */ + allowUnrecognizedUnionMembers?: boolean; + + /** + * whether to fail when an unrecognized enum value is encountered + * + * @default false + */ + allowUnrecognizedEnumValues?: boolean; + + /** + * whether to allow data that doesn't conform to the schema. + * invalid data is passed through without transformation. + * + * when this is enabled, .parse() and .json() will always + * return `ok: true`. `.parseOrThrow()` and `.jsonOrThrow()` + * will never fail. + * + * @default false + */ + skipValidation?: boolean; + + /** + * each validation failure contains a "path" property, which is + * the breadcrumbs to the offending node in the JSON. you can supply + * a prefix that is prepended to all the errors' paths. this can be + * helpful for zurg's internal debug logging. + */ + breadcrumbsPrefix?: string[]; + + /** + * whether to send 'null' for optional properties explicitly set to 'undefined'. + */ + omitUndefined?: boolean; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/date/date.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/date/date.ts new file mode 100644 index 00000000000..b70f24b045a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/date/date.ts @@ -0,0 +1,65 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +// https://stackoverflow.com/questions/12756159/regex-and-iso8601-formatted-datetime +const ISO_8601_REGEX = + /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; + +export function date(): Schema { + const baseSchema: BaseSchema = { + parse: (raw, { breadcrumbsPrefix = [] } = {}) => { + if (typeof raw !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "string"), + }, + ], + }; + } + if (!ISO_8601_REGEX.test(raw)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "ISO 8601 date string"), + }, + ], + }; + } + return { + ok: true, + value: new Date(raw), + }; + }, + json: (date, { breadcrumbsPrefix = [] } = {}) => { + if (date instanceof Date) { + return { + ok: true, + value: date.toISOString(), + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(date, "Date object"), + }, + ], + }; + } + }, + getType: () => SchemaType.DATE, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/date/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/date/index.ts new file mode 100644 index 00000000000..187b29040f6 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/date/index.ts @@ -0,0 +1 @@ +export { date } from "./date"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/enum/enum.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/enum/enum.ts new file mode 100644 index 00000000000..c1e24d69dec --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/enum/enum.ts @@ -0,0 +1,43 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function enum_(values: E): Schema { + const validValues = new Set(values); + + const schemaCreator = createIdentitySchemaCreator( + SchemaType.ENUM, + (value, { allowUnrecognizedEnumValues, breadcrumbsPrefix = [] } = {}) => { + if (typeof value !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "string"), + }, + ], + }; + } + + if (!validValues.has(value) && !allowUnrecognizedEnumValues) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "enum"), + }, + ], + }; + } + + return { + ok: true, + value: value as U, + }; + } + ); + + return schemaCreator(); +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/enum/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/enum/index.ts new file mode 100644 index 00000000000..fe6faed93e3 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/enum/index.ts @@ -0,0 +1 @@ +export { enum_ } from "./enum"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/index.ts new file mode 100644 index 00000000000..050cd2c4efb --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/index.ts @@ -0,0 +1,13 @@ +export * from "./date"; +export * from "./enum"; +export * from "./lazy"; +export * from "./list"; +export * from "./literals"; +export * from "./object"; +export * from "./object-like"; +export * from "./primitives"; +export * from "./record"; +export * from "./schema-utils"; +export * from "./set"; +export * from "./undiscriminated-union"; +export * from "./union"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/index.ts new file mode 100644 index 00000000000..77420fb031c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/index.ts @@ -0,0 +1,3 @@ +export { lazy } from "./lazy"; +export type { SchemaGetter } from "./lazy"; +export { lazyObject } from "./lazyObject"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/lazy.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/lazy.ts new file mode 100644 index 00000000000..835c61f8a56 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/lazy.ts @@ -0,0 +1,32 @@ +import { BaseSchema, Schema } from "../../Schema"; +import { getSchemaUtils } from "../schema-utils"; + +export type SchemaGetter> = () => SchemaType; + +export function lazy(getter: SchemaGetter>): Schema { + const baseSchema = constructLazyBaseSchema(getter); + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} + +export function constructLazyBaseSchema( + getter: SchemaGetter> +): BaseSchema { + return { + parse: (raw, opts) => getMemoizedSchema(getter).parse(raw, opts), + json: (parsed, opts) => getMemoizedSchema(getter).json(parsed, opts), + getType: () => getMemoizedSchema(getter).getType(), + }; +} + +type MemoizedGetter> = SchemaGetter & { __zurg_memoized?: SchemaType }; + +export function getMemoizedSchema>(getter: SchemaGetter): SchemaType { + const castedGetter = getter as MemoizedGetter; + if (castedGetter.__zurg_memoized == null) { + castedGetter.__zurg_memoized = getter(); + } + return castedGetter.__zurg_memoized; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/lazyObject.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/lazyObject.ts new file mode 100644 index 00000000000..38c9e28404b --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/lazy/lazyObject.ts @@ -0,0 +1,20 @@ +import { getObjectUtils } from "../object"; +import { getObjectLikeUtils } from "../object-like"; +import { BaseObjectSchema, ObjectSchema } from "../object/types"; +import { getSchemaUtils } from "../schema-utils"; +import { constructLazyBaseSchema, getMemoizedSchema, SchemaGetter } from "./lazy"; + +export function lazyObject(getter: SchemaGetter>): ObjectSchema { + const baseSchema: BaseObjectSchema = { + ...constructLazyBaseSchema(getter), + _getRawProperties: () => getMemoizedSchema(getter)._getRawProperties(), + _getParsedProperties: () => getMemoizedSchema(getter)._getParsedProperties(), + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/list/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/list/index.ts new file mode 100644 index 00000000000..25f4bcc1737 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/list/index.ts @@ -0,0 +1 @@ +export { list } from "./list"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/list/list.ts new file mode 100644 index 00000000000..e4c5c4a4a99 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/list/list.ts @@ -0,0 +1,73 @@ +import { BaseSchema, MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +export function list(schema: Schema): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => + validateAndTransformArray(raw, (item, index) => + schema.parse(item, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `[${index}]`], + }) + ), + json: (parsed, opts) => + validateAndTransformArray(parsed, (item, index) => + schema.json(item, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `[${index}]`], + }) + ), + getType: () => SchemaType.LIST, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformArray( + value: unknown, + transformItem: (item: Raw, index: number) => MaybeValid +): MaybeValid { + if (!Array.isArray(value)) { + return { + ok: false, + errors: [ + { + message: getErrorMessageForIncorrectType(value, "list"), + path: [], + }, + ], + }; + } + + const maybeValidItems = value.map((item, index) => transformItem(item, index)); + + return maybeValidItems.reduce>( + (acc, item) => { + if (acc.ok && item.ok) { + return { + ok: true, + value: [...acc.value, item.value], + }; + } + + const errors: ValidationError[] = []; + if (!acc.ok) { + errors.push(...acc.errors); + } + if (!item.ok) { + errors.push(...item.errors); + } + + return { + ok: false, + errors, + }; + }, + { ok: true, value: [] } + ); +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/booleanLiteral.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/booleanLiteral.ts new file mode 100644 index 00000000000..a83d22cd48a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/booleanLiteral.ts @@ -0,0 +1,29 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function booleanLiteral(literal: V): Schema { + const schemaCreator = createIdentitySchemaCreator( + SchemaType.BOOLEAN_LITERAL, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (value === literal) { + return { + ok: true, + value: literal, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, `${literal.toString()}`), + }, + ], + }; + } + } + ); + + return schemaCreator(); +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/index.ts new file mode 100644 index 00000000000..d2bf08fc6ca --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/index.ts @@ -0,0 +1,2 @@ +export { stringLiteral } from "./stringLiteral"; +export { booleanLiteral } from "./booleanLiteral"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/stringLiteral.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/stringLiteral.ts new file mode 100644 index 00000000000..3939b76b48d --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/literals/stringLiteral.ts @@ -0,0 +1,29 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function stringLiteral(literal: V): Schema { + const schemaCreator = createIdentitySchemaCreator( + SchemaType.STRING_LITERAL, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (value === literal) { + return { + ok: true, + value: literal, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, `"${literal}"`), + }, + ], + }; + } + } + ); + + return schemaCreator(); +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/getObjectLikeUtils.ts new file mode 100644 index 00000000000..8331d08da89 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -0,0 +1,79 @@ +import { BaseSchema } from "../../Schema"; +import { filterObject } from "../../utils/filterObject"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { getSchemaUtils } from "../schema-utils"; +import { ObjectLikeSchema, ObjectLikeUtils } from "./types"; + +export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { + return { + withParsedProperties: (properties) => withParsedProperties(schema, properties), + }; +} + +/** + * object-like utils are defined in one file to resolve issues with circular imports + */ + +export function withParsedProperties( + objectLike: BaseSchema, + properties: { [K in keyof Properties]: Properties[K] | ((parsed: ParsedObjectShape) => Properties[K]) } +): ObjectLikeSchema { + const objectSchema: BaseSchema = { + parse: (raw, opts) => { + const parsedObject = objectLike.parse(raw, opts); + if (!parsedObject.ok) { + return parsedObject; + } + + const additionalProperties = Object.entries(properties).reduce>( + (processed, [key, value]) => { + return { + ...processed, + [key]: typeof value === "function" ? value(parsedObject.value) : value, + }; + }, + {} + ); + + return { + ok: true, + value: { + ...parsedObject.value, + ...(additionalProperties as Properties), + }, + }; + }, + + json: (parsed, opts) => { + if (!isPlainObject(parsed)) { + return { + ok: false, + errors: [ + { + path: opts?.breadcrumbsPrefix ?? [], + message: getErrorMessageForIncorrectType(parsed, "object"), + }, + ], + }; + } + + // strip out added properties + const addedPropertyKeys = new Set(Object.keys(properties)); + const parsedWithoutAddedProperties = filterObject( + parsed, + Object.keys(parsed).filter((key) => !addedPropertyKeys.has(key)) + ); + + return objectLike.json(parsedWithoutAddedProperties as ParsedObjectShape, opts); + }, + + getType: () => objectLike.getType(), + }; + + return { + ...objectSchema, + ...getSchemaUtils(objectSchema), + ...getObjectLikeUtils(objectSchema), + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/index.ts new file mode 100644 index 00000000000..c342e72cf9d --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/index.ts @@ -0,0 +1,2 @@ +export { getObjectLikeUtils, withParsedProperties } from "./getObjectLikeUtils"; +export type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/types.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/types.ts new file mode 100644 index 00000000000..75b3698729c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object-like/types.ts @@ -0,0 +1,11 @@ +import { BaseSchema, Schema } from "../../Schema"; + +export type ObjectLikeSchema = Schema & + BaseSchema & + ObjectLikeUtils; + +export interface ObjectLikeUtils { + withParsedProperties: >(properties: { + [K in keyof T]: T[K] | ((parsed: Parsed) => T[K]); + }) => ObjectLikeSchema; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/index.ts new file mode 100644 index 00000000000..e3f4388db28 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/index.ts @@ -0,0 +1,22 @@ +export { getObjectUtils, object } from "./object"; +export { objectWithoutOptionalProperties } from "./objectWithoutOptionalProperties"; +export type { + inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas, + inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas, +} from "./objectWithoutOptionalProperties"; +export { isProperty, property } from "./property"; +export type { Property } from "./property"; +export type { + BaseObjectSchema, + inferObjectSchemaFromPropertySchemas, + inferParsedObject, + inferParsedObjectFromPropertySchemas, + inferParsedPropertySchema, + inferRawKey, + inferRawObject, + inferRawObjectFromPropertySchemas, + inferRawPropertySchema, + ObjectSchema, + ObjectUtils, + PropertySchemas, +} from "./types"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/object.ts new file mode 100644 index 00000000000..e00136d72fc --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/object.ts @@ -0,0 +1,324 @@ +import { MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { entries } from "../../utils/entries"; +import { filterObject } from "../../utils/filterObject"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { keys } from "../../utils/keys"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { partition } from "../../utils/partition"; +import { getObjectLikeUtils } from "../object-like"; +import { getSchemaUtils } from "../schema-utils"; +import { isProperty } from "./property"; +import { + BaseObjectSchema, + inferObjectSchemaFromPropertySchemas, + inferParsedObjectFromPropertySchemas, + inferRawObjectFromPropertySchemas, + ObjectSchema, + ObjectUtils, + PropertySchemas, +} from "./types"; + +interface ObjectPropertyWithRawKey { + rawKey: string; + parsedKey: string; + valueSchema: Schema; +} + +export function object>( + schemas: T +): inferObjectSchemaFromPropertySchemas { + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const rawKeyToProperty: Record = {}; + const requiredKeys: string[] = []; + + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; + + const property: ObjectPropertyWithRawKey = { + rawKey, + parsedKey: parsedKey as string, + valueSchema, + }; + + rawKeyToProperty[rawKey] = property; + + if (isSchemaRequired(valueSchema)) { + requiredKeys.push(rawKey); + } + } + + return validateAndTransformObject({ + value: raw, + requiredKeys, + getProperty: (rawKey) => { + const property = rawKeyToProperty[rawKey]; + if (property == null) { + return undefined; + } + return { + transformedKey: property.parsedKey, + transform: (propertyValue) => + property.valueSchema.parse(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], + }), + }; + }, + unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, + skipValidation: opts?.skipValidation, + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + omitUndefined: opts?.omitUndefined, + }); + }, + + json: (parsed, opts) => { + const requiredKeys: string[] = []; + + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; + + if (isSchemaRequired(valueSchema)) { + requiredKeys.push(parsedKey as string); + } + } + + return validateAndTransformObject({ + value: parsed, + requiredKeys, + getProperty: ( + parsedKey + ): { transformedKey: string; transform: (propertyValue: unknown) => MaybeValid } | undefined => { + const property = schemas[parsedKey as keyof T]; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (property == null) { + return undefined; + } + + if (isProperty(property)) { + return { + transformedKey: property.rawKey, + transform: (propertyValue) => + property.valueSchema.json(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], + }), + }; + } else { + return { + transformedKey: parsedKey, + transform: (propertyValue) => + property.json(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], + }), + }; + } + }, + unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, + skipValidation: opts?.skipValidation, + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + omitUndefined: opts?.omitUndefined, + }); + }, + + getType: () => SchemaType.OBJECT, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; +} + +function validateAndTransformObject({ + value, + requiredKeys, + getProperty, + unrecognizedObjectKeys = "fail", + skipValidation = false, + breadcrumbsPrefix = [], +}: { + value: unknown; + requiredKeys: string[]; + getProperty: ( + preTransformedKey: string + ) => { transformedKey: string; transform: (propertyValue: unknown) => MaybeValid } | undefined; + unrecognizedObjectKeys: "fail" | "passthrough" | "strip" | undefined; + skipValidation: boolean | undefined; + breadcrumbsPrefix: string[] | undefined; + omitUndefined: boolean | undefined; +}): MaybeValid { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + const missingRequiredKeys = new Set(requiredKeys); + const errors: ValidationError[] = []; + const transformed: Record = {}; + + for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + const property = getProperty(preTransformedKey); + + if (property != null) { + missingRequiredKeys.delete(preTransformedKey); + + const value = property.transform(preTransformedItemValue); + if (value.ok) { + transformed[property.transformedKey] = value.value; + } else { + transformed[preTransformedKey] = preTransformedItemValue; + errors.push(...value.errors); + } + } else { + switch (unrecognizedObjectKeys) { + case "fail": + errors.push({ + path: [...breadcrumbsPrefix, preTransformedKey], + message: `Unexpected key "${preTransformedKey}"`, + }); + break; + case "strip": + break; + case "passthrough": + transformed[preTransformedKey] = preTransformedItemValue; + break; + } + } + } + + errors.push( + ...requiredKeys + .filter((key) => missingRequiredKeys.has(key)) + .map((key) => ({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + })) + ); + + if (errors.length === 0 || skipValidation) { + return { + ok: true, + value: transformed as Transformed, + }; + } else { + return { + ok: false, + errors, + }; + } +} + +export function getObjectUtils(schema: BaseObjectSchema): ObjectUtils { + return { + extend: (extension: ObjectSchema) => { + const baseSchema: BaseObjectSchema = { + _getParsedProperties: () => [...schema._getParsedProperties(), ...extension._getParsedProperties()], + _getRawProperties: () => [...schema._getRawProperties(), ...extension._getRawProperties()], + parse: (raw, opts) => { + return validateAndTransformExtendedObject({ + extensionKeys: extension._getRawProperties(), + value: raw, + transformBase: (rawBase) => schema.parse(rawBase, opts), + transformExtension: (rawExtension) => extension.parse(rawExtension, opts), + }); + }, + json: (parsed, opts) => { + return validateAndTransformExtendedObject({ + extensionKeys: extension._getParsedProperties(), + value: parsed, + transformBase: (parsedBase) => schema.json(parsedBase, opts), + transformExtension: (parsedExtension) => extension.json(parsedExtension, opts), + }); + }, + getType: () => SchemaType.OBJECT, + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; + }, + }; +} + +function validateAndTransformExtendedObject({ + extensionKeys, + value, + transformBase, + transformExtension, +}: { + extensionKeys: (keyof PreTransformedExtension)[]; + value: unknown; + transformBase: (value: unknown) => MaybeValid; + transformExtension: (value: unknown) => MaybeValid; +}): MaybeValid { + const extensionPropertiesSet = new Set(extensionKeys); + const [extensionProperties, baseProperties] = partition(keys(value), (key) => + extensionPropertiesSet.has(key as keyof PreTransformedExtension) + ); + + const transformedBase = transformBase(filterObject(value, baseProperties)); + const transformedExtension = transformExtension(filterObject(value, extensionProperties)); + + if (transformedBase.ok && transformedExtension.ok) { + return { + ok: true, + value: { + ...transformedBase.value, + ...transformedExtension.value, + }, + }; + } else { + return { + ok: false, + errors: [ + ...(transformedBase.ok ? [] : transformedBase.errors), + ...(transformedExtension.ok ? [] : transformedExtension.errors), + ], + }; + } +} + +function isSchemaRequired(schema: Schema): boolean { + return !isSchemaOptional(schema); +} + +function isSchemaOptional(schema: Schema): boolean { + switch (schema.getType()) { + case SchemaType.ANY: + case SchemaType.UNKNOWN: + case SchemaType.OPTIONAL: + return true; + default: + return false; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/objectWithoutOptionalProperties.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/objectWithoutOptionalProperties.ts new file mode 100644 index 00000000000..a0951f48efc --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/objectWithoutOptionalProperties.ts @@ -0,0 +1,18 @@ +import { object } from "./object"; +import { inferParsedPropertySchema, inferRawObjectFromPropertySchemas, ObjectSchema, PropertySchemas } from "./types"; + +export function objectWithoutOptionalProperties>( + schemas: T +): inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas { + return object(schemas) as unknown as inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas; +} + +export type inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas> = + ObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas + >; + +export type inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas> = { + [K in keyof T]: inferParsedPropertySchema; +}; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/property.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/property.ts new file mode 100644 index 00000000000..d245c4b193a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/property.ts @@ -0,0 +1,23 @@ +import { Schema } from "../../Schema"; + +export function property( + rawKey: RawKey, + valueSchema: Schema +): Property { + return { + rawKey, + valueSchema, + isProperty: true, + }; +} + +export interface Property { + rawKey: RawKey; + valueSchema: Schema; + isProperty: true; +} + +export function isProperty>(maybeProperty: unknown): maybeProperty is O { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return (maybeProperty as O).isProperty; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/types.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/types.ts new file mode 100644 index 00000000000..de9bb4074e5 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/object/types.ts @@ -0,0 +1,72 @@ +import { BaseSchema, inferParsed, inferRaw, Schema } from "../../Schema"; +import { addQuestionMarksToNullableProperties } from "../../utils/addQuestionMarksToNullableProperties"; +import { ObjectLikeUtils } from "../object-like"; +import { SchemaUtils } from "../schema-utils"; +import { Property } from "./property"; + +export type ObjectSchema = BaseObjectSchema & + ObjectLikeUtils & + ObjectUtils & + SchemaUtils; + +export interface BaseObjectSchema extends BaseSchema { + _getRawProperties: () => (keyof Raw)[]; + _getParsedProperties: () => (keyof Parsed)[]; +} + +export interface ObjectUtils { + extend: ( + schemas: ObjectSchema + ) => ObjectSchema; +} + +export type inferRawObject> = O extends ObjectSchema ? Raw : never; + +export type inferParsedObject> = O extends ObjectSchema + ? Parsed + : never; + +export type inferObjectSchemaFromPropertySchemas> = ObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas +>; + +export type inferRawObjectFromPropertySchemas> = + addQuestionMarksToNullableProperties<{ + [ParsedKey in keyof T as inferRawKey]: inferRawPropertySchema; + }>; + +export type inferParsedObjectFromPropertySchemas> = + addQuestionMarksToNullableProperties<{ + [K in keyof T]: inferParsedPropertySchema; + }>; + +export type PropertySchemas = Record< + ParsedKeys, + Property | Schema +>; + +export type inferRawPropertySchema

| Schema> = P extends Property< + any, + infer Raw, + any +> + ? Raw + : P extends Schema + ? inferRaw

+ : never; + +export type inferParsedPropertySchema

| Schema> = P extends Property< + any, + any, + infer Parsed +> + ? Parsed + : P extends Schema + ? inferParsed

+ : never; + +export type inferRawKey< + ParsedKey extends string | number | symbol, + P extends Property | Schema +> = P extends Property ? Raw : ParsedKey; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/any.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/any.ts new file mode 100644 index 00000000000..fcaeb04255a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/any.ts @@ -0,0 +1,4 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; + +export const any = createIdentitySchemaCreator(SchemaType.ANY, (value) => ({ ok: true, value })); diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/boolean.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/boolean.ts new file mode 100644 index 00000000000..fad60562120 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/boolean.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const boolean = createIdentitySchemaCreator( + SchemaType.BOOLEAN, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "boolean") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "boolean"), + }, + ], + }; + } + } +); diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/index.ts new file mode 100644 index 00000000000..788f9416bfe --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/index.ts @@ -0,0 +1,5 @@ +export { any } from "./any"; +export { boolean } from "./boolean"; +export { number } from "./number"; +export { string } from "./string"; +export { unknown } from "./unknown"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/number.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/number.ts new file mode 100644 index 00000000000..c2689456936 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/number.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const number = createIdentitySchemaCreator( + SchemaType.NUMBER, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "number") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "number"), + }, + ], + }; + } + } +); diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/string.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/string.ts new file mode 100644 index 00000000000..949f1f2a630 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/string.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const string = createIdentitySchemaCreator( + SchemaType.STRING, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "string") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "string"), + }, + ], + }; + } + } +); diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/unknown.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/unknown.ts new file mode 100644 index 00000000000..4d5249571f5 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/primitives/unknown.ts @@ -0,0 +1,4 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; + +export const unknown = createIdentitySchemaCreator(SchemaType.UNKNOWN, (value) => ({ ok: true, value })); diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/index.ts new file mode 100644 index 00000000000..82e25c5c2af --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/index.ts @@ -0,0 +1,2 @@ +export { record } from "./record"; +export type { BaseRecordSchema, RecordSchema } from "./types"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/record.ts new file mode 100644 index 00000000000..6683ac3609f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/record.ts @@ -0,0 +1,130 @@ +import { MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { entries } from "../../utils/entries"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; +import { BaseRecordSchema, RecordSchema } from "./types"; + +export function record( + keySchema: Schema, + valueSchema: Schema +): RecordSchema { + const baseSchema: BaseRecordSchema = { + parse: (raw, opts) => { + return validateAndTransformRecord({ + value: raw, + isKeyNumeric: keySchema.getType() === SchemaType.NUMBER, + transformKey: (key) => + keySchema.parse(key, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key} (key)`], + }), + transformValue: (value, key) => + valueSchema.parse(value, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key}`], + }), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + json: (parsed, opts) => { + return validateAndTransformRecord({ + value: parsed, + isKeyNumeric: keySchema.getType() === SchemaType.NUMBER, + transformKey: (key) => + keySchema.json(key, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key} (key)`], + }), + transformValue: (value, key) => + valueSchema.json(value, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key}`], + }), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + getType: () => SchemaType.RECORD, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformRecord({ + value, + isKeyNumeric, + transformKey, + transformValue, + breadcrumbsPrefix = [], +}: { + value: unknown; + isKeyNumeric: boolean; + transformKey: (key: string | number) => MaybeValid; + transformValue: (value: unknown, key: string | number) => MaybeValid; + breadcrumbsPrefix: string[] | undefined; +}): MaybeValid> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + return entries(value).reduce>>( + (accPromise, [stringKey, value]) => { + // skip nullish keys + if (value == null) { + return accPromise; + } + + const acc = accPromise; + + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!isNaN(numberKey)) { + key = numberKey; + } + } + const transformedKey = transformKey(key); + + const transformedValue = transformValue(value, key); + + if (acc.ok && transformedKey.ok && transformedValue.ok) { + return { + ok: true, + value: { + ...acc.value, + [transformedKey.value]: transformedValue.value, + }, + }; + } + + const errors: ValidationError[] = []; + if (!acc.ok) { + errors.push(...acc.errors); + } + if (!transformedKey.ok) { + errors.push(...transformedKey.errors); + } + if (!transformedValue.ok) { + errors.push(...transformedValue.errors); + } + + return { + ok: false, + errors, + }; + }, + { ok: true, value: {} as Record } + ); +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/types.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/types.ts new file mode 100644 index 00000000000..eb82cc7f65c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/record/types.ts @@ -0,0 +1,17 @@ +import { BaseSchema } from "../../Schema"; +import { SchemaUtils } from "../schema-utils"; + +export type RecordSchema< + RawKey extends string | number, + RawValue, + ParsedKey extends string | number, + ParsedValue +> = BaseRecordSchema & + SchemaUtils, Record>; + +export type BaseRecordSchema< + RawKey extends string | number, + RawValue, + ParsedKey extends string | number, + ParsedValue +> = BaseSchema, Record>; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/JsonError.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/JsonError.ts new file mode 100644 index 00000000000..2b89ca0e7ad --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/JsonError.ts @@ -0,0 +1,9 @@ +import { ValidationError } from "../../Schema"; +import { stringifyValidationError } from "./stringifyValidationErrors"; + +export class JsonError extends Error { + constructor(public readonly errors: ValidationError[]) { + super(errors.map(stringifyValidationError).join("; ")); + Object.setPrototypeOf(this, JsonError.prototype); + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/ParseError.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/ParseError.ts new file mode 100644 index 00000000000..d056eb45cf7 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/ParseError.ts @@ -0,0 +1,9 @@ +import { ValidationError } from "../../Schema"; +import { stringifyValidationError } from "./stringifyValidationErrors"; + +export class ParseError extends Error { + constructor(public readonly errors: ValidationError[]) { + super(errors.map(stringifyValidationError).join("; ")); + Object.setPrototypeOf(this, ParseError.prototype); + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/getSchemaUtils.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/getSchemaUtils.ts new file mode 100644 index 00000000000..79ecad92132 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/getSchemaUtils.ts @@ -0,0 +1,105 @@ +import { BaseSchema, Schema, SchemaOptions, SchemaType } from "../../Schema"; +import { JsonError } from "./JsonError"; +import { ParseError } from "./ParseError"; + +export interface SchemaUtils { + optional: () => Schema; + transform: (transformer: SchemaTransformer) => Schema; + parseOrThrow: (raw: unknown, opts?: SchemaOptions) => Parsed; + jsonOrThrow: (raw: unknown, opts?: SchemaOptions) => Raw; +} + +export interface SchemaTransformer { + transform: (parsed: Parsed) => Transformed; + untransform: (transformed: any) => Parsed; +} + +export function getSchemaUtils(schema: BaseSchema): SchemaUtils { + return { + optional: () => optional(schema), + transform: (transformer) => transform(schema, transformer), + parseOrThrow: (raw, opts) => { + const parsed = schema.parse(raw, opts); + if (parsed.ok) { + return parsed.value; + } + throw new ParseError(parsed.errors); + }, + jsonOrThrow: (parsed, opts) => { + const raw = schema.json(parsed, opts); + if (raw.ok) { + return raw.value; + } + throw new JsonError(raw.errors); + }, + }; +} + +/** + * schema utils are defined in one file to resolve issues with circular imports + */ + +export function optional( + schema: BaseSchema +): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => { + if (raw == null) { + return { + ok: true, + value: undefined, + }; + } + return schema.parse(raw, opts); + }, + json: (parsed, opts) => { + if (opts?.omitUndefined && parsed === undefined) { + return { + ok: true, + value: undefined, + }; + } + if (parsed == null) { + return { + ok: true, + value: null, + }; + } + return schema.json(parsed, opts); + }, + getType: () => SchemaType.OPTIONAL, + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} + +export function transform( + schema: BaseSchema, + transformer: SchemaTransformer +): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => { + const parsed = schema.parse(raw, opts); + if (!parsed.ok) { + return parsed; + } + return { + ok: true, + value: transformer.transform(parsed.value), + }; + }, + json: (transformed, opts) => { + const parsed = transformer.untransform(transformed); + return schema.json(parsed, opts); + }, + getType: () => schema.getType(), + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/index.ts new file mode 100644 index 00000000000..aa04e051dfa --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/index.ts @@ -0,0 +1,4 @@ +export { getSchemaUtils, optional, transform } from "./getSchemaUtils"; +export type { SchemaUtils } from "./getSchemaUtils"; +export { JsonError } from "./JsonError"; +export { ParseError } from "./ParseError"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts new file mode 100644 index 00000000000..4160f0a2617 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts @@ -0,0 +1,8 @@ +import { ValidationError } from "../../Schema"; + +export function stringifyValidationError(error: ValidationError): string { + if (error.path.length === 0) { + return error.message; + } + return `${error.path.join(" -> ")}: ${error.message}`; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/set/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/set/index.ts new file mode 100644 index 00000000000..f3310e8bdad --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/set/index.ts @@ -0,0 +1 @@ +export { set } from "./set"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/set/set.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/set/set.ts new file mode 100644 index 00000000000..e9e6bb7e539 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/set/set.ts @@ -0,0 +1,43 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { list } from "../list"; +import { getSchemaUtils } from "../schema-utils"; + +export function set(schema: Schema): Schema> { + const listSchema = list(schema); + const baseSchema: BaseSchema> = { + parse: (raw, opts) => { + const parsedList = listSchema.parse(raw, opts); + if (parsedList.ok) { + return { + ok: true, + value: new Set(parsedList.value), + }; + } else { + return parsedList; + } + }, + json: (parsed, opts) => { + if (!(parsed instanceof Set)) { + return { + ok: false, + errors: [ + { + path: opts?.breadcrumbsPrefix ?? [], + message: getErrorMessageForIncorrectType(parsed, "Set"), + }, + ], + }; + } + const jsonList = listSchema.json([...parsed], opts); + return jsonList; + }, + getType: () => SchemaType.SET, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/index.ts new file mode 100644 index 00000000000..75b71cb3565 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/index.ts @@ -0,0 +1,6 @@ +export type { + inferParsedUnidiscriminatedUnionSchema, + inferRawUnidiscriminatedUnionSchema, + UndiscriminatedUnionSchema, +} from "./types"; +export { undiscriminatedUnion } from "./undiscriminatedUnion"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/types.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/types.ts new file mode 100644 index 00000000000..43e7108a060 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/types.ts @@ -0,0 +1,10 @@ +import { inferParsed, inferRaw, Schema } from "../../Schema"; + +export type UndiscriminatedUnionSchema = Schema< + inferRawUnidiscriminatedUnionSchema, + inferParsedUnidiscriminatedUnionSchema +>; + +export type inferRawUnidiscriminatedUnionSchema = inferRaw; + +export type inferParsedUnidiscriminatedUnionSchema = inferParsed; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts new file mode 100644 index 00000000000..21ed3df0f40 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts @@ -0,0 +1,60 @@ +import { BaseSchema, MaybeValid, Schema, SchemaOptions, SchemaType, ValidationError } from "../../Schema"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; +import { inferParsedUnidiscriminatedUnionSchema, inferRawUnidiscriminatedUnionSchema } from "./types"; + +export function undiscriminatedUnion, ...Schema[]]>( + schemas: Schemas +): Schema, inferParsedUnidiscriminatedUnionSchema> { + const baseSchema: BaseSchema< + inferRawUnidiscriminatedUnionSchema, + inferParsedUnidiscriminatedUnionSchema + > = { + parse: (raw, opts) => { + return validateAndTransformUndiscriminatedUnion>( + (schema, opts) => schema.parse(raw, opts), + schemas, + opts + ); + }, + json: (parsed, opts) => { + return validateAndTransformUndiscriminatedUnion>( + (schema, opts) => schema.json(parsed, opts), + schemas, + opts + ); + }, + getType: () => SchemaType.UNDISCRIMINATED_UNION, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformUndiscriminatedUnion( + transform: (schema: Schema, opts: SchemaOptions) => MaybeValid, + schemas: Schema[], + opts: SchemaOptions | undefined +): MaybeValid { + const errors: ValidationError[] = []; + for (const [index, schema] of schemas.entries()) { + const transformed = transform(schema, { ...opts, skipValidation: false }); + if (transformed.ok) { + return transformed; + } else { + for (const error of transformed.errors) { + errors.push({ + path: error.path, + message: `[Variant ${index}] ${error.message}`, + }); + } + } + } + + return { + ok: false, + errors, + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/discriminant.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/discriminant.ts new file mode 100644 index 00000000000..55065bc8946 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/discriminant.ts @@ -0,0 +1,14 @@ +export function discriminant( + parsedDiscriminant: ParsedDiscriminant, + rawDiscriminant: RawDiscriminant +): Discriminant { + return { + parsedDiscriminant, + rawDiscriminant, + }; +} + +export interface Discriminant { + parsedDiscriminant: ParsedDiscriminant; + rawDiscriminant: RawDiscriminant; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/index.ts new file mode 100644 index 00000000000..85fc008a2d8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/index.ts @@ -0,0 +1,10 @@ +export { discriminant } from "./discriminant"; +export type { Discriminant } from "./discriminant"; +export type { + inferParsedDiscriminant, + inferParsedUnion, + inferRawDiscriminant, + inferRawUnion, + UnionSubtypes, +} from "./types"; +export { union } from "./union"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/types.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/types.ts new file mode 100644 index 00000000000..6f82c868b2d --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/types.ts @@ -0,0 +1,26 @@ +import { inferParsedObject, inferRawObject, ObjectSchema } from "../object"; +import { Discriminant } from "./discriminant"; + +export type UnionSubtypes = { + [K in DiscriminantValues]: ObjectSchema; +}; + +export type inferRawUnion, U extends UnionSubtypes> = { + [K in keyof U]: Record, K> & inferRawObject; +}[keyof U]; + +export type inferParsedUnion, U extends UnionSubtypes> = { + [K in keyof U]: Record, K> & inferParsedObject; +}[keyof U]; + +export type inferRawDiscriminant> = D extends string + ? D + : D extends Discriminant + ? Raw + : never; + +export type inferParsedDiscriminant> = D extends string + ? D + : D extends Discriminant + ? Parsed + : never; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/union.ts new file mode 100644 index 00000000000..ab61475a572 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/builders/union/union.ts @@ -0,0 +1,170 @@ +import { BaseSchema, MaybeValid, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { keys } from "../../utils/keys"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { enum_ } from "../enum"; +import { ObjectSchema } from "../object"; +import { getObjectLikeUtils, ObjectLikeSchema } from "../object-like"; +import { getSchemaUtils } from "../schema-utils"; +import { Discriminant } from "./discriminant"; +import { inferParsedDiscriminant, inferParsedUnion, inferRawDiscriminant, inferRawUnion, UnionSubtypes } from "./types"; + +export function union, U extends UnionSubtypes>( + discriminant: D, + union: U +): ObjectLikeSchema, inferParsedUnion> { + const rawDiscriminant = + typeof discriminant === "string" ? discriminant : (discriminant.rawDiscriminant as inferRawDiscriminant); + const parsedDiscriminant = + typeof discriminant === "string" + ? discriminant + : (discriminant.parsedDiscriminant as inferParsedDiscriminant); + + const discriminantValueSchema = enum_(keys(union) as string[]); + + const baseSchema: BaseSchema, inferParsedUnion> = { + parse: (raw, opts) => { + return transformAndValidateUnion({ + value: raw, + discriminant: rawDiscriminant, + transformedDiscriminant: parsedDiscriminant, + transformDiscriminantValue: (discriminantValue) => + discriminantValueSchema.parse(discriminantValue, { + allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawDiscriminant], + }), + getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], + allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, + transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => + additionalPropertiesSchema.parse(additionalProperties, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + json: (parsed, opts) => { + return transformAndValidateUnion({ + value: parsed, + discriminant: parsedDiscriminant, + transformedDiscriminant: rawDiscriminant, + transformDiscriminantValue: (discriminantValue) => + discriminantValueSchema.json(discriminantValue, { + allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedDiscriminant], + }), + getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], + allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, + transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => + additionalPropertiesSchema.json(additionalProperties, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + getType: () => SchemaType.UNION, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + }; +} + +function transformAndValidateUnion< + TransformedDiscriminant extends string, + TransformedDiscriminantValue extends string, + TransformedAdditionalProperties +>({ + value, + discriminant, + transformedDiscriminant, + transformDiscriminantValue, + getAdditionalPropertiesSchema, + allowUnrecognizedUnionMembers = false, + transformAdditionalProperties, + breadcrumbsPrefix = [], +}: { + value: unknown; + discriminant: string; + transformedDiscriminant: TransformedDiscriminant; + transformDiscriminantValue: (discriminantValue: unknown) => MaybeValid; + getAdditionalPropertiesSchema: (discriminantValue: string) => ObjectSchema | undefined; + allowUnrecognizedUnionMembers: boolean | undefined; + transformAdditionalProperties: ( + additionalProperties: unknown, + additionalPropertiesSchema: ObjectSchema + ) => MaybeValid; + breadcrumbsPrefix: string[] | undefined; +}): MaybeValid & TransformedAdditionalProperties> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + const { [discriminant]: discriminantValue, ...additionalProperties } = value; + + if (discriminantValue == null) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: `Missing discriminant ("${discriminant}")`, + }, + ], + }; + } + + const transformedDiscriminantValue = transformDiscriminantValue(discriminantValue); + if (!transformedDiscriminantValue.ok) { + return { + ok: false, + errors: transformedDiscriminantValue.errors, + }; + } + + const additionalPropertiesSchema = getAdditionalPropertiesSchema(transformedDiscriminantValue.value); + + if (additionalPropertiesSchema == null) { + if (allowUnrecognizedUnionMembers) { + return { + ok: true, + value: { + [transformedDiscriminant]: transformedDiscriminantValue.value, + ...additionalProperties, + } as Record & TransformedAdditionalProperties, + }; + } else { + return { + ok: false, + errors: [ + { + path: [...breadcrumbsPrefix, discriminant], + message: "Unexpected discriminant value", + }, + ], + }; + } + } + + const transformedAdditionalProperties = transformAdditionalProperties( + additionalProperties, + additionalPropertiesSchema + ); + if (!transformedAdditionalProperties.ok) { + return transformedAdditionalProperties; + } + + return { + ok: true, + value: { + [transformedDiscriminant]: discriminantValue, + ...transformedAdditionalProperties.value, + } as Record & TransformedAdditionalProperties, + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/index.ts new file mode 100644 index 00000000000..5429d8b43eb --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/index.ts @@ -0,0 +1,2 @@ +export * from "./builders"; +export type { inferParsed, inferRaw, Schema, SchemaOptions } from "./Schema"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/MaybePromise.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/MaybePromise.ts new file mode 100644 index 00000000000..9cd354b3418 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/MaybePromise.ts @@ -0,0 +1 @@ +export type MaybePromise = T | Promise; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts new file mode 100644 index 00000000000..4111d703cd0 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts @@ -0,0 +1,15 @@ +export type addQuestionMarksToNullableProperties = { + [K in OptionalKeys]?: T[K]; +} & Pick>; + +export type OptionalKeys = { + [K in keyof T]-?: undefined extends T[K] + ? K + : null extends T[K] + ? K + : 1 extends (any extends T[K] ? 0 : 1) + ? never + : K; +}[keyof T]; + +export type RequiredKeys = Exclude>; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/createIdentitySchemaCreator.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/createIdentitySchemaCreator.ts new file mode 100644 index 00000000000..de107cf5ee1 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/createIdentitySchemaCreator.ts @@ -0,0 +1,21 @@ +import { getSchemaUtils } from "../builders/schema-utils"; +import { BaseSchema, MaybeValid, Schema, SchemaOptions, SchemaType } from "../Schema"; +import { maybeSkipValidation } from "./maybeSkipValidation"; + +export function createIdentitySchemaCreator( + schemaType: SchemaType, + validate: (value: unknown, opts?: SchemaOptions) => MaybeValid +): () => Schema { + return () => { + const baseSchema: BaseSchema = { + parse: validate, + json: validate, + getType: () => schemaType, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/entries.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/entries.ts new file mode 100644 index 00000000000..e122952137d --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/entries.ts @@ -0,0 +1,3 @@ +export function entries(object: T): [keyof T, T[keyof T]][] { + return Object.entries(object) as [keyof T, T[keyof T]][]; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/filterObject.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/filterObject.ts new file mode 100644 index 00000000000..2c25a3455bc --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/filterObject.ts @@ -0,0 +1,10 @@ +export function filterObject(obj: T, keysToInclude: K[]): Pick { + const keysToIncludeSet = new Set(keysToInclude); + return Object.entries(obj).reduce((acc, [key, value]) => { + if (keysToIncludeSet.has(key as K)) { + acc[key as K] = value; + } + return acc; + // eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter + }, {} as Pick); +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/getErrorMessageForIncorrectType.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/getErrorMessageForIncorrectType.ts new file mode 100644 index 00000000000..438012df418 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/getErrorMessageForIncorrectType.ts @@ -0,0 +1,21 @@ +export function getErrorMessageForIncorrectType(value: unknown, expectedType: string): string { + return `Expected ${expectedType}. Received ${getTypeAsString(value)}.`; +} + +function getTypeAsString(value: unknown): string { + if (Array.isArray(value)) { + return "list"; + } + if (value === null) { + return "null"; + } + switch (typeof value) { + case "string": + return `"${value}"`; + case "number": + case "boolean": + case "undefined": + return `${value}`; + } + return typeof value; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/isPlainObject.ts new file mode 100644 index 00000000000..db82a722c35 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/isPlainObject.ts @@ -0,0 +1,17 @@ +// borrowed from https://github.com/lodash/lodash/blob/master/isPlainObject.js +export function isPlainObject(value: unknown): value is Record { + if (typeof value !== "object" || value === null) { + return false; + } + + if (Object.getPrototypeOf(value) === null) { + return true; + } + + let proto = value; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + + return Object.getPrototypeOf(value) === proto; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/keys.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/keys.ts new file mode 100644 index 00000000000..01867098287 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/keys.ts @@ -0,0 +1,3 @@ +export function keys(object: T): (keyof T)[] { + return Object.keys(object) as (keyof T)[]; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/maybeSkipValidation.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/maybeSkipValidation.ts new file mode 100644 index 00000000000..86c07abf2b4 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/maybeSkipValidation.ts @@ -0,0 +1,38 @@ +import { BaseSchema, MaybeValid, SchemaOptions } from "../Schema"; + +export function maybeSkipValidation, Raw, Parsed>(schema: S): S { + return { + ...schema, + json: transformAndMaybeSkipValidation(schema.json), + parse: transformAndMaybeSkipValidation(schema.parse), + }; +} + +function transformAndMaybeSkipValidation( + transform: (value: unknown, opts?: SchemaOptions) => MaybeValid +): (value: unknown, opts?: SchemaOptions) => MaybeValid { + return (value, opts): MaybeValid => { + const transformed = transform(value, opts); + const { skipValidation = false } = opts ?? {}; + if (!transformed.ok && skipValidation) { + // eslint-disable-next-line no-console + console.warn( + [ + "Failed to validate.", + ...transformed.errors.map( + (error) => + " - " + + (error.path.length > 0 ? `${error.path.join(".")}: ${error.message}` : error.message) + ), + ].join("\n") + ); + + return { + ok: true, + value: value as T, + }; + } else { + return transformed; + } + }; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/partition.ts b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/partition.ts new file mode 100644 index 00000000000..f58d6f3d35f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/schemas/utils/partition.ts @@ -0,0 +1,12 @@ +export function partition(items: readonly T[], predicate: (item: T) => boolean): [T[], T[]] { + const trueItems: T[] = [], + falseItems: T[] = []; + for (const item of items) { + if (predicate(item)) { + trueItems.push(item); + } else { + falseItems.push(item); + } + } + return [trueItems, falseItems]; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/errors/SeedMixedFileDirectoryError.ts b/seed/ts-sdk/mixed-file-directory/src/errors/SeedMixedFileDirectoryError.ts new file mode 100644 index 00000000000..27e6fbaa642 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/errors/SeedMixedFileDirectoryError.ts @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export class SeedMixedFileDirectoryError extends Error { + readonly statusCode?: number; + readonly body?: unknown; + + constructor({ message, statusCode, body }: { message?: string; statusCode?: number; body?: unknown }) { + super(buildMessage({ message, statusCode, body })); + Object.setPrototypeOf(this, SeedMixedFileDirectoryError.prototype); + if (statusCode != null) { + this.statusCode = statusCode; + } + + if (body !== undefined) { + this.body = body; + } + } +} + +function buildMessage({ + message, + statusCode, + body, +}: { + message: string | undefined; + statusCode: number | undefined; + body: unknown | undefined; +}): string { + let lines: string[] = []; + if (message != null) { + lines.push(message); + } + + if (statusCode != null) { + lines.push(`Status code: ${statusCode.toString()}`); + } + + if (body != null) { + lines.push(`Body: ${JSON.stringify(body, undefined, 2)}`); + } + + return lines.join("\n"); +} diff --git a/seed/ts-sdk/mixed-file-directory/src/errors/SeedMixedFileDirectoryTimeoutError.ts b/seed/ts-sdk/mixed-file-directory/src/errors/SeedMixedFileDirectoryTimeoutError.ts new file mode 100644 index 00000000000..ae981d511db --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/errors/SeedMixedFileDirectoryTimeoutError.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export class SeedMixedFileDirectoryTimeoutError extends Error { + constructor() { + super("Timeout"); + Object.setPrototypeOf(this, SeedMixedFileDirectoryTimeoutError.prototype); + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/errors/index.ts b/seed/ts-sdk/mixed-file-directory/src/errors/index.ts new file mode 100644 index 00000000000..50f4dd76ba2 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/errors/index.ts @@ -0,0 +1,2 @@ +export { SeedMixedFileDirectoryError } from "./SeedMixedFileDirectoryError"; +export { SeedMixedFileDirectoryTimeoutError } from "./SeedMixedFileDirectoryTimeoutError"; diff --git a/seed/ts-sdk/mixed-file-directory/src/index.ts b/seed/ts-sdk/mixed-file-directory/src/index.ts new file mode 100644 index 00000000000..5c18f79a8c5 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/index.ts @@ -0,0 +1,3 @@ +export * as SeedMixedFileDirectory from "./api"; +export { SeedMixedFileDirectoryClient } from "./Client"; +export { SeedMixedFileDirectoryError, SeedMixedFileDirectoryTimeoutError } from "./errors"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/index.ts new file mode 100644 index 00000000000..3ce0a3e38e8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./resources"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/index.ts new file mode 100644 index 00000000000..f281c7d14eb --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/index.ts @@ -0,0 +1,4 @@ +export * as organization from "./organization"; +export * from "./organization/types"; +export * as user from "./user"; +export * from "./user/types"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/index.ts new file mode 100644 index 00000000000..eea524d6557 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/CreateOrganizationRequest.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/CreateOrganizationRequest.ts new file mode 100644 index 00000000000..7dd01a9fbf2 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/CreateOrganizationRequest.ts @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as SeedMixedFileDirectory from "../../../../api/index"; +import * as core from "../../../../core"; + +export const CreateOrganizationRequest: core.serialization.ObjectSchema< + serializers.CreateOrganizationRequest.Raw, + SeedMixedFileDirectory.CreateOrganizationRequest +> = core.serialization.object({ + name: core.serialization.string(), +}); + +export declare namespace CreateOrganizationRequest { + interface Raw { + name: string; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/Organization.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/Organization.ts new file mode 100644 index 00000000000..6a5f8ee12e3 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/Organization.ts @@ -0,0 +1,26 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as SeedMixedFileDirectory from "../../../../api/index"; +import * as core from "../../../../core"; +import { Id } from "../../../types/Id"; +import { User } from "../../user/types/User"; + +export const Organization: core.serialization.ObjectSchema< + serializers.Organization.Raw, + SeedMixedFileDirectory.Organization +> = core.serialization.object({ + id: Id, + name: core.serialization.string(), + users: core.serialization.list(User), +}); + +export declare namespace Organization { + interface Raw { + id: Id.Raw; + name: string; + users: User.Raw[]; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/index.ts new file mode 100644 index 00000000000..c4b0dd7c2d6 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/organization/types/index.ts @@ -0,0 +1,2 @@ +export * from "./Organization"; +export * from "./CreateOrganizationRequest"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/client/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/client/index.ts new file mode 100644 index 00000000000..abbe30ae7ad --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/client/index.ts @@ -0,0 +1 @@ +export * as list from "./list"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/client/list.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/client/list.ts new file mode 100644 index 00000000000..a3b2c2fbc97 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/client/list.ts @@ -0,0 +1,15 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as SeedMixedFileDirectory from "../../../../api/index"; +import * as core from "../../../../core"; +import { User } from "../types/User"; + +export const Response: core.serialization.Schema = + core.serialization.list(User); + +export declare namespace Response { + type Raw = User.Raw[]; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/index.ts new file mode 100644 index 00000000000..a931b36375c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./resources"; +export * from "./client"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/client/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/client/index.ts new file mode 100644 index 00000000000..ad7c868f2b7 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/client/index.ts @@ -0,0 +1 @@ +export * as listEvents from "./listEvents"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/client/listEvents.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/client/listEvents.ts new file mode 100644 index 00000000000..cc7f8179ad5 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/client/listEvents.ts @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../../../index"; +import * as SeedMixedFileDirectory from "../../../../../../api/index"; +import * as core from "../../../../../../core"; +import { Event } from "../types/Event"; + +export const Response: core.serialization.Schema< + serializers.user.events.listEvents.Response.Raw, + SeedMixedFileDirectory.user.Event[] +> = core.serialization.list(Event); + +export declare namespace Response { + type Raw = Event.Raw[]; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/index.ts new file mode 100644 index 00000000000..a931b36375c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./resources"; +export * from "./client"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/index.ts new file mode 100644 index 00000000000..20085af104d --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/index.ts @@ -0,0 +1,2 @@ +export * as metadata from "./metadata"; +export * from "./metadata/types"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/index.ts new file mode 100644 index 00000000000..eea524d6557 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/types/Metadata.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/types/Metadata.ts new file mode 100644 index 00000000000..53eb171dacd --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/types/Metadata.ts @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../../../../../index"; +import * as SeedMixedFileDirectory from "../../../../../../../../api/index"; +import * as core from "../../../../../../../../core"; +import { Id } from "../../../../../../../types/Id"; + +export const Metadata: core.serialization.ObjectSchema< + serializers.user.events.Metadata.Raw, + SeedMixedFileDirectory.user.events.Metadata +> = core.serialization.object({ + id: Id, + value: core.serialization.unknown(), +}); + +export declare namespace Metadata { + interface Raw { + id: Id.Raw; + value?: unknown; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/types/index.ts new file mode 100644 index 00000000000..8abb66966d0 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/resources/metadata/types/index.ts @@ -0,0 +1 @@ +export * from "./Metadata"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/types/Event.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/types/Event.ts new file mode 100644 index 00000000000..ac8dd8697c0 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/types/Event.ts @@ -0,0 +1,21 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../../../index"; +import * as SeedMixedFileDirectory from "../../../../../../api/index"; +import * as core from "../../../../../../core"; +import { Id } from "../../../../../types/Id"; + +export const Event: core.serialization.ObjectSchema = + core.serialization.object({ + id: Id, + name: core.serialization.string(), + }); + +export declare namespace Event { + interface Raw { + id: Id.Raw; + name: string; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/types/index.ts new file mode 100644 index 00000000000..6868d665e48 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/events/types/index.ts @@ -0,0 +1 @@ +export * from "./Event"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/index.ts new file mode 100644 index 00000000000..f8858c12a24 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/resources/index.ts @@ -0,0 +1,2 @@ +export * as events from "./events"; +export * from "./events/types"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/types/User.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/types/User.ts new file mode 100644 index 00000000000..d2c682d85cc --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/types/User.ts @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as SeedMixedFileDirectory from "../../../../api/index"; +import * as core from "../../../../core"; +import { Id } from "../../../types/Id"; + +export const User: core.serialization.ObjectSchema = + core.serialization.object({ + id: Id, + name: core.serialization.string(), + age: core.serialization.number(), + }); + +export declare namespace User { + interface Raw { + id: Id.Raw; + name: string; + age: number; + } +} diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/types/index.ts new file mode 100644 index 00000000000..3ce758c1197 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/resources/user/types/index.ts @@ -0,0 +1 @@ +export * from "./User"; diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/types/Id.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/types/Id.ts new file mode 100644 index 00000000000..51c5b259c49 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/types/Id.ts @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../index"; +import * as SeedMixedFileDirectory from "../../api/index"; +import * as core from "../../core"; + +export const Id: core.serialization.Schema = core.serialization.string(); + +export declare namespace Id { + type Raw = string; +} diff --git a/seed/ts-sdk/mixed-file-directory/src/serialization/types/index.ts b/seed/ts-sdk/mixed-file-directory/src/serialization/types/index.ts new file mode 100644 index 00000000000..6823c3ab871 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/serialization/types/index.ts @@ -0,0 +1 @@ +export * from "./Id"; diff --git a/seed/ts-sdk/mixed-file-directory/tests/custom.test.ts b/seed/ts-sdk/mixed-file-directory/tests/custom.test.ts new file mode 100644 index 00000000000..7f5e031c839 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/custom.test.ts @@ -0,0 +1,13 @@ +/** + * This is a custom test file, if you wish to add more tests + * to your SDK. + * Be sure to mark this file in `.fernignore`. + * + * If you include example requests/responses in your fern definition, + * you will have tests automatically generated for you. + */ +describe("test", () => { + it("default", () => { + expect(true).toBe(true); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/Fetcher.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/Fetcher.test.ts new file mode 100644 index 00000000000..0e14a8c77f8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/Fetcher.test.ts @@ -0,0 +1,25 @@ +import fetchMock from "fetch-mock-jest"; +import { Fetcher, fetcherImpl } from "../../../src/core/fetcher/Fetcher"; + +describe("Test fetcherImpl", () => { + it("should handle successful request", async () => { + const mockArgs: Fetcher.Args = { + url: "https://httpbin.org/post", + method: "POST", + headers: { "X-Test": "x-test-header" }, + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + }; + + fetchMock.mock("https://httpbin.org/post", 200, { + response: JSON.stringify({ data: "test" }), + }); + + const result = await fetcherImpl(mockArgs); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.body).toEqual({ data: "test" }); + } + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/createRequestUrl.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/createRequestUrl.test.ts new file mode 100644 index 00000000000..f2cd24b6721 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/createRequestUrl.test.ts @@ -0,0 +1,51 @@ +import { createRequestUrl } from "../../../src/core/fetcher/createRequestUrl"; + +describe("Test createRequestUrl", () => { + it("should return the base URL when no query parameters are provided", () => { + const baseUrl = "https://api.example.com"; + expect(createRequestUrl(baseUrl)).toBe(baseUrl); + }); + + it("should append simple query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { key: "value", another: "param" }; + expect(createRequestUrl(baseUrl, queryParams)).toBe("https://api.example.com?key=value&another=param"); + }); + + it("should handle array query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { items: ["a", "b", "c"] }; + expect(createRequestUrl(baseUrl, queryParams)).toBe("https://api.example.com?items=a&items=b&items=c"); + }); + + it("should handle object query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { filter: { name: "John", age: 30 } }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?filter%5Bname%5D=John&filter%5Bage%5D=30" + ); + }); + + it("should handle mixed types of query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { + simple: "value", + array: ["x", "y"], + object: { key: "value" }, + }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?simple=value&array=x&array=y&object%5Bkey%5D=value" + ); + }); + + it("should handle empty query parameters object", () => { + const baseUrl = "https://api.example.com"; + expect(createRequestUrl(baseUrl, {})).toBe(baseUrl); + }); + + it("should encode special characters in query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { special: "a&b=c d" }; + expect(createRequestUrl(baseUrl, queryParams)).toBe("https://api.example.com?special=a%26b%3Dc%20d"); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getFetchFn.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getFetchFn.test.ts new file mode 100644 index 00000000000..9b315ad095a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getFetchFn.test.ts @@ -0,0 +1,22 @@ +import { RUNTIME } from "../../../src/core/runtime"; +import { getFetchFn } from "../../../src/core/fetcher/getFetchFn"; + +describe("Test for getFetchFn", () => { + it("should get node-fetch function", async () => { + if (RUNTIME.type == "node") { + if (RUNTIME.parsedVersion != null && RUNTIME.parsedVersion >= 18) { + expect(await getFetchFn()).toBe(fetch); + } else { + expect(await getFetchFn()).toEqual((await import("node-fetch")).default as any); + } + } + }); + + it("should get fetch function", async () => { + if (RUNTIME.type == "browser") { + const fetchFn = await getFetchFn(); + expect(typeof fetchFn).toBe("function"); + expect(fetchFn.name).toBe("fetch"); + } + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getRequestBody.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getRequestBody.test.ts new file mode 100644 index 00000000000..1b1462c51bd --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getRequestBody.test.ts @@ -0,0 +1,81 @@ +import { RUNTIME } from "../../../src/core/runtime"; +import { getRequestBody } from "../../../src/core/fetcher/getRequestBody"; + +if (RUNTIME.type === "browser") { + require("jest-fetch-mock").enableMocks(); +} + +describe("Test getRequestBody", () => { + it("should return FormData as is in Node environment", async () => { + if (RUNTIME.type === "node") { + const formData = new (await import("formdata-node")).FormData(); + formData.append("key", "value"); + const result = await getRequestBody({ + body: formData, + type: "file", + }); + expect(result).toBe(formData); + } + }); + + it("should stringify body if not FormData in Node environment", async () => { + if (RUNTIME.type === "node") { + const body = { key: "value" }; + const result = await getRequestBody({ + body, + type: "json", + }); + expect(result).toBe('{"key":"value"}'); + } + }); + + it("should return FormData in browser environment", async () => { + if (RUNTIME.type === "browser") { + const formData = new (await import("form-data")).default(); + formData.append("key", "value"); + const result = await getRequestBody({ + body: formData, + type: "file", + }); + expect(result).toBe(formData); + } + }); + + it("should stringify body if not FormData in browser environment", async () => { + if (RUNTIME.type === "browser") { + const body = { key: "value" }; + const result = await getRequestBody({ + body, + type: "json", + }); + expect(result).toBe('{"key":"value"}'); + } + }); + + it("should return the Uint8Array", async () => { + const input = new Uint8Array([1, 2, 3]); + const result = await getRequestBody({ + body: input, + type: "bytes", + }); + expect(result).toBe(input); + }); + + it("should return the input for content-type 'application/x-www-form-urlencoded'", async () => { + const input = "key=value&another=param"; + const result = await getRequestBody({ + body: input, + type: "other", + }); + expect(result).toBe(input); + }); + + it("should JSON stringify objects", async () => { + const input = { key: "value" }; + const result = await getRequestBody({ + body: input, + type: "json", + }); + expect(result).toBe('{"key":"value"}'); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getResponseBody.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getResponseBody.test.ts new file mode 100644 index 00000000000..3510779e3f9 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/getResponseBody.test.ts @@ -0,0 +1,68 @@ +import { RUNTIME } from "../../../src/core/runtime"; +import { getResponseBody } from "../../../src/core/fetcher/getResponseBody"; +import { chooseStreamWrapper } from "../../../src/core/fetcher/stream-wrappers/chooseStreamWrapper"; + +if (RUNTIME.type === "browser") { + require("jest-fetch-mock").enableMocks(); +} + +describe("Test getResponseBody", () => { + it("should handle blob response type", async () => { + const mockBlob = new Blob(["test"], { type: "text/plain" }); + const mockResponse = new Response(mockBlob); + const result = await getResponseBody(mockResponse, "blob"); + // @ts-expect-error + expect(result.constructor.name).toBe("Blob"); + }); + + it("should handle sse response type", async () => { + if (RUNTIME.type === "node") { + const mockStream = new ReadableStream(); + const mockResponse = new Response(mockStream); + const result = await getResponseBody(mockResponse, "sse"); + expect(result).toBe(mockStream); + } + }); + + it("should handle streaming response type", async () => { + if (RUNTIME.type === "node") { + const mockStream = new ReadableStream(); + const mockResponse = new Response(mockStream); + const result = await getResponseBody(mockResponse, "streaming"); + // need to reinstantiate string as a result of locked state in Readable Stream after registration with Response + expect(JSON.stringify(result)).toBe(JSON.stringify(await chooseStreamWrapper(new ReadableStream()))); + } + }); + + it("should handle text response type", async () => { + const mockResponse = new Response("test text"); + const result = await getResponseBody(mockResponse, "text"); + expect(result).toBe("test text"); + }); + + it("should handle JSON response", async () => { + const mockJson = { key: "value" }; + const mockResponse = new Response(JSON.stringify(mockJson)); + const result = await getResponseBody(mockResponse); + expect(result).toEqual(mockJson); + }); + + it("should handle empty response", async () => { + const mockResponse = new Response(""); + const result = await getResponseBody(mockResponse); + expect(result).toBeUndefined(); + }); + + it("should handle non-JSON response", async () => { + const mockResponse = new Response("invalid json"); + const result = await getResponseBody(mockResponse); + expect(result).toEqual({ + ok: false, + error: { + reason: "non-json", + statusCode: 200, + rawBody: "invalid json", + }, + }); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/makeRequest.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/makeRequest.test.ts new file mode 100644 index 00000000000..5969d5155ac --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/makeRequest.test.ts @@ -0,0 +1,58 @@ +import { RUNTIME } from "../../../src/core/runtime"; +import { makeRequest } from "../../../src/core/fetcher/makeRequest"; + +if (RUNTIME.type === "browser") { + require("jest-fetch-mock").enableMocks(); +} + +describe("Test makeRequest", () => { + const mockPostUrl = "https://httpbin.org/post"; + const mockGetUrl = "https://httpbin.org/get"; + const mockHeaders = { "Content-Type": "application/json" }; + const mockBody = JSON.stringify({ key: "value" }); + + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ test: "successful" }), { status: 200 })); + }); + + it("should handle POST request correctly", async () => { + const response = await makeRequest(mockFetch, mockPostUrl, "POST", mockHeaders, mockBody); + const responseBody = await response.json(); + expect(responseBody).toEqual({ test: "successful" }); + expect(mockFetch).toHaveBeenCalledTimes(1); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe(mockPostUrl); + expect(calledOptions).toEqual( + expect.objectContaining({ + method: "POST", + headers: mockHeaders, + body: mockBody, + credentials: undefined, + }) + ); + expect(calledOptions.signal).toBeDefined(); + expect(calledOptions.signal).toBeInstanceOf(AbortSignal); + }); + + it("should handle GET request correctly", async () => { + const response = await makeRequest(mockFetch, mockGetUrl, "GET", mockHeaders, undefined); + const responseBody = await response.json(); + expect(responseBody).toEqual({ test: "successful" }); + expect(mockFetch).toHaveBeenCalledTimes(1); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe(mockGetUrl); + expect(calledOptions).toEqual( + expect.objectContaining({ + method: "GET", + headers: mockHeaders, + body: undefined, + credentials: undefined, + }) + ); + expect(calledOptions.signal).toBeDefined(); + expect(calledOptions.signal).toBeInstanceOf(AbortSignal); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/requestWithRetries.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/requestWithRetries.test.ts new file mode 100644 index 00000000000..b53e04367c5 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/requestWithRetries.test.ts @@ -0,0 +1,85 @@ +import { RUNTIME } from "../../../src/core/runtime"; +import { requestWithRetries } from "../../../src/core/fetcher/requestWithRetries"; + +if (RUNTIME.type === "browser") { + require("jest-fetch-mock").enableMocks(); +} + +describe("Test exponential backoff", () => { + let mockFetch: jest.Mock; + let originalSetTimeout: typeof setTimeout; + + beforeEach(() => { + mockFetch = jest.fn(); + originalSetTimeout = global.setTimeout; + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + global.setTimeout = originalSetTimeout; + }); + + it("should retry on 408, 409, 429, 500+", async () => { + mockFetch + .mockResolvedValueOnce(new Response("", { status: 408 })) + .mockResolvedValueOnce(new Response("", { status: 409 })) + .mockResolvedValueOnce(new Response("", { status: 429 })) + .mockResolvedValueOnce(new Response("", { status: 500 })) + .mockResolvedValueOnce(new Response("", { status: 502 })) + .mockResolvedValueOnce(new Response("", { status: 200 })) + .mockResolvedValueOnce(new Response("", { status: 408 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 10); + + await jest.advanceTimersByTimeAsync(10000); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(6); + expect(response.status).toBe(200); + }); + + it("should retry max 3 times", async () => { + mockFetch + .mockResolvedValueOnce(new Response("", { status: 408 })) + .mockResolvedValueOnce(new Response("", { status: 409 })) + .mockResolvedValueOnce(new Response("", { status: 429 })) + .mockResolvedValueOnce(new Response("", { status: 429 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + + await jest.advanceTimersByTimeAsync(10000); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(4); + expect(response.status).toBe(429); + }); + it("should not retry on 200", async () => { + mockFetch + .mockResolvedValueOnce(new Response("", { status: 200 })) + .mockResolvedValueOnce(new Response("", { status: 409 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + + await jest.advanceTimersByTimeAsync(10000); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(response.status).toBe(200); + }); + + it("should retry with exponential backoff timing", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + const maxRetries = 7; + const responsePromise = requestWithRetries(() => mockFetch(), maxRetries); + expect(mockFetch).toHaveBeenCalledTimes(1); + + const delays = [1, 2, 4, 8, 16, 32, 64]; + for (let i = 0; i < delays.length; i++) { + await jest.advanceTimersByTimeAsync(delays[i] as number); + expect(mockFetch).toHaveBeenCalledTimes(Math.min(i + 2, maxRetries + 1)); + } + const response = await responsePromise; + expect(response.status).toBe(500); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/signals.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/signals.test.ts new file mode 100644 index 00000000000..9cabfa07447 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/signals.test.ts @@ -0,0 +1,69 @@ +import { anySignal, getTimeoutSignal } from "../../../src/core/fetcher/signals"; + +describe("Test getTimeoutSignal", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should return an object with signal and abortId", () => { + const { signal, abortId } = getTimeoutSignal(1000); + + expect(signal).toBeDefined(); + expect(abortId).toBeDefined(); + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal.aborted).toBe(false); + }); + + it("should create a signal that aborts after the specified timeout", () => { + const timeoutMs = 5000; + const { signal } = getTimeoutSignal(timeoutMs); + + expect(signal.aborted).toBe(false); + + jest.advanceTimersByTime(timeoutMs - 1); + expect(signal.aborted).toBe(false); + + jest.advanceTimersByTime(1); + expect(signal.aborted).toBe(true); + }); +}); + +describe("Test anySignal", () => { + it("should return an AbortSignal", () => { + const signal = anySignal(new AbortController().signal); + expect(signal).toBeInstanceOf(AbortSignal); + }); + + it("should abort when any of the input signals is aborted", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + const signal = anySignal(controller1.signal, controller2.signal); + + expect(signal.aborted).toBe(false); + controller1.abort(); + expect(signal.aborted).toBe(true); + }); + + it("should handle an array of signals", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + const signal = anySignal([controller1.signal, controller2.signal]); + + expect(signal.aborted).toBe(false); + controller2.abort(); + expect(signal.aborted).toBe(true); + }); + + it("should abort immediately if one of the input signals is already aborted", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + controller1.abort(); + + const signal = anySignal(controller1.signal, controller2.signal); + expect(signal.aborted).toBe(true); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/Node18UniversalStreamWrapper.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/Node18UniversalStreamWrapper.test.ts new file mode 100644 index 00000000000..1dc9be0cc0e --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/Node18UniversalStreamWrapper.test.ts @@ -0,0 +1,178 @@ +import { Node18UniversalStreamWrapper } from "../../../../src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper"; + +describe("Node18UniversalStreamWrapper", () => { + it("should set encoding to utf-8", async () => { + const rawStream = new ReadableStream(); + const stream = new Node18UniversalStreamWrapper(rawStream); + const setEncodingSpy = jest.spyOn(stream, "setEncoding"); + + stream.setEncoding("utf-8"); + + expect(setEncodingSpy).toHaveBeenCalledWith("utf-8"); + }); + + it("should register an event listener for readable", async () => { + const rawStream = new ReadableStream(); + const stream = new Node18UniversalStreamWrapper(rawStream); + const onSpy = jest.spyOn(stream, "on"); + + stream.on("readable", () => {}); + + expect(onSpy).toHaveBeenCalledWith("readable", expect.any(Function)); + }); + + it("should remove an event listener for data", async () => { + const rawStream = new ReadableStream(); + const stream = new Node18UniversalStreamWrapper(rawStream); + const offSpy = jest.spyOn(stream, "off"); + + const fn = () => {}; + stream.on("data", fn); + stream.off("data", fn); + + expect(offSpy).toHaveBeenCalledWith("data", expect.any(Function)); + }); + + it("should write to dest when calling pipe to writable stream", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + const stream = new Node18UniversalStreamWrapper(rawStream); + const dest = new WritableStream({ + write(chunk) { + expect(chunk).toEqual(new TextEncoder().encode("test")); + }, + }); + + stream.pipe(dest); + }); + + it("should write to dest when calling pipe to node writable stream", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + const stream = new Node18UniversalStreamWrapper(rawStream); + const dest = new (await import("readable-stream")).Writable({ + write(chunk, encoding, callback) { + expect(chunk.toString()).toEqual("test"); + callback(); + }, + }); + + stream.pipe(dest); + }); + + it("should write nothing when calling pipe and unpipe", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + const stream = new Node18UniversalStreamWrapper(rawStream); + const buffer: Uint8Array[] = []; + const dest = new WritableStream({ + write(chunk) { + buffer.push(chunk); + }, + }); + + stream.pipe(dest); + stream.unpipe(dest); + expect(buffer).toEqual([]); + }); + + it("should destroy the stream", async () => { + const rawStream = new ReadableStream(); + const stream = new Node18UniversalStreamWrapper(rawStream); + const destroySpy = jest.spyOn(stream, "destroy"); + + stream.destroy(); + + expect(destroySpy).toHaveBeenCalled(); + }); + + it("should pause and resume the stream", async () => { + const rawStream = new ReadableStream(); + const stream = new Node18UniversalStreamWrapper(rawStream); + const pauseSpy = jest.spyOn(stream, "pause"); + const resumeSpy = jest.spyOn(stream, "resume"); + + expect(stream.isPaused).toBe(false); + stream.pause(); + expect(stream.isPaused).toBe(true); + stream.resume(); + + expect(pauseSpy).toHaveBeenCalled(); + expect(resumeSpy).toHaveBeenCalled(); + }); + + it("should read the stream", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + const stream = new Node18UniversalStreamWrapper(rawStream); + + expect(await stream.read()).toEqual(new TextEncoder().encode("test")); + expect(await stream.read()).toEqual(new TextEncoder().encode("test")); + }); + + it("should read the stream as text", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + const stream = new Node18UniversalStreamWrapper(rawStream); + + const data = await stream.text(); + + expect(data).toEqual("testtest"); + }); + + it("should read the stream as json", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(JSON.stringify({ test: "test" }))); + controller.close(); + }, + }); + const stream = new Node18UniversalStreamWrapper(rawStream); + + const data = await stream.json(); + + expect(data).toEqual({ test: "test" }); + }); + + it("should allow use with async iteratable stream", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + let data = ""; + const stream = new Node18UniversalStreamWrapper(rawStream); + for await (const chunk of stream) { + data += new TextDecoder().decode(chunk); + } + + expect(data).toEqual("testtest"); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/NodePre18StreamWrapper.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/NodePre18StreamWrapper.test.ts new file mode 100644 index 00000000000..0c99d3b2655 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/NodePre18StreamWrapper.test.ts @@ -0,0 +1,124 @@ +import { NodePre18StreamWrapper } from "../../../../src/core/fetcher/stream-wrappers/NodePre18StreamWrapper"; + +describe("NodePre18StreamWrapper", () => { + it("should set encoding to utf-8", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + const setEncodingSpy = jest.spyOn(stream, "setEncoding"); + + stream.setEncoding("utf-8"); + + expect(setEncodingSpy).toHaveBeenCalledWith("utf-8"); + }); + + it("should register an event listener for readable", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + const onSpy = jest.spyOn(stream, "on"); + + stream.on("readable", () => {}); + + expect(onSpy).toHaveBeenCalledWith("readable", expect.any(Function)); + }); + + it("should remove an event listener for data", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + const offSpy = jest.spyOn(stream, "off"); + + const fn = () => {}; + stream.on("data", fn); + stream.off("data", fn); + + expect(offSpy).toHaveBeenCalledWith("data", expect.any(Function)); + }); + + it("should write to dest when calling pipe to node writable stream", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + const dest = new (await import("readable-stream")).Writable({ + write(chunk, encoding, callback) { + expect(chunk.toString()).toEqual("test"); + callback(); + }, + }); + + stream.pipe(dest); + }); + + it("should write nothing when calling pipe and unpipe", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + const buffer: Uint8Array[] = []; + const dest = new (await import("readable-stream")).Writable({ + write(chunk, encoding, callback) { + buffer.push(chunk); + callback(); + }, + }); + stream.pipe(dest); + stream.unpipe(); + + expect(buffer).toEqual([]); + }); + + it("should destroy the stream", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + const destroySpy = jest.spyOn(stream, "destroy"); + + stream.destroy(); + + expect(destroySpy).toHaveBeenCalledWith(); + }); + + it("should pause the stream and resume", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + const pauseSpy = jest.spyOn(stream, "pause"); + + stream.pause(); + expect(stream.isPaused).toBe(true); + stream.resume(); + expect(stream.isPaused).toBe(false); + + expect(pauseSpy).toHaveBeenCalledWith(); + }); + + it("should read the stream", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + + expect(await stream.read()).toEqual("test"); + expect(await stream.read()).toEqual("test"); + }); + + it("should read the stream as text", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + const stream = new NodePre18StreamWrapper(rawStream); + + const data = await stream.text(); + + expect(data).toEqual("testtest"); + }); + + it("should read the stream as json", async () => { + const rawStream = (await import("readable-stream")).Readable.from([JSON.stringify({ test: "test" })]); + const stream = new NodePre18StreamWrapper(rawStream); + + const data = await stream.json(); + + expect(data).toEqual({ test: "test" }); + }); + + it("should allow use with async iteratable stream", async () => { + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); + let data = ""; + const stream = new NodePre18StreamWrapper(rawStream); + for await (const chunk of stream) { + data += chunk; + } + + expect(data).toEqual("testtest"); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/UndiciStreamWrapper.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/UndiciStreamWrapper.test.ts new file mode 100644 index 00000000000..1d171ce6c67 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/UndiciStreamWrapper.test.ts @@ -0,0 +1,153 @@ +import { UndiciStreamWrapper } from "../../../../src/core/fetcher/stream-wrappers/UndiciStreamWrapper"; + +describe("UndiciStreamWrapper", () => { + it("should set encoding to utf-8", async () => { + const rawStream = new ReadableStream(); + const stream = new UndiciStreamWrapper(rawStream); + const setEncodingSpy = jest.spyOn(stream, "setEncoding"); + + stream.setEncoding("utf-8"); + + expect(setEncodingSpy).toHaveBeenCalledWith("utf-8"); + }); + + it("should register an event listener for readable", async () => { + const rawStream = new ReadableStream(); + const stream = new UndiciStreamWrapper(rawStream); + const onSpy = jest.spyOn(stream, "on"); + + stream.on("readable", () => {}); + + expect(onSpy).toHaveBeenCalledWith("readable", expect.any(Function)); + }); + + it("should remove an event listener for data", async () => { + const rawStream = new ReadableStream(); + const stream = new UndiciStreamWrapper(rawStream); + const offSpy = jest.spyOn(stream, "off"); + + const fn = () => {}; + stream.on("data", fn); + stream.off("data", fn); + + expect(offSpy).toHaveBeenCalledWith("data", expect.any(Function)); + }); + + it("should write to dest when calling pipe to writable stream", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + const stream = new UndiciStreamWrapper(rawStream); + const dest = new WritableStream({ + write(chunk) { + expect(chunk).toEqual(new TextEncoder().encode("test")); + }, + }); + + stream.pipe(dest); + }); + + it("should write nothing when calling pipe and unpipe", async () => { + const rawStream = new ReadableStream(); + const stream = new UndiciStreamWrapper(rawStream); + const buffer: Uint8Array[] = []; + const dest = new WritableStream({ + write(chunk) { + buffer.push(chunk); + }, + }); + stream.pipe(dest); + stream.unpipe(dest); + + expect(buffer).toEqual([]); + }); + + it("should destroy the stream", async () => { + const rawStream = new ReadableStream(); + const stream = new UndiciStreamWrapper(rawStream); + const destroySpy = jest.spyOn(stream, "destroy"); + + stream.destroy(); + + expect(destroySpy).toHaveBeenCalled(); + }); + + it("should pause and resume the stream", async () => { + const rawStream = new ReadableStream(); + const stream = new UndiciStreamWrapper(rawStream); + const pauseSpy = jest.spyOn(stream, "pause"); + const resumeSpy = jest.spyOn(stream, "resume"); + + expect(stream.isPaused).toBe(false); + stream.pause(); + expect(stream.isPaused).toBe(true); + stream.resume(); + + expect(pauseSpy).toHaveBeenCalled(); + expect(resumeSpy).toHaveBeenCalled(); + }); + + it("should read the stream", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + const stream = new UndiciStreamWrapper(rawStream); + + expect(await stream.read()).toEqual(new TextEncoder().encode("test")); + expect(await stream.read()).toEqual(new TextEncoder().encode("test")); + }); + + it("should read the stream as text", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + const stream = new UndiciStreamWrapper(rawStream); + + const data = await stream.text(); + + expect(data).toEqual("testtest"); + }); + + it("should read the stream as json", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(JSON.stringify({ test: "test" }))); + controller.close(); + }, + }); + const stream = new UndiciStreamWrapper(rawStream); + + const data = await stream.json(); + + expect(data).toEqual({ test: "test" }); + }); + + it("should allow use with async iteratable stream", async () => { + const rawStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test")); + controller.enqueue(new TextEncoder().encode("test")); + controller.close(); + }, + }); + let data = ""; + const stream = new UndiciStreamWrapper(rawStream); + for await (const chunk of stream) { + data += new TextDecoder().decode(chunk); + } + + expect(data).toEqual("testtest"); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/chooseStreamWrapper.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/chooseStreamWrapper.test.ts new file mode 100644 index 00000000000..17cf37a2f7f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/chooseStreamWrapper.test.ts @@ -0,0 +1,43 @@ +import { RUNTIME } from "../../../../src/core/runtime"; +import { chooseStreamWrapper } from "../../../../src/core/fetcher/stream-wrappers/chooseStreamWrapper"; +import { Node18UniversalStreamWrapper } from "../../../../src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper"; +import { NodePre18StreamWrapper } from "../../../../src/core/fetcher/stream-wrappers/NodePre18StreamWrapper"; +import { UndiciStreamWrapper } from "../../../../src/core/fetcher/stream-wrappers/UndiciStreamWrapper"; + +describe("chooseStreamWrapper", () => { + beforeEach(() => { + RUNTIME.type = "unknown"; + RUNTIME.parsedVersion = 0; + }); + + it('should return a Node18UniversalStreamWrapper when RUNTIME.type is "node" and RUNTIME.parsedVersion is not null and RUNTIME.parsedVersion is greater than or equal to 18', async () => { + const expected = new Node18UniversalStreamWrapper(new ReadableStream()); + RUNTIME.type = "node"; + RUNTIME.parsedVersion = 18; + + const result = await chooseStreamWrapper(new ReadableStream()); + + expect(JSON.stringify(result)).toBe(JSON.stringify(expected)); + }); + + it('should return a NodePre18StreamWrapper when RUNTIME.type is "node" and RUNTIME.parsedVersion is not null and RUNTIME.parsedVersion is less than 18', async () => { + const stream = await import("readable-stream"); + const expected = new NodePre18StreamWrapper(new stream.Readable()); + + RUNTIME.type = "node"; + RUNTIME.parsedVersion = 16; + + const result = await chooseStreamWrapper(new stream.Readable()); + + expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); + }); + + it('should return a Undici when RUNTIME.type is not "node"', async () => { + const expected = new UndiciStreamWrapper(new ReadableStream()); + RUNTIME.type = "browser"; + + const result = await chooseStreamWrapper(new ReadableStream()); + + expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/webpack.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/webpack.test.ts new file mode 100644 index 00000000000..2e8272918f5 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/stream-wrappers/webpack.test.ts @@ -0,0 +1,35 @@ +import webpack from "webpack"; + +describe("test env compatibility", () => { + test("webpack", () => { + return new Promise((resolve, reject) => { + webpack( + { + mode: "production", + entry: "./src/index.ts", + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + }, + }, + (err, stats) => { + try { + expect(err).toBe(null); + expect(stats?.hasErrors()).toBe(false); + resolve(); + } catch (error) { + reject(error); + } + } + ); + }); + }, 60_000); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..ab0df0285cd --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { enum_ } from "../../../../src/core/schemas/builders/enum"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..6906bf4cf91 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,57 @@ +import { Schema } from "../../../../src/core/schemas/Schema"; +import { lazy, list, object, string } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..8813cc9fbb4 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,18 @@ +import { lazyObject, number, object, string } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..424ed642db2 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,41 @@ +import { list, object, property, string } from "../../../../src/core/schemas/builders"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..fa6c88873c6 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..9f5dd0ed39b --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,57 @@ +import { object, property, string, stringLiteral } from "../../../../src/core/schemas/builders"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..54fc8c4ebf8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,89 @@ +import { boolean, object, property, string, stringLiteral } from "../../../../src/core/schemas/builders"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..0acf0e240f6 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,255 @@ +import { any, number, object, property, string, stringLiteral, unknown } from "../../../../src/core/schemas/builders"; +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..d87a65febfd --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,21 @@ +import { objectWithoutOptionalProperties, string, stringLiteral } from "../../../../src/core/schemas/builders"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..1adbbe2a838 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..897a8295dca --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { boolean } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..2d01415a60b --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { number } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..57b2368784a --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { string } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..4d17a7dbd00 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..7e4ba39cc55 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,34 @@ +import { number, record, string } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..da10086bc1d --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,83 @@ +import { object, string } from "../../../../src/core/schemas/builders"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("omitUndefined", () => { + it("serializes undefined as null", async () => { + const value = object({ + a: string().optional(), + b: string().optional(), + }).jsonOrThrow({ + a: "hello", + b: undefined, + }); + expect(value).toEqual({ a: "hello", b: null }); + }); + + it("omits undefined values", async () => { + const value = object({ + a: string().optional(), + b: string().optional(), + }).jsonOrThrow( + { + a: "hello", + b: undefined, + }, + { + omitUndefined: true, + } + ); + expect(value).toEqual({ a: "hello" }); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..e17f908c80e --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,48 @@ +import { set, string } from "../../../../src/core/schemas/builders"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..0e66433371c --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,44 @@ +import { number, object, property, string, undiscriminatedUnion } from "../../../../src/core/schemas/builders"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..790184603ac --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,113 @@ +import { boolean, discriminant, number, object, string, union } from "../../../../src/core/schemas/builders"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/mixed-file-directory/tsconfig.json b/seed/ts-sdk/mixed-file-directory/tsconfig.json new file mode 100644 index 00000000000..538c94fe015 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "extendedDiagnostics": true, + "strict": true, + "target": "ES6", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "baseUrl": "src" + }, + "include": ["src"], + "exclude": [] +} diff --git a/test-definitions/fern/apis/mixed-file-directory/definition/__package__.yml b/test-definitions/fern/apis/mixed-file-directory/definition/__package__.yml new file mode 100644 index 00000000000..c4224b55354 --- /dev/null +++ b/test-definitions/fern/apis/mixed-file-directory/definition/__package__.yml @@ -0,0 +1,2 @@ +types: + Id: string diff --git a/test-definitions/fern/apis/mixed-file-directory/definition/api.yml b/test-definitions/fern/apis/mixed-file-directory/definition/api.yml new file mode 100644 index 00000000000..7d680d624f8 --- /dev/null +++ b/test-definitions/fern/apis/mixed-file-directory/definition/api.yml @@ -0,0 +1 @@ +name: mixed-file-directory diff --git a/test-definitions/fern/apis/mixed-file-directory/definition/organization.yml b/test-definitions/fern/apis/mixed-file-directory/definition/organization.yml new file mode 100644 index 00000000000..6b1021dfd9c --- /dev/null +++ b/test-definitions/fern/apis/mixed-file-directory/definition/organization.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + user: user.yml + +types: + Organization: + properties: + id: root.Id + name: string + users: list + + CreateOrganizationRequest: + properties: + name: string + +service: + auth: false + base-path: /organizations + endpoints: + create: + path: / + method: POST + auth: false + docs: Create a new organization. + request: CreateOrganizationRequest + response: Organization diff --git a/test-definitions/fern/apis/mixed-file-directory/definition/user.yml b/test-definitions/fern/apis/mixed-file-directory/definition/user.yml new file mode 100644 index 00000000000..f6d372b45f4 --- /dev/null +++ b/test-definitions/fern/apis/mixed-file-directory/definition/user.yml @@ -0,0 +1,26 @@ +imports: + root: __package__.yml + +types: + User: + properties: + id: root.Id + name: string + age: integer + +service: + auth: false + base-path: /users + endpoints: + list: + path: / + method: GET + auth: false + docs: List all users. + request: + name: ListUsersRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/test-definitions/fern/apis/mixed-file-directory/definition/user/events.yml b/test-definitions/fern/apis/mixed-file-directory/definition/user/events.yml new file mode 100644 index 00000000000..e0d993ff09b --- /dev/null +++ b/test-definitions/fern/apis/mixed-file-directory/definition/user/events.yml @@ -0,0 +1,26 @@ +imports: + root: ../__package__.yml + user: ../user.yml + +types: + Event: + properties: + id: root.Id + name: string + +service: + auth: false + base-path: /users/events + endpoints: + listEvents: + path: / + method: GET + auth: false + docs: List all user events. + request: + name: ListUserEventsRequest + query-parameters: + limit: + type: optional + docs: The maximum number of results to return. + response: list diff --git a/test-definitions/fern/apis/mixed-file-directory/definition/user/events/metadata.yml b/test-definitions/fern/apis/mixed-file-directory/definition/user/events/metadata.yml new file mode 100644 index 00000000000..f38b5afcb12 --- /dev/null +++ b/test-definitions/fern/apis/mixed-file-directory/definition/user/events/metadata.yml @@ -0,0 +1,23 @@ +imports: + root: ../../__package__.yml + +types: + Metadata: + properties: + id: root.Id + value: unknown + +service: + auth: false + base-path: /users/events/metadata + endpoints: + getMetadata: + path: / + method: GET + auth: false + docs: Get event metadata. + request: + name: GetEventMetadataRequest + query-parameters: + id: root.Id + response: Metadata diff --git a/test-definitions/fern/apis/mixed-file-directory/generators.yml b/test-definitions/fern/apis/mixed-file-directory/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/test-definitions/fern/apis/mixed-file-directory/generators.yml @@ -0,0 +1 @@ +{}