diff --git a/generators/php/codegen/src/context/PhpAttributeMapper.ts b/generators/php/codegen/src/context/PhpAttributeMapper.ts index 16bd73dad2d..8f005ce02c7 100644 --- a/generators/php/codegen/src/context/PhpAttributeMapper.ts +++ b/generators/php/codegen/src/context/PhpAttributeMapper.ts @@ -74,13 +74,7 @@ export class PhpAttributeMapper { }); } - public getUnionTypeParameters({ - types, - isOptional = false - }: { - types: php.Type[]; - isOptional?: boolean; - }): php.AstNode[] { + public getUnionTypeParameters({ types, isOptional }: { types: php.Type[]; isOptional?: boolean }): php.AstNode[] { const typeAttributeArguments = types.map((type) => this.getTypeAttributeArgument(type)); // remove duplicates, such as "string" and "string" if enums and strings are both in the union return uniqWith([...typeAttributeArguments, ...(isOptional ? [php.codeblock("'null'")] : [])], isEqual); diff --git a/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts b/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts index 878f07c53be..92c54d43ee0 100644 --- a/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts +++ b/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts @@ -302,10 +302,7 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { arguments_: UnnamedArgument[]; types: php.Type[]; }): php.CodeBlock { - const unionTypeParameters = this.context.phpAttributeMapper.getUnionTypeParameters({ - types, - isOptional: false - }); + const unionTypeParameters = this.context.phpAttributeMapper.getUnionTypeParameters({ types }); // if deduping in getUnionTypeParameters results in one type, treat it like just that type if (unionTypeParameters.length === 1) { return this.decodeJsonResponse(types[0]); diff --git a/seed/csharp-model/server-sent-events/.gitignore b/seed/csharp-model/server-sent-events/.gitignore index 9965de29662..5e57f18055d 100644 --- a/seed/csharp-model/server-sent-events/.gitignore +++ b/seed/csharp-model/server-sent-events/.gitignore @@ -1,7 +1,10 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## Get latest from `dotnet new gitignore` + +# dotenv files +.env # User-specific files *.rsuser @@ -399,6 +402,7 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.idea ## ## Visual studio for Mac @@ -475,3 +479,6 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk + +# Vim temporary swap files +*.swp diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/SeedServerSentEvents.Test.csproj b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/SeedServerSentEvents.Test.csproj index 31335ba802f..7fa553ecb76 100644 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/SeedServerSentEvents.Test.csproj +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/SeedServerSentEvents.Test.csproj @@ -15,6 +15,8 @@ + + diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Completions/StreamedCompletion.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Completions/StreamedCompletion.cs index 108d0ed7e69..42b2117e3c2 100644 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Completions/StreamedCompletion.cs +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Completions/StreamedCompletion.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using SeedServerSentEvents.Core; #nullable enable @@ -11,4 +12,9 @@ public record StreamedCompletion [JsonPropertyName("tokens")] public int? Tokens { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } } diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/CollectionItemSerializer.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/CollectionItemSerializer.cs index 46753a77c5a..98010900521 100644 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/CollectionItemSerializer.cs +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/CollectionItemSerializer.cs @@ -10,7 +10,7 @@ namespace SeedServerSentEvents.Core; /// /// Type of item to convert. /// Converter to use for individual items. -public class CollectionItemSerializer +internal class CollectionItemSerializer : JsonConverter> where TConverterType : JsonConverter { diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/Constants.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/Constants.cs new file mode 100644 index 00000000000..44b6ee86fcc --- /dev/null +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/Constants.cs @@ -0,0 +1,7 @@ +namespace SeedServerSentEvents.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/server-sent-events/src/SeedServerSentEvents/Core/DateTimeSerializer.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/DateTimeSerializer.cs new file mode 100644 index 00000000000..2dd35f4ec96 --- /dev/null +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/DateTimeSerializer.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedServerSentEvents.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/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs new file mode 100644 index 00000000000..4e016a1bb3b --- /dev/null +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedServerSentEvents.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/server-sent-events/src/SeedServerSentEvents/Core/OneOfSerializer.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/OneOfSerializer.cs index fb6caa0f90f..71b9dbcc630 100644 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/OneOfSerializer.cs +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/OneOfSerializer.cs @@ -5,10 +5,9 @@ namespace SeedServerSentEvents.Core; -public class OneOfSerializer : JsonConverter - where TOneOf : IOneOf +internal class OneOfSerializer : JsonConverter { - public override TOneOf? Read( + public override IOneOf? Read( ref Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options @@ -17,14 +16,14 @@ JsonSerializerOptions options if (reader.TokenType is JsonTokenType.Null) return default; - foreach (var (type, cast) in s_types) + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) { try { var readerCopy = reader; var result = JsonSerializer.Deserialize(ref readerCopy, type, options); reader.Skip(); - return (TOneOf)cast.Invoke(null, [result])!; + return (IOneOf)cast.Invoke(null, [result])!; } catch (JsonException) { } } @@ -34,20 +33,18 @@ JsonSerializerOptions options ); } - private static readonly (System.Type type, MethodInfo cast)[] s_types = GetOneOfTypes(); - - public override void Write(Utf8JsonWriter writer, TOneOf value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, IOneOf value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value.Value, options); } - private static (System.Type type, MethodInfo cast)[] GetOneOfTypes() + private static (System.Type type, MethodInfo cast)[] GetOneOfTypes(System.Type typeToConvert) { - var casts = typeof(TOneOf) + var casts = typeToConvert .GetRuntimeMethods() .Where(m => m.IsSpecialName && m.Name == "op_Implicit") .ToArray(); - var type = typeof(TOneOf); + var type = typeToConvert; while (type != null) { if ( @@ -62,6 +59,11 @@ private static (System.Type type, MethodInfo cast)[] GetOneOfTypes() type = type.BaseType; } - throw new InvalidOperationException($"{typeof(TOneOf)} isn't OneOf or OneOfBase"); + 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/server-sent-events/src/SeedServerSentEvents/Core/Public/Version.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/Public/Version.cs new file mode 100644 index 00000000000..8296b59ade5 --- /dev/null +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/Public/Version.cs @@ -0,0 +1,6 @@ +namespace SeedServerSentEvents; + +internal class Version +{ + public const string Current = "0.0.1"; +} diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs index 143654892f7..707a72f31b4 100644 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs @@ -4,7 +4,7 @@ namespace SeedServerSentEvents.Core; -public class StringEnumSerializer : JsonConverter +internal class StringEnumSerializer : JsonConverter where TEnum : struct, System.Enum { private readonly Dictionary _enumToString = new(); diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/SeedServerSentEvents.csproj b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/SeedServerSentEvents.csproj index 11f735b98cc..dfaed0b7562 100644 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/SeedServerSentEvents.csproj +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/SeedServerSentEvents.csproj @@ -11,7 +11,7 @@ README.md https://github.com/server-sent-events/fern - + true @@ -41,5 +41,10 @@ + + + <_Parameter1>SeedServerSentEvents.Test + + diff --git a/seed/csharp-sdk/server-sent-events/README.md b/seed/csharp-sdk/server-sent-events/README.md new file mode 100644 index 00000000000..91c010b022f --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/README.md @@ -0,0 +1,87 @@ +# Seed C# Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FC%23) +[![nuget shield](https://img.shields.io/nuget/v/SeedServerSentEvents)](https://nuget.org/packages/SeedServerSentEvents) + +The Seed C# library provides convenient access to the Seed API from C#. + +## Installation + +```sh +nuget install SeedServerSentEvents +``` + +## Usage + +Instantiate and use the client with the following: + +```csharp +using SeedServerSentEvents; + +var client = new SeedServerSentEventsClient(); +await client.Completions.StreamAsync(new StreamCompletionRequest { Query = "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 SeedServerSentEvents; + +try { + var response = await client.Completions.StreamAsync(...); +} catch (SeedServerSentEventsApiException 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.Completions.StreamAsync( + ..., + 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.Completions.StreamAsync( + ..., + 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/server-sent-events/reference.md b/seed/csharp-sdk/server-sent-events/reference.md new file mode 100644 index 00000000000..76ccc7a0adf --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/reference.md @@ -0,0 +1,41 @@ +# Reference +## Completions +
client.Completions.StreamAsync(StreamCompletionRequest { ... }) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Completions.StreamAsync(new StreamCompletionRequest { Query = "string" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `StreamCompletionRequest` + +
+
+
+
+ + +
+
+
diff --git a/seed/csharp-sdk/server-sent-events/snippet-templates.json b/seed/csharp-sdk/server-sent-events/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/csharp-sdk/server-sent-events/snippet.json b/seed/csharp-sdk/server-sent-events/snippet.json new file mode 100644 index 00000000000..ad0e1d38051 --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/snippet.json @@ -0,0 +1,17 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": null, + "id": { + "path": "/stream", + "method": "POST", + "identifier_override": "endpoint_completions.stream" + }, + "snippet": { + "type": "typescript", + "client": "using SeedServerSentEvents;\n\nvar client = new SeedServerSentEventsClient();\nawait client.Completions.StreamAsync(new StreamCompletionRequest { Query = \"string\" });\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/RawClientTests.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/RawClientTests.cs new file mode 100644 index 00000000000..42f412c3b10 --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/RawClientTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Net.Http; +using FluentAssertions; +using NUnit.Framework; +using SeedServerSentEvents.Core; +using WireMock.Server; +using SystemTask = System.Threading.Tasks.Task; +using WireMockRequest = WireMock.RequestBuilders.Request; +using WireMockResponse = WireMock.ResponseBuilders.Response; + +namespace SeedServerSentEvents.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/server-sent-events/src/SeedServerSentEvents.Test/SeedServerSentEvents.Test.csproj b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/SeedServerSentEvents.Test.csproj index 5988c7f8c42..7fa553ecb76 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/SeedServerSentEvents.Test.csproj +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/SeedServerSentEvents.Test.csproj @@ -16,6 +16,7 @@ + diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Utils/JsonDiffChecker.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Utils/JsonDiffChecker.cs deleted file mode 100644 index 8542d7fd40c..00000000000 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Utils/JsonDiffChecker.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Newtonsoft.Json.Linq; -using NUnit.Framework; - -namespace SeedServerSentEvents.Test.Utils; - -public static class JsonDiffChecker -{ - public static void AssertJsonEquals(string jsonString1, string jsonString2) - { - var token1 = JToken.Parse(jsonString1); - var token2 = JToken.Parse(jsonString2); - var differences = GetJsonDifferences(token1, token2); - - Assert.That( - differences, - Is.Empty, - $"The JSON strings are not equal: {string.Join(", ", differences)}" - ); - } - - private static List GetJsonDifferences(JToken token1, JToken token2, string path = "") - { - var differences = new List(); - - if (token1.Type != token2.Type) - { - differences.Add($"{path} has different types: {token1.Type} vs {token2.Type}"); - return differences; - } - - if (token1 is JObject obj1 && token2 is JObject obj2) - { - foreach (var property in obj1.Properties()) - { - var newPath = string.IsNullOrEmpty(path) - ? property.Name - : $"{path}.{property.Name}"; - if (!obj2.TryGetValue(property.Name, out JToken token2Value)) - { - differences.Add($"{newPath} is missing in the second JSON"); - } - else - { - differences.AddRange(GetJsonDifferences(property.Value, token2Value, newPath)); - } - } - - foreach (var property in obj2.Properties()) - { - var newPath = string.IsNullOrEmpty(path) - ? property.Name - : $"{path}.{property.Name}"; - if (!obj1.TryGetValue(property.Name, out _)) - { - differences.Add($"{newPath} is missing in the first JSON"); - } - } - } - else if (token1 is JArray array1 && token2 is JArray array2) - { - for (var i = 0; i < Math.Max(array1.Count, array2.Count); i++) - { - var newPath = $"{path}[{i}]"; - if (i >= array1.Count) - { - differences.Add($"{newPath} is missing in the first JSON"); - } - else if (i >= array2.Count) - { - differences.Add($"{newPath} is missing in the second JSON"); - } - else - { - differences.AddRange(GetJsonDifferences(array1[i], array2[i], newPath)); - } - } - } - else if (!JToken.DeepEquals(token1, token2)) - { - differences.Add($"{path} has different values: {token1} vs {token2}"); - } - - return differences; - } -} diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Wire/BaseWireTest.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Wire/BaseWireTest.cs deleted file mode 100644 index 73cd5c96c11..00000000000 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Wire/BaseWireTest.cs +++ /dev/null @@ -1,36 +0,0 @@ -using NUnit.Framework; -using SeedServerSentEvents; -using SeedServerSentEvents.Core; -using WireMock.Logging; -using WireMock.Server; -using WireMock.Settings; - -#nullable enable - -namespace SeedServerSentEvents.Test.Wire; - -[SetUpFixture] -public class BaseWireTest -{ - protected static WireMockServer Server { get; set; } = null!; - - protected static SeedServerSentEventsClient Client { get; set; } = null!; - - [OneTimeSetUp] - public void GlobalSetup() - { - // Start the WireMock server - Server = WireMockServer.Start( - new WireMockServerSettings { Logger = new WireMockConsoleLogger() } - ); - - // Initialize the Client - Client = new SeedServerSentEventsClient(new ClientOptions { BaseUrl = Server.Urls[0] }); - } - - [OneTimeTearDown] - public void GlobalTeardown() - { - Server.Stop(); - } -} diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/CompletionsClient.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/CompletionsClient.cs index 67703a6bded..19c515299ed 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/CompletionsClient.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/CompletionsClient.cs @@ -1,21 +1,31 @@ using System.Net.Http; -using SeedServerSentEvents; +using System.Threading; +using System.Threading.Tasks; using SeedServerSentEvents.Core; #nullable enable namespace SeedServerSentEvents; -public class CompletionsClient +public partial class CompletionsClient { private RawClient _client; - public CompletionsClient(RawClient client) + internal CompletionsClient(RawClient client) { _client = client; } - public async Task StreamAsync(StreamCompletionRequest request, RequestOptions? options = null) + /// + /// + /// await client.Completions.StreamAsync(new StreamCompletionRequest { Query = "string" }); + /// + /// + public async Task StreamAsync( + StreamCompletionRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) { var response = await _client.MakeRequestAsync( new RawClient.JsonApiRequest @@ -24,14 +34,15 @@ public async Task StreamAsync(StreamCompletionRequest request, RequestOptions? o Method = HttpMethod.Post, Path = "stream", Body = request, - Options = options - } + Options = options, + }, + cancellationToken ); var responseBody = await response.Raw.Content.ReadAsStringAsync(); throw new SeedServerSentEventsApiException( $"Error with status code {response.StatusCode}", response.StatusCode, - JsonUtils.Deserialize(responseBody) + responseBody ); } } diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/Requests/StreamCompletionRequest.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/Requests/StreamCompletionRequest.cs index 44558dbe087..3d5461dd219 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/Requests/StreamCompletionRequest.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/Requests/StreamCompletionRequest.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using SeedServerSentEvents.Core; #nullable enable @@ -8,4 +9,9 @@ public record StreamCompletionRequest { [JsonPropertyName("query")] public required string Query { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } } diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/Types/StreamedCompletion.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/Types/StreamedCompletion.cs index 108d0ed7e69..42b2117e3c2 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/Types/StreamedCompletion.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Completions/Types/StreamedCompletion.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using SeedServerSentEvents.Core; #nullable enable @@ -11,4 +12,9 @@ public record StreamedCompletion [JsonPropertyName("tokens")] public int? Tokens { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } } diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/CollectionItemSerializer.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/CollectionItemSerializer.cs index 46753a77c5a..98010900521 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/CollectionItemSerializer.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/CollectionItemSerializer.cs @@ -10,7 +10,7 @@ namespace SeedServerSentEvents.Core; /// /// Type of item to convert. /// Converter to use for individual items. -public class CollectionItemSerializer +internal class CollectionItemSerializer : JsonConverter> where TConverterType : JsonConverter { diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Constants.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Constants.cs index 2b6193397eb..44b6ee86fcc 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Constants.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Constants.cs @@ -1,6 +1,7 @@ namespace SeedServerSentEvents.Core; -public static class Constants +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/server-sent-events/src/SeedServerSentEvents/Core/DateTimeSerializer.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/DateTimeSerializer.cs index 5c4e6d31960..2dd35f4ec96 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/DateTimeSerializer.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/DateTimeSerializer.cs @@ -4,7 +4,7 @@ namespace SeedServerSentEvents.Core; -public class DateTimeSerializer : JsonConverter +internal class DateTimeSerializer : JsonConverter { public override DateTime Read( ref Utf8JsonReader reader, diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Extensions.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Extensions.cs new file mode 100644 index 00000000000..0ecf7b52315 --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Extensions.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace SeedServerSentEvents.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/server-sent-events/src/SeedServerSentEvents/Core/HeaderValue.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/HeaderValue.cs new file mode 100644 index 00000000000..dfaba92fec5 --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/HeaderValue.cs @@ -0,0 +1,17 @@ +using OneOf; + +namespace SeedServerSentEvents.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/server-sent-events/src/SeedServerSentEvents/Core/Headers.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Headers.cs new file mode 100644 index 00000000000..9d919f4428e --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Headers.cs @@ -0,0 +1,17 @@ +namespace SeedServerSentEvents.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/server-sent-events/src/SeedServerSentEvents/Core/HttpMethodExtensions.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/HttpMethodExtensions.cs index b260d85c623..2ff0ab00192 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/HttpMethodExtensions.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/HttpMethodExtensions.cs @@ -2,7 +2,7 @@ namespace SeedServerSentEvents.Core; -public static class HttpMethodExtensions +internal static class HttpMethodExtensions { public static readonly HttpMethod Patch = new("PATCH"); } diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs index d33a62e34f7..4e016a1bb3b 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs @@ -3,7 +3,7 @@ namespace SeedServerSentEvents.Core; -public static class JsonOptions +internal static class JsonOptions { public static readonly JsonSerializerOptions JsonSerializerOptions; @@ -11,14 +11,14 @@ static JsonOptions() { JsonSerializerOptions = new JsonSerializerOptions { - Converters = { new DateTimeSerializer() }, + Converters = { new DateTimeSerializer(), new OneOfSerializer() }, WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; } } -public static class JsonUtils +internal static class JsonUtils { public static string Serialize(T obj) { diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/OneOfSerializer.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/OneOfSerializer.cs index fb6caa0f90f..71b9dbcc630 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/OneOfSerializer.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/OneOfSerializer.cs @@ -5,10 +5,9 @@ namespace SeedServerSentEvents.Core; -public class OneOfSerializer : JsonConverter - where TOneOf : IOneOf +internal class OneOfSerializer : JsonConverter { - public override TOneOf? Read( + public override IOneOf? Read( ref Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options @@ -17,14 +16,14 @@ JsonSerializerOptions options if (reader.TokenType is JsonTokenType.Null) return default; - foreach (var (type, cast) in s_types) + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) { try { var readerCopy = reader; var result = JsonSerializer.Deserialize(ref readerCopy, type, options); reader.Skip(); - return (TOneOf)cast.Invoke(null, [result])!; + return (IOneOf)cast.Invoke(null, [result])!; } catch (JsonException) { } } @@ -34,20 +33,18 @@ JsonSerializerOptions options ); } - private static readonly (System.Type type, MethodInfo cast)[] s_types = GetOneOfTypes(); - - public override void Write(Utf8JsonWriter writer, TOneOf value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, IOneOf value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value.Value, options); } - private static (System.Type type, MethodInfo cast)[] GetOneOfTypes() + private static (System.Type type, MethodInfo cast)[] GetOneOfTypes(System.Type typeToConvert) { - var casts = typeof(TOneOf) + var casts = typeToConvert .GetRuntimeMethods() .Where(m => m.IsSpecialName && m.Name == "op_Implicit") .ToArray(); - var type = typeof(TOneOf); + var type = typeToConvert; while (type != null) { if ( @@ -62,6 +59,11 @@ private static (System.Type type, MethodInfo cast)[] GetOneOfTypes() type = type.BaseType; } - throw new InvalidOperationException($"{typeof(TOneOf)} isn't OneOf or OneOfBase"); + 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/server-sent-events/src/SeedServerSentEvents/Core/ClientOptions.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/ClientOptions.cs similarity index 75% rename from seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/ClientOptions.cs rename to seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/ClientOptions.cs index 302ec64fd21..85fde994e61 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/ClientOptions.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/ClientOptions.cs @@ -1,9 +1,10 @@ using System; using System.Net.Http; +using SeedServerSentEvents.Core; #nullable enable -namespace SeedServerSentEvents.Core; +namespace SeedServerSentEvents; public partial class ClientOptions { @@ -26,4 +27,9 @@ public partial class ClientOptions /// 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/server-sent-events/src/SeedServerSentEvents/Core/RequestOptions.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/RequestOptions.cs similarity index 74% rename from seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/RequestOptions.cs rename to seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/RequestOptions.cs index c5907ea7c40..4d4bc1763d6 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/RequestOptions.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/RequestOptions.cs @@ -1,9 +1,10 @@ using System; using System.Net.Http; +using SeedServerSentEvents.Core; #nullable enable -namespace SeedServerSentEvents.Core; +namespace SeedServerSentEvents; public partial class RequestOptions { @@ -26,4 +27,9 @@ public partial class RequestOptions /// 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/server-sent-events/src/SeedServerSentEvents/Core/SeedServerSentEventsApiException.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/SeedServerSentEventsApiException.cs similarity index 66% rename from seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/SeedServerSentEventsApiException.cs rename to seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/SeedServerSentEventsApiException.cs index 54884e37904..3279f8f7911 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/SeedServerSentEventsApiException.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/SeedServerSentEventsApiException.cs @@ -1,8 +1,4 @@ -using SeedServerSentEvents.Core; - -#nullable enable - -namespace SeedServerSentEvents.Core; +namespace SeedServerSentEvents; /// /// This exception type will be thrown for any non-2XX API responses. @@ -19,9 +15,4 @@ public class SeedServerSentEventsApiException(string message, int statusCode, ob /// The body of the response that triggered the exception. /// public object Body => body; - - public override string ToString() - { - return $"SeedServerSentEventsApiException {{ message: {Message}, statusCode: {StatusCode}, body: {Body} }}"; - } } diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/SeedServerSentEventsException.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/SeedServerSentEventsException.cs similarity index 87% rename from seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/SeedServerSentEventsException.cs rename to seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/SeedServerSentEventsException.cs index 0b4fe99d37a..63d07005ada 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/SeedServerSentEventsException.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/SeedServerSentEventsException.cs @@ -2,7 +2,7 @@ #nullable enable -namespace SeedServerSentEvents.Core; +namespace SeedServerSentEvents; /// /// Base exception class for all exceptions thrown by the SDK. diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/Version.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/Version.cs new file mode 100644 index 00000000000..8296b59ade5 --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/Public/Version.cs @@ -0,0 +1,6 @@ +namespace SeedServerSentEvents; + +internal class Version +{ + public const string Current = "0.0.1"; +} diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/RawClient.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/RawClient.cs index 2c8bcc1a1cb..cbb8c0c992b 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/RawClient.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/RawClient.cs @@ -1,5 +1,6 @@ using System.Net.Http; using System.Text; +using System.Threading; namespace SeedServerSentEvents.Core; @@ -8,65 +9,28 @@ namespace SeedServerSentEvents.Core; /// /// Utility class for making raw HTTP requests to the API. /// -public class RawClient( - Dictionary headers, - Dictionary> headerSuppliers, - ClientOptions clientOptions -) +internal class RawClient(ClientOptions clientOptions) { - /// - /// The http client used to make requests. - /// - public readonly ClientOptions Options = clientOptions; + private const int InitialRetryDelayMs = 1000; + private const int MaxRetryDelayMs = 60000; /// - /// Global headers to be sent with every request. + /// The client options applied on every request. /// - private readonly Dictionary _headers = headers; + public readonly ClientOptions Options = clientOptions; - public async Task MakeRequestAsync(BaseApiRequest request) + public async Task MakeRequestAsync( + BaseApiRequest request, + CancellationToken cancellationToken = default + ) { - var url = BuildUrl(request); - var httpRequest = new HttpRequestMessage(request.Method, url); - if (request.ContentType != null) - { - request.Headers.Add("Content-Type", request.ContentType); - } - // Add global headers to the request - foreach (var header in _headers) - { - httpRequest.Headers.Add(header.Key, header.Value); - } - // Add global headers to the request from supplier - foreach (var header in headerSuppliers) - { - httpRequest.Headers.Add(header.Key, header.Value.Invoke()); - } - // Add request headers to the request - foreach (var header in request.Headers) - { - httpRequest.Headers.Add(header.Key, header.Value); - } - // Add the request body to the request - if (request is JsonApiRequest jsonRequest) - { - if (jsonRequest.Body != null) - { - httpRequest.Content = new StringContent( - JsonUtils.Serialize(jsonRequest.Body), - Encoding.UTF8, - "application/json" - ); - } - } - else if (request is StreamApiRequest { Body: not null } streamRequest) - { - httpRequest.Content = new StreamContent(streamRequest.Body); - } - // Send the request - var httpClient = request.Options?.HttpClient ?? Options.HttpClient; - var response = await httpClient.SendAsync(httpRequest); - return new ApiResponse { StatusCode = (int)response.StatusCode, Raw = response }; + // 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 @@ -81,7 +45,7 @@ public record BaseApiRequest public Dictionary Query { get; init; } = new(); - public Dictionary Headers { get; init; } = new(); + public Headers Headers { get; init; } = new(); public RequestOptions? Options { get; init; } } @@ -112,7 +76,67 @@ public record ApiResponse public required HttpResponseMessage Raw { get; init; } } - private string BuildUrl(BaseApiRequest request) + 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('/'); @@ -143,7 +167,19 @@ private string BuildUrl(BaseApiRequest request) return current; } ); - url = url.Substring(0, url.Length - 1); + 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/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs index 143654892f7..707a72f31b4 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs @@ -4,7 +4,7 @@ namespace SeedServerSentEvents.Core; -public class StringEnumSerializer : JsonConverter +internal class StringEnumSerializer : JsonConverter where TEnum : struct, System.Enum { private readonly Dictionary _enumToString = new(); diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEvents.csproj b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEvents.csproj index 11f735b98cc..dfaed0b7562 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEvents.csproj +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEvents.csproj @@ -11,7 +11,7 @@ README.md https://github.com/server-sent-events/fern - + true @@ -41,5 +41,10 @@ + + + <_Parameter1>SeedServerSentEvents.Test + + diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEventsClient.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEventsClient.cs index e91de74227f..09c8e604565 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEventsClient.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEventsClient.cs @@ -1,5 +1,3 @@ -using System; -using SeedServerSentEvents; using SeedServerSentEvents.Core; #nullable enable @@ -12,11 +10,24 @@ public partial class SeedServerSentEventsClient public SeedServerSentEventsClient(ClientOptions? clientOptions = null) { - _client = new RawClient( - new Dictionary() { { "X-Fern-Language", "C#" }, }, - new Dictionary>() { }, - clientOptions ?? new ClientOptions() + var defaultHeaders = new Headers( + new Dictionary() + { + { "X-Fern-Language", "C#" }, + { "X-Fern-SDK-Name", "SeedServerSentEvents" }, + { "X-Fern-SDK-Version", Version.Current }, + { "User-Agent", "Fernserver-sent-events/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); Completions = new CompletionsClient(_client); } diff --git a/seed/fastapi/seed.yml b/seed/fastapi/seed.yml index fa67bbbf2ac..8ed825b7ee4 100644 --- a/seed/fastapi/seed.yml +++ b/seed/fastapi/seed.yml @@ -67,6 +67,7 @@ allowedFailures: - websocket - enum - server-sent-events + - server-sent-event-examples - streaming-parameter - any-auth # Complex circular refs diff --git a/seed/java-model/server-sent-events/src/main/java/com/seed/serverSentEvents/model/completions/StreamedCompletion.java b/seed/java-model/server-sent-events/src/main/java/com/seed/serverSentEvents/model/completions/StreamedCompletion.java index 0a05329a4a7..b271add6f6f 100644 --- a/seed/java-model/server-sent-events/src/main/java/com/seed/serverSentEvents/model/completions/StreamedCompletion.java +++ b/seed/java-model/server-sent-events/src/main/java/com/seed/serverSentEvents/model/completions/StreamedCompletion.java @@ -91,7 +91,7 @@ public Builder from(StreamedCompletion other) { @java.lang.Override @JsonSetter("delta") public _FinalStage delta(String delta) { - this.delta = delta; + this.delta = Objects.requireNonNull(delta, "delta must not be null"); return this; } diff --git a/seed/java-sdk/server-sent-events/snippet-templates.json b/seed/java-sdk/server-sent-events/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-sdk/server-sent-events/snippet.json b/seed/java-sdk/server-sent-events/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/core/FileStream.java b/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/core/FileStream.java new file mode 100644 index 00000000000..cf1177b976d --- /dev/null +++ b/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/core/FileStream.java @@ -0,0 +1,60 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.serverSentEvents.core; + +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a file stream with associated metadata for file uploads. + */ +public class FileStream { + private final InputStream inputStream; + private final String fileName; + private final MediaType contentType; + + /** + * Constructs a FileStream with the given input stream and optional metadata. + * + * @param inputStream The input stream of the file content. Must not be null. + * @param fileName The name of the file, or null if unknown. + * @param contentType The MIME type of the file content, or null if unknown. + * @throws NullPointerException if inputStream is null + */ + public FileStream(InputStream inputStream, @Nullable String fileName, @Nullable MediaType contentType) { + this.inputStream = Objects.requireNonNull(inputStream, "Input stream cannot be null"); + this.fileName = fileName; + this.contentType = contentType; + } + + public FileStream(InputStream inputStream) { + this(inputStream, null, null); + } + + public InputStream getInputStream() { + return inputStream; + } + + @Nullable + public String getFileName() { + return fileName; + } + + @Nullable + public MediaType getContentType() { + return contentType; + } + + /** + * Creates a RequestBody suitable for use with OkHttp client. + * + * @return A RequestBody instance representing this file stream. + */ + public RequestBody toRequestBody() { + return new InputStreamRequestBody(contentType, inputStream); + } +} diff --git a/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/core/InputStreamRequestBody.java b/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/core/InputStreamRequestBody.java new file mode 100644 index 00000000000..a97c3290df6 --- /dev/null +++ b/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/core/InputStreamRequestBody.java @@ -0,0 +1,79 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.serverSentEvents.core; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.internal.Util; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; +import org.jetbrains.annotations.Nullable; + +/** + * A custom implementation of OkHttp's RequestBody that wraps an InputStream. + * This class allows streaming of data from an InputStream directly to an HTTP request body, + * which is useful for file uploads or sending large amounts of data without loading it all into memory. + */ +public class InputStreamRequestBody extends RequestBody { + private final InputStream inputStream; + private final MediaType contentType; + + /** + * Constructs an InputStreamRequestBody with the specified content type and input stream. + * + * @param contentType the MediaType of the content, or null if not known + * @param inputStream the InputStream containing the data to be sent + * @throws NullPointerException if inputStream is null + */ + public InputStreamRequestBody(@Nullable MediaType contentType, InputStream inputStream) { + this.contentType = contentType; + this.inputStream = Objects.requireNonNull(inputStream, "inputStream == null"); + } + + /** + * Returns the content type of this request body. + * + * @return the MediaType of the content, or null if not specified + */ + @Nullable + @Override + public MediaType contentType() { + return contentType; + } + + /** + * Returns the content length of this request body, if known. + * This method attempts to determine the length using the InputStream's available() method, + * which may not always accurately reflect the total length of the stream. + * + * @return the content length, or -1 if the length is unknown + * @throws IOException if an I/O error occurs + */ + @Override + public long contentLength() throws IOException { + return inputStream.available() == 0 ? -1 : inputStream.available(); + } + + /** + * Writes the content of the InputStream to the given BufferedSink. + * This method is responsible for transferring the data from the InputStream to the network request. + * + * @param sink the BufferedSink to write the content to + * @throws IOException if an I/O error occurs during writing + */ + @Override + public void writeTo(BufferedSink sink) throws IOException { + Source source = null; + try { + source = Okio.source(inputStream); + sink.writeAll(source); + } finally { + Util.closeQuietly(Objects.requireNonNull(source)); + } + } +} diff --git a/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/resources/completions/requests/StreamCompletionRequest.java b/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/resources/completions/requests/StreamCompletionRequest.java index d05a1da4881..a27c8e22024 100644 --- a/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/resources/completions/requests/StreamCompletionRequest.java +++ b/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/resources/completions/requests/StreamCompletionRequest.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = StreamCompletionRequest.Builder.class) @@ -62,7 +63,7 @@ public static QueryStage builder() { } public interface QueryStage { - _FinalStage query(String query); + _FinalStage query(@NotNull String query); Builder from(StreamCompletionRequest other); } @@ -88,8 +89,8 @@ public Builder from(StreamCompletionRequest other) { @java.lang.Override @JsonSetter("query") - public _FinalStage query(String query) { - this.query = query; + public _FinalStage query(@NotNull String query) { + this.query = Objects.requireNonNull(query, "query must not be null"); return this; } diff --git a/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/resources/completions/types/StreamedCompletion.java b/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/resources/completions/types/StreamedCompletion.java index 11e6253dcad..cf21a4b65e4 100644 --- a/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/resources/completions/types/StreamedCompletion.java +++ b/seed/java-sdk/server-sent-events/src/main/java/com/seed/serverSentEvents/resources/completions/types/StreamedCompletion.java @@ -16,6 +16,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = StreamedCompletion.Builder.class) @@ -72,7 +73,7 @@ public static DeltaStage builder() { } public interface DeltaStage { - _FinalStage delta(String delta); + _FinalStage delta(@NotNull String delta); Builder from(StreamedCompletion other); } @@ -105,8 +106,8 @@ public Builder from(StreamedCompletion other) { @java.lang.Override @JsonSetter("delta") - public _FinalStage delta(String delta) { - this.delta = delta; + public _FinalStage delta(@NotNull String delta) { + this.delta = Objects.requireNonNull(delta, "delta must not be null"); return this; } diff --git a/seed/java-spring/seed.yml b/seed/java-spring/seed.yml index 752c958045e..6ff96320949 100644 --- a/seed/java-spring/seed.yml +++ b/seed/java-spring/seed.yml @@ -46,5 +46,6 @@ allowedFailures: - response-property - streaming - server-sent-events + - server-sent-event-examples - alias-extends - any-auth diff --git a/seed/php-model/grpc-proto-exhaustive/src/Column.php b/seed/php-model/grpc-proto-exhaustive/src/Column.php index 2707a6bbb1a..574db314c45 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Column.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Column.php @@ -24,7 +24,7 @@ class Column extends JsonSerializableType /** * @var array|array|null $metadata */ - #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $metadata; /** diff --git a/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php b/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php index f23e5bf33cb..68edbdae71b 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php +++ b/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php @@ -30,7 +30,7 @@ class QueryColumn extends JsonSerializableType /** * @var array|array|null $filter */ - #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $filter; /** diff --git a/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php b/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php index 1d89380be18..6b6493d3c40 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php +++ b/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php @@ -30,7 +30,7 @@ class ScoredColumn extends JsonSerializableType /** * @var array|array|null $metadata */ - #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $metadata; /** diff --git a/seed/php-model/grpc-proto/src/UserModel.php b/seed/php-model/grpc-proto/src/UserModel.php index b28be2ae864..7072af6d470 100644 --- a/seed/php-model/grpc-proto/src/UserModel.php +++ b/seed/php-model/grpc-proto/src/UserModel.php @@ -35,7 +35,7 @@ class UserModel extends JsonSerializableType /** * @var array|array|null $metadata */ - #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $metadata; /** diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php index 4bc66433a79..ab476a3d1b7 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php @@ -30,7 +30,7 @@ class DeleteRequest extends JsonSerializableType /** * @var array|array|null $filter */ - #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $filter; /** diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php index 1a527e70876..7c3319bf6b7 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php @@ -11,7 +11,7 @@ class DescribeRequest extends JsonSerializableType /** * @var array|array|null $filter */ - #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $filter; /** diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php index e91a72ecdf8..bcbb33495c6 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php @@ -26,7 +26,7 @@ class QueryRequest extends JsonSerializableType /** * @var array|array|null $filter */ - #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $filter; /** diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php index 31fc8adcacc..a6ac2c80eaa 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php @@ -25,7 +25,7 @@ class UpdateRequest extends JsonSerializableType /** * @var array|array|null $setMetadata */ - #[JsonProperty('setMetadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('setMetadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $setMetadata; /** diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php index 956015cca43..6450e66da03 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php @@ -24,7 +24,7 @@ class Column extends JsonSerializableType /** * @var array|array|null $metadata */ - #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $metadata; /** diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php index 65b22409292..71a7e5b693b 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php @@ -30,7 +30,7 @@ class QueryColumn extends JsonSerializableType /** * @var array|array|null $filter */ - #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $filter; /** diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php index 1c276d7ab0b..768fd0ac7b7 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php @@ -30,7 +30,7 @@ class ScoredColumn extends JsonSerializableType /** * @var array|array|null $metadata */ - #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $metadata; /** diff --git a/seed/php-sdk/grpc-proto/src/Types/UserModel.php b/seed/php-sdk/grpc-proto/src/Types/UserModel.php index e893f5fff06..a666ae3e7a7 100644 --- a/seed/php-sdk/grpc-proto/src/Types/UserModel.php +++ b/seed/php-sdk/grpc-proto/src/Types/UserModel.php @@ -35,7 +35,7 @@ class UserModel extends JsonSerializableType /** * @var array|array|null $metadata */ - #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $metadata; /** diff --git a/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php b/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php index 6314f884cd1..15c1426fe36 100644 --- a/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php +++ b/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php @@ -35,7 +35,7 @@ class CreateRequest extends JsonSerializableType /** * @var array|array|null $metadata */ - #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'], 'null')] public array|null $metadata; /** diff --git a/seed/postman/server-sent-events/collection.json b/seed/postman/server-sent-events/collection.json index a0cea86dec9..857ab05350e 100644 --- a/seed/postman/server-sent-events/collection.json +++ b/seed/postman/server-sent-events/collection.json @@ -34,12 +34,18 @@ "query": [], "variable": [] }, - "header": [], + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], "method": "POST", "auth": null, "body": { "mode": "raw", - "raw": "{\n \"query\": \"example\"\n}", + "raw": "{\n \"query\": \"string\"\n}", "options": { "raw": { "language": "json" @@ -47,7 +53,48 @@ } } }, - "response": [] + "response": [ + { + "name": "Success", + "status": "OK", + "code": 200, + "originalRequest": { + "description": null, + "url": { + "raw": "{{baseUrl}}/stream", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "stream" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "type": "text", + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "POST", + "auth": null, + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"string\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "description": null, + "body": "[\n {\n \"delta\": \"string\",\n \"tokens\": 1\n },\n {\n \"delta\": \"string\",\n \"tokens\": 1\n }\n]", + "_postman_previewlanguage": "json" + } + ] } ] } diff --git a/seed/pydantic/server-sent-events/.github/workflows/ci.yml b/seed/pydantic/server-sent-events/.github/workflows/ci.yml index 17b9d4fa0e8..b204fa604e2 100644 --- a/seed/pydantic/server-sent-events/.github/workflows/ci.yml +++ b/seed/pydantic/server-sent-events/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: run: poetry install - name: Test - run: poetry run pytest ./tests/custom/ + run: poetry run pytest -rP . publish: needs: [compile, test] diff --git a/seed/pydantic/server-sent-events/pyproject.toml b/seed/pydantic/server-sent-events/pyproject.toml index 3ecc58493cc..e5b0d831e44 100644 --- a/seed/pydantic/server-sent-events/pyproject.toml +++ b/seed/pydantic/server-sent-events/pyproject.toml @@ -50,6 +50,9 @@ asyncio_mode = "auto" [tool.mypy] plugins = ["pydantic.mypy"] +[tool.ruff] +line-length = 120 + [build-system] requires = ["poetry-core"] diff --git a/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/__init__.py b/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/__init__.py index 85460274fba..9c7cd65aa25 100644 --- a/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/__init__.py +++ b/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/__init__.py @@ -5,7 +5,6 @@ IS_PYDANTIC_V2, UniversalBaseModel, UniversalRootModel, - deep_union_pydantic_dicts, parse_obj_as, universal_field_validator, universal_root_validator, @@ -18,7 +17,6 @@ "IS_PYDANTIC_V2", "UniversalBaseModel", "UniversalRootModel", - "deep_union_pydantic_dicts", "parse_obj_as", "serialize_datetime", "universal_field_validator", diff --git a/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/datetime_utils.py b/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/datetime_utils.py index 47344e9d9cc..7c9864a944c 100644 --- a/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/datetime_utils.py +++ b/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/datetime_utils.py @@ -13,9 +13,7 @@ def serialize_datetime(v: dt.datetime) -> str: """ def _serialize_zoned_datetime(v: dt.datetime) -> str: - if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname( - None - ): + 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: diff --git a/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/pydantic_utilities.py b/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/pydantic_utilities.py index 1a4cd514c6a..bbe1de41431 100644 --- a/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/pydantic_utilities.py +++ b/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/pydantic_utilities.py @@ -4,7 +4,8 @@ import datetime as dt import typing from collections import defaultdict -from functools import wraps + +import typing_extensions import pydantic @@ -54,19 +55,6 @@ Model = typing.TypeVar("Model", bound=pydantic.BaseModel) -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 - - def parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T: if IS_PYDANTIC_V2: adapter = pydantic.TypeAdapter(type_) # type: ignore # Pydantic v2 @@ -92,6 +80,8 @@ class Config: smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} + # Allow fields begining with `model_` to be used in the model + protected_namespaces = () def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = { @@ -105,44 +95,107 @@ def json(self, **kwargs: typing.Any) -> str: return super().json(**kwargs_with_defaults) def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - + """ + 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, + } return 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: - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) + _fields_set = self.__fields_set__.copy() + + 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 is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields: typing.Any = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + return super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + +def _union_list_of_pydantic_dicts( + source: typing.List[typing.Any], destination: typing.List[typing.Any] +) -> typing.List[typing.Any]: + converted_list: typing.List[typing.Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] # type: ignore + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +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(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination -UniversalRootModel: typing.Type[pydantic.BaseModel] if IS_PYDANTIC_V2: class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore # Pydantic v2 pass - UniversalRootModel = V2RootModel + UniversalRootModel: typing_extensions.TypeAlias = V2RootModel # type: ignore else: - UniversalRootModel = UniversalBaseModel + 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) + 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_,) @@ -153,11 +206,11 @@ def encode_by_type(o: typing.Any) -> typing.Any: return encoder(o) -def update_forward_refs(model: typing.Type["Model"]) -> None: +def update_forward_refs(model: typing.Type["Model"], **localns: typing.Any) -> None: if IS_PYDANTIC_V2: model.model_rebuild(raise_errors=False) # type: ignore # Pydantic v2 else: - model.update_forward_refs() + model.update_forward_refs(**localns) # Mirrors Pydantic's internal typing @@ -168,37 +221,45 @@ def universal_root_validator( pre: bool = False, ) -> typing.Callable[[AnyCallable], AnyCallable]: def decorator(func: AnyCallable) -> AnyCallable: - @wraps(func) - def validate(*args: typing.Any, **kwargs: typing.Any) -> AnyCallable: - if IS_PYDANTIC_V2: - wrapped_func = pydantic.model_validator("before" if pre else "after")( - func - ) # type: ignore # Pydantic v2 - else: - wrapped_func = pydantic.root_validator(pre=pre)(func) # type: ignore # Pydantic v1 + 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 wrapped_func(*args, **kwargs) + return decorator - return validate + +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 -def universal_field_validator( - field_name: str, pre: bool = False -) -> typing.Callable[[AnyCallable], AnyCallable]: - def decorator(func: AnyCallable) -> AnyCallable: - @wraps(func) - def validate(*args: typing.Any, **kwargs: typing.Any) -> AnyCallable: - if IS_PYDANTIC_V2: - wrapped_func = pydantic.field_validator( - field_name, mode="before" if pre else "after" - )(func) # type: ignore # Pydantic v2 - else: - wrapped_func = pydantic.validator(field_name, pre=pre)(func) +PydanticField = typing.Union[ModelField, pydantic.fields.FieldInfo] - return wrapped_func(*args, **kwargs) - return validate +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 - return decorator + +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/server-sent-events/src/seed/server_sent_events/core/serialization.py b/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/serialization.py index 5400ca0bc3b..cb5dcbf93a9 100644 --- a/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/serialization.py +++ b/seed/pydantic/server-sent-events/src/seed/server_sent_events/core/serialization.py @@ -1,10 +1,13 @@ # This file was auto-generated by Fern from our API Definition. import collections +import inspect import typing import typing_extensions +import pydantic + class FieldMetadata: """ @@ -29,6 +32,7 @@ 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 @@ -56,49 +60,77 @@ def convert_and_respect_annotation_metadata( inner_type = annotation clean_type = _remove_annotations(inner_type) - if typing_extensions.is_typeddict(clean_type) and isinstance( - object_, typing.Mapping + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) ): - return _convert_typeddict(object_, clean_type) + 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 ( - # If you're iterating on a string, do not bother to coerce it to a sequence. - (not isinstance(object_, str)) - and ( - ( - ( - 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.Set - or typing_extensions.get_origin(clean_type) == set - or clean_type == typing.Set - ) - and isinstance(object_, typing.Set) + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, ) - or ( - ( - typing_extensions.get_origin(clean_type) == typing.Sequence - or typing_extensions.get_origin(clean_type) - == collections.abc.Sequence - or clean_type == typing.Sequence + for key, value in object_.items() + } + + # 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, ) - and isinstance(object_, typing.Sequence) + 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 ) - ) - ): - inner_type = typing_extensions.get_args(clean_type)[0] - return [ - convert_and_respect_annotation_metadata( - object_=item, annotation=annotation, inner_type=inner_type + 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 ) - for item in object_ - ] + 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 @@ -107,7 +139,10 @@ def convert_and_respect_annotation_metadata( # 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 + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, ) return object_ @@ -120,18 +155,34 @@ def convert_and_respect_annotation_metadata( return object_ -def _convert_typeddict( - object_: typing.Mapping[str, object], expected_type: typing.Any +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(): - type_ = annotations.get(key) + 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_)] = ( - convert_and_respect_annotation_metadata(object_=value, annotation=type_) + 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 @@ -165,7 +216,39 @@ def _remove_annotations(type_: typing.Any) -> typing.Any: return type_ -def _alias_key(key: str, type_: typing.Any) -> str: +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: @@ -175,5 +258,15 @@ def _alias_key(key: str, type_: typing.Any) -> str: for annotation in annotations: if isinstance(annotation, FieldMetadata) and annotation.alias is not None: return annotation.alias + return None + - return key +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/server-sent-events/src/seed/server_sent_events/resources/completions/streamed_completion.py b/seed/pydantic/server-sent-events/src/seed/server_sent_events/resources/completions/streamed_completion.py index 2a6089db912..1d8a2c11dd3 100644 --- a/seed/pydantic/server-sent-events/src/seed/server_sent_events/resources/completions/streamed_completion.py +++ b/seed/pydantic/server-sent-events/src/seed/server_sent_events/resources/completions/streamed_completion.py @@ -11,9 +11,7 @@ class StreamedCompletion(UniversalBaseModel): tokens: typing.Optional[int] = None if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow" - ) # type: ignore # Pydantic v2 + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 else: class Config: diff --git a/seed/ruby-model/undiscriminated-unions/lib/requests.rb b/seed/ruby-model/undiscriminated-unions/lib/requests.rb deleted file mode 100644 index 3ebe1c4d180..00000000000 --- a/seed/ruby-model/undiscriminated-unions/lib/requests.rb +++ /dev/null @@ -1,132 +0,0 @@ -# frozen_string_literal: true - -require "faraday" -require "faraday/retry" -require "async/http/faraday" - -module SeedUndiscriminatedUnionsClient - 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 [SeedUndiscriminatedUnionsClient::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 [SeedUndiscriminatedUnionsClient::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": "seed_undiscriminated_unions_client" } - 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 [SeedUndiscriminatedUnionsClient::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 [SeedUndiscriminatedUnionsClient::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": "seed_undiscriminated_unions_client" } - 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 [SeedUndiscriminatedUnionsClient::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 [SeedUndiscriminatedUnionsClient::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-model/undiscriminated-unions/lib/seed_undiscriminated_unions_client.rb b/seed/ruby-model/undiscriminated-unions/lib/seed_undiscriminated_unions_client.rb index f409a52e7d3..e6fbf023528 100644 --- a/seed/ruby-model/undiscriminated-unions/lib/seed_undiscriminated_unions_client.rb +++ b/seed/ruby-model/undiscriminated-unions/lib/seed_undiscriminated_unions_client.rb @@ -1,43 +1,7 @@ # frozen_string_literal: true -require_relative "types_export" -require_relative "requests" -require_relative "seed_undiscriminated_unions_client/union/client" - -module SeedUndiscriminatedUnionsClient - class Client - # @return [SeedUndiscriminatedUnionsClient::UnionClient] - attr_reader :union - - # @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 [SeedUndiscriminatedUnionsClient::Client] - def initialize(base_url: nil, max_retries: nil, timeout_in_seconds: nil) - @request_client = SeedUndiscriminatedUnionsClient::RequestClient.new( - base_url: base_url, - max_retries: max_retries, - timeout_in_seconds: timeout_in_seconds - ) - @union = SeedUndiscriminatedUnionsClient::UnionClient.new(request_client: @request_client) - end - end - - class AsyncClient - # @return [SeedUndiscriminatedUnionsClient::AsyncUnionClient] - attr_reader :union - - # @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 [SeedUndiscriminatedUnionsClient::AsyncClient] - def initialize(base_url: nil, max_retries: nil, timeout_in_seconds: nil) - @async_request_client = SeedUndiscriminatedUnionsClient::AsyncRequestClient.new( - base_url: base_url, - max_retries: max_retries, - timeout_in_seconds: timeout_in_seconds - ) - @union = SeedUndiscriminatedUnionsClient::AsyncUnionClient.new(request_client: @async_request_client) - end - end -end +require_relative "seed_undiscriminated_unions_client/union/types/type_with_optional_union" +require_relative "seed_undiscriminated_unions_client/union/types/my_union" +require_relative "seed_undiscriminated_unions_client/union/types/key_type" +require_relative "seed_undiscriminated_unions_client/union/types/key" +require_relative "seed_undiscriminated_unions_client/union/types/metadata" diff --git a/seed/ruby-model/undiscriminated-unions/lib/seed_undiscriminated_unions_client/union/client.rb b/seed/ruby-model/undiscriminated-unions/lib/seed_undiscriminated_unions_client/union/client.rb deleted file mode 100644 index 615ab6c5d1e..00000000000 --- a/seed/ruby-model/undiscriminated-unions/lib/seed_undiscriminated_unions_client/union/client.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -require_relative "../../requests" -require_relative "types/my_union" -require_relative "types/metadata" -require "json" -require "async" - -module SeedUndiscriminatedUnionsClient - class UnionClient - # @return [SeedUndiscriminatedUnionsClient::RequestClient] - attr_reader :request_client - - # @param request_client [SeedUndiscriminatedUnionsClient::RequestClient] - # @return [SeedUndiscriminatedUnionsClient::UnionClient] - def initialize(request_client:) - @request_client = request_client - end - - # @param request [String, Array, Integer, Array, Array>, Set] - # @param request_options [SeedUndiscriminatedUnionsClient::RequestOptions] - # @return [String, Array, Integer, Array, Array>, Set] - # @example - # undiscriminated_unions = SeedUndiscriminatedUnionsClient::Client.new(base_url: "https://api.example.com") - # undiscriminated_unions.union.get(request: "string") - def get(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)}/" - end - SeedUndiscriminatedUnionsClient::Union::MyUnion.from_json(json_object: response.body) - end - - # @param request_options [SeedUndiscriminatedUnionsClient::RequestOptions] - # @return [SeedUndiscriminatedUnionsClient::Union::METADATA] - # @example - # undiscriminated_unions = SeedUndiscriminatedUnionsClient::Client.new(base_url: "https://api.example.com") - # undiscriminated_unions.union.get_metadata - def get_metadata(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 - 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)}/metadata" - end - JSON.parse(response.body) - end - end - - class AsyncUnionClient - # @return [SeedUndiscriminatedUnionsClient::AsyncRequestClient] - attr_reader :request_client - - # @param request_client [SeedUndiscriminatedUnionsClient::AsyncRequestClient] - # @return [SeedUndiscriminatedUnionsClient::AsyncUnionClient] - def initialize(request_client:) - @request_client = request_client - end - - # @param request [String, Array, Integer, Array, Array>, Set] - # @param request_options [SeedUndiscriminatedUnionsClient::RequestOptions] - # @return [String, Array, Integer, Array, Array>, Set] - # @example - # undiscriminated_unions = SeedUndiscriminatedUnionsClient::Client.new(base_url: "https://api.example.com") - # undiscriminated_unions.union.get(request: "string") - def get(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)}/" - end - SeedUndiscriminatedUnionsClient::Union::MyUnion.from_json(json_object: response.body) - end - end - - # @param request_options [SeedUndiscriminatedUnionsClient::RequestOptions] - # @return [SeedUndiscriminatedUnionsClient::Union::METADATA] - # @example - # undiscriminated_unions = SeedUndiscriminatedUnionsClient::Client.new(base_url: "https://api.example.com") - # undiscriminated_unions.union.get_metadata - def get_metadata(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 - 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)}/metadata" - end - parsed_json = JSON.parse(response.body) - parsed_json - end - end - end -end diff --git a/seed/ruby-model/undiscriminated-unions/lib/types_export.rb b/seed/ruby-model/undiscriminated-unions/lib/types_export.rb deleted file mode 100644 index e6fbf023528..00000000000 --- a/seed/ruby-model/undiscriminated-unions/lib/types_export.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require_relative "seed_undiscriminated_unions_client/union/types/type_with_optional_union" -require_relative "seed_undiscriminated_unions_client/union/types/my_union" -require_relative "seed_undiscriminated_unions_client/union/types/key_type" -require_relative "seed_undiscriminated_unions_client/union/types/key" -require_relative "seed_undiscriminated_unions_client/union/types/metadata" diff --git a/seed/ruby-model/undiscriminated-unions/seed_undiscriminated_unions_client.gemspec b/seed/ruby-model/undiscriminated-unions/seed_undiscriminated_unions_client.gemspec index 27bfc481523..62adc349d93 100644 --- a/seed/ruby-model/undiscriminated-unions/seed_undiscriminated_unions_client.gemspec +++ b/seed/ruby-model/undiscriminated-unions/seed_undiscriminated_unions_client.gemspec @@ -18,8 +18,4 @@ 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/ts-express/seed.yml b/seed/ts-express/seed.yml index 66f691f2f30..37e30c303bd 100644 --- a/seed/ts-express/seed.yml +++ b/seed/ts-express/seed.yml @@ -85,5 +85,6 @@ allowedFailures: - streaming - streaming-parameter - server-sent-events + - server-sent-event-examples diff --git a/seed/ts-sdk/grpc-proto/.mock/proto/google/api/field_behavior.proto b/seed/ts-sdk/grpc-proto/.mock/proto/google/api/field_behavior.proto deleted file mode 100644 index 128799c558d..00000000000 --- a/seed/ts-sdk/grpc-proto/.mock/proto/google/api/field_behavior.proto +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -import "google/protobuf/descriptor.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "FieldBehaviorProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.FieldOptions { - // A designation of a specific field behavior (required, output only, etc.) - // in protobuf messages. - // - // Examples: - // - // string name = 1 [(google.api.field_behavior) = REQUIRED]; - // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; - // google.protobuf.Duration ttl = 1 - // [(google.api.field_behavior) = INPUT_ONLY]; - // google.protobuf.Timestamp expire_time = 1 - // [(google.api.field_behavior) = OUTPUT_ONLY, - // (google.api.field_behavior) = IMMUTABLE]; - repeated google.api.FieldBehavior field_behavior = 1052; -} - -// An indicator of the behavior of a given field (for example, that a field -// is required in requests, or given as output but ignored as input). -// This **does not** change the behavior in protocol buffers itself; it only -// denotes the behavior and may affect how API tooling handles the field. -// -// Note: This enum **may** receive new values in the future. -enum FieldBehavior { - // Conventional default for enums. Do not use this. - FIELD_BEHAVIOR_UNSPECIFIED = 0; - - // Specifically denotes a field as optional. - // While all fields in protocol buffers are optional, this may be specified - // for emphasis if appropriate. - OPTIONAL = 1; - - // Denotes a field as required. - // This indicates that the field **must** be provided as part of the request, - // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). - REQUIRED = 2; - - // Denotes a field as output only. - // This indicates that the field is provided in responses, but including the - // field in a request does nothing (the server *must* ignore it and - // *must not* throw an error as a result of the field's presence). - OUTPUT_ONLY = 3; - - // Denotes a field as input only. - // This indicates that the field is provided in requests, and the - // corresponding field is not included in output. - INPUT_ONLY = 4; - - // Denotes a field as immutable. - // This indicates that the field may be set once in a request to create a - // resource, but may not be changed thereafter. - IMMUTABLE = 5; - - // Denotes that a (repeated) field is an unordered list. - // This indicates that the service may provide the elements of the list - // in any arbitrary order, rather than the order the user originally - // provided. Additionally, the list's order may or may not be stable. - UNORDERED_LIST = 6; - - // Denotes that this field returns a non-empty default value if not set. - // This indicates that if the user provides the empty value in a request, - // a non-empty value will be returned. The user will not be aware of what - // non-empty value to expect. - NON_EMPTY_DEFAULT = 7; - - // Denotes that the field in a resource (a message annotated with - // google.api.resource) is used in the resource name to uniquely identify the - // resource. For AIP-compliant APIs, this should only be applied to the - // `name` field on the resource. - // - // This behavior should not be applied to references to other resources within - // the message. - // - // The identifier field of resources often have different field behavior - // depending on the request it is embedded in (e.g. for Create methods name - // is optional and unused, while for Update methods it is required). Instead - // of method-specific annotations, only `IDENTIFIER` is required. - IDENTIFIER = 8; -} \ No newline at end of file diff --git a/seed/ts-sdk/grpc-proto/README.md b/seed/ts-sdk/grpc-proto/README.md index 742840c2b15..cd80361ab85 100644 --- a/seed/ts-sdk/grpc-proto/README.md +++ b/seed/ts-sdk/grpc-proto/README.md @@ -1,6 +1,6 @@ # 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) +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FTypeScript) [![npm shield](https://img.shields.io/npm/v/@fern/grpc-proto)](https://www.npmjs.com/package/@fern/grpc-proto) The Seed TypeScript library provides convenient access to the Seed API from TypeScript. @@ -11,6 +11,10 @@ The Seed TypeScript library provides convenient access to the Seed API from Type npm i -s @fern/grpc-proto ``` +## Reference + +A full reference for this library is available [here](./reference.md). + ## Usage Instantiate and use the client with the following: diff --git a/seed/ts-sdk/server-sent-events/README.md b/seed/ts-sdk/server-sent-events/README.md index e1128c89a07..acb70e95f5b 100644 --- a/seed/ts-sdk/server-sent-events/README.md +++ b/seed/ts-sdk/server-sent-events/README.md @@ -1,6 +1,6 @@ # 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) +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FTypeScript) [![npm shield](https://img.shields.io/npm/v/@fern/server-sent-events)](https://www.npmjs.com/package/@fern/server-sent-events) The Seed TypeScript library provides convenient access to the Seed API from TypeScript. @@ -11,6 +11,10 @@ The Seed TypeScript library provides convenient access to the Seed API from Type npm i -s @fern/server-sent-events ``` +## Reference + +A full reference for this library is available [here](./reference.md). + ## Usage Instantiate and use the client with the following: diff --git a/seed/ts-sdk/server-sent-events/jest.config.js b/seed/ts-sdk/server-sent-events/jest.config.js new file mode 100644 index 00000000000..35d6e65bf93 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", +}; diff --git a/seed/ts-sdk/server-sent-events/package.json b/seed/ts-sdk/server-sent-events/package.json index 3dc98b60480..1a0ddcd6a8f 100644 --- a/seed/ts-sdk/server-sent-events/package.json +++ b/seed/ts-sdk/server-sent-events/package.json @@ -16,13 +16,17 @@ "form-data": "^4.0.0", "formdata-node": "^6.0.3", "node-fetch": "2.7.0", - "qs": "6.11.2" + "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", diff --git a/seed/ts-sdk/server-sent-events/resolved-snippet-templates.md b/seed/ts-sdk/server-sent-events/resolved-snippet-templates.md new file mode 100644 index 00000000000..4422ea50f16 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/resolved-snippet-templates.md @@ -0,0 +1,11 @@ +```typescript +import { SeedServerSentEventsClient } from "@fern/server-sent-events"; + +const client = new SeedServerSentEventsClient({ environment: "YOUR_BASE_URL" }); +await client.completions.stream({ + query: "string", +}); + +``` + + diff --git a/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper.ts b/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper.ts index b8504841c77..4d7b7d52e8f 100644 --- a/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper.ts +++ b/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/Node18UniversalStreamWrapper.ts @@ -1,4 +1,4 @@ -import type { Writable } from "stream"; +import type { Writable } from "readable-stream"; import { EventCallback, StreamWrapper } from "./chooseStreamWrapper"; export class Node18UniversalStreamWrapper diff --git a/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/NodePre18StreamWrapper.ts b/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/NodePre18StreamWrapper.ts index f9bead21841..ba5f7276750 100644 --- a/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/NodePre18StreamWrapper.ts +++ b/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/NodePre18StreamWrapper.ts @@ -1,4 +1,4 @@ -import type { Readable, Writable } from "stream"; +import type { Readable, Writable } from "readable-stream"; import { EventCallback, StreamWrapper } from "./chooseStreamWrapper"; export class NodePre18StreamWrapper implements StreamWrapper { diff --git a/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/UndiciStreamWrapper.ts b/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/UndiciStreamWrapper.ts index 6725061ec27..263af00911f 100644 --- a/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/UndiciStreamWrapper.ts +++ b/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/UndiciStreamWrapper.ts @@ -78,7 +78,7 @@ export class UndiciStreamWrapper | WritableStream): void { + public unpipe(dest: UndiciStreamWrapper | WritableStream): void { this.off("data", (chunk) => { if (dest instanceof UndiciStreamWrapper) { dest._write(chunk); diff --git a/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/chooseStreamWrapper.ts b/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/chooseStreamWrapper.ts index d60991da089..2abd6b2ba1c 100644 --- a/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/chooseStreamWrapper.ts +++ b/seed/ts-sdk/server-sent-events/src/core/fetcher/stream-wrappers/chooseStreamWrapper.ts @@ -1,4 +1,4 @@ -import type { Readable } from "stream"; +import type { Readable } from "readable-stream"; import { RUNTIME } from "../../runtime"; export type EventCallback = (data?: any) => void; @@ -25,7 +25,7 @@ export async function chooseStreamWrapper(responseBody: any): Promise { } export const SchemaType = { + BIGINT: "bigint", DATE: "date", ENUM: "enum", LIST: "list", diff --git a/seed/ts-sdk/server-sent-events/src/core/schemas/builders/bigint/bigint.ts b/seed/ts-sdk/server-sent-events/src/core/schemas/builders/bigint/bigint.ts new file mode 100644 index 00000000000..dc9c742e007 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/src/core/schemas/builders/bigint/bigint.ts @@ -0,0 +1,50 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +export function bigint(): Schema { + const baseSchema: BaseSchema = { + parse: (raw, { breadcrumbsPrefix = [] } = {}) => { + if (typeof raw !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "string"), + }, + ], + }; + } + return { + ok: true, + value: BigInt(raw), + }; + }, + json: (bigint, { breadcrumbsPrefix = [] } = {}) => { + if (typeof bigint === "bigint") { + return { + ok: true, + value: bigint.toString(), + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(bigint, "bigint"), + }, + ], + }; + } + }, + getType: () => SchemaType.BIGINT, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/seed/ts-sdk/server-sent-events/src/core/schemas/builders/bigint/index.ts b/seed/ts-sdk/server-sent-events/src/core/schemas/builders/bigint/index.ts new file mode 100644 index 00000000000..e5843043fcb --- /dev/null +++ b/seed/ts-sdk/server-sent-events/src/core/schemas/builders/bigint/index.ts @@ -0,0 +1 @@ +export { bigint } from "./bigint"; diff --git a/seed/ts-sdk/server-sent-events/src/core/schemas/builders/index.ts b/seed/ts-sdk/server-sent-events/src/core/schemas/builders/index.ts index 050cd2c4efb..65211f92522 100644 --- a/seed/ts-sdk/server-sent-events/src/core/schemas/builders/index.ts +++ b/seed/ts-sdk/server-sent-events/src/core/schemas/builders/index.ts @@ -1,3 +1,4 @@ +export * from "./bigint"; export * from "./date"; export * from "./enum"; export * from "./lazy"; diff --git a/seed/ts-sdk/server-sent-events/src/core/schemas/utils/getErrorMessageForIncorrectType.ts b/seed/ts-sdk/server-sent-events/src/core/schemas/utils/getErrorMessageForIncorrectType.ts index 438012df418..1a5c31027ce 100644 --- a/seed/ts-sdk/server-sent-events/src/core/schemas/utils/getErrorMessageForIncorrectType.ts +++ b/seed/ts-sdk/server-sent-events/src/core/schemas/utils/getErrorMessageForIncorrectType.ts @@ -9,9 +9,13 @@ function getTypeAsString(value: unknown): string { if (value === null) { return "null"; } + if (value instanceof BigInt) { + return "BigInt"; + } switch (typeof value) { case "string": return `"${value}"`; + case "bigint": case "number": case "boolean": case "undefined": diff --git a/seed/ts-sdk/server-sent-events/src/version.ts b/seed/ts-sdk/server-sent-events/src/version.ts new file mode 100644 index 00000000000..b643a3e3ea2 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/src/version.ts @@ -0,0 +1 @@ +export const SDK_VERSION = "0.0.1"; diff --git a/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/Node18UniversalStreamWrapper.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/Node18UniversalStreamWrapper.test.ts index e307b1589a7..1dc9be0cc0e 100644 --- a/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/Node18UniversalStreamWrapper.test.ts +++ b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/Node18UniversalStreamWrapper.test.ts @@ -60,7 +60,7 @@ describe("Node18UniversalStreamWrapper", () => { }, }); const stream = new Node18UniversalStreamWrapper(rawStream); - const dest = new (await import("stream")).Writable({ + const dest = new (await import("readable-stream")).Writable({ write(chunk, encoding, callback) { expect(chunk.toString()).toEqual("test"); callback(); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/NodePre18StreamWrapper.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/NodePre18StreamWrapper.test.ts index 861142a08b0..0c99d3b2655 100644 --- a/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/NodePre18StreamWrapper.test.ts +++ b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/NodePre18StreamWrapper.test.ts @@ -2,7 +2,7 @@ import { NodePre18StreamWrapper } from "../../../../src/core/fetcher/stream-wrap describe("NodePre18StreamWrapper", () => { it("should set encoding to utf-8", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); const setEncodingSpy = jest.spyOn(stream, "setEncoding"); @@ -12,7 +12,7 @@ describe("NodePre18StreamWrapper", () => { }); it("should register an event listener for readable", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); const onSpy = jest.spyOn(stream, "on"); @@ -22,7 +22,7 @@ describe("NodePre18StreamWrapper", () => { }); it("should remove an event listener for data", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); const offSpy = jest.spyOn(stream, "off"); @@ -34,9 +34,9 @@ describe("NodePre18StreamWrapper", () => { }); it("should write to dest when calling pipe to node writable stream", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); - const dest = new (await import("stream")).Writable({ + const dest = new (await import("readable-stream")).Writable({ write(chunk, encoding, callback) { expect(chunk.toString()).toEqual("test"); callback(); @@ -47,10 +47,10 @@ describe("NodePre18StreamWrapper", () => { }); it("should write nothing when calling pipe and unpipe", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); const buffer: Uint8Array[] = []; - const dest = new (await import("stream")).Writable({ + const dest = new (await import("readable-stream")).Writable({ write(chunk, encoding, callback) { buffer.push(chunk); callback(); @@ -63,7 +63,7 @@ describe("NodePre18StreamWrapper", () => { }); it("should destroy the stream", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); const destroySpy = jest.spyOn(stream, "destroy"); @@ -73,7 +73,7 @@ describe("NodePre18StreamWrapper", () => { }); it("should pause the stream and resume", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); const pauseSpy = jest.spyOn(stream, "pause"); @@ -86,7 +86,7 @@ describe("NodePre18StreamWrapper", () => { }); it("should read the stream", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); expect(await stream.read()).toEqual("test"); @@ -94,7 +94,7 @@ describe("NodePre18StreamWrapper", () => { }); it("should read the stream as text", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); const stream = new NodePre18StreamWrapper(rawStream); const data = await stream.text(); @@ -103,7 +103,7 @@ describe("NodePre18StreamWrapper", () => { }); it("should read the stream as json", async () => { - const rawStream = (await import("stream")).Readable.from([JSON.stringify({ test: "test" })]); + const rawStream = (await import("readable-stream")).Readable.from([JSON.stringify({ test: "test" })]); const stream = new NodePre18StreamWrapper(rawStream); const data = await stream.json(); @@ -112,7 +112,7 @@ describe("NodePre18StreamWrapper", () => { }); it("should allow use with async iteratable stream", async () => { - const rawStream = (await import("stream")).Readable.from(["test", "test"]); + const rawStream = (await import("readable-stream")).Readable.from(["test", "test"]); let data = ""; const stream = new NodePre18StreamWrapper(rawStream); for await (const chunk of stream) { diff --git a/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/chooseStreamWrapper.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/chooseStreamWrapper.test.ts index aff7579e47a..17cf37a2f7f 100644 --- a/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/chooseStreamWrapper.test.ts +++ b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/chooseStreamWrapper.test.ts @@ -21,7 +21,7 @@ describe("chooseStreamWrapper", () => { }); 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("stream"); + const stream = await import("readable-stream"); const expected = new NodePre18StreamWrapper(new stream.Readable()); RUNTIME.type = "node"; diff --git a/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/webpack.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/webpack.test.ts new file mode 100644 index 00000000000..557db6dc4ef --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/stream-wrappers/webpack.test.ts @@ -0,0 +1,38 @@ +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); + if (stats?.hasErrors()) { + console.log(stats?.toString()); + } + expect(stats?.hasErrors()).toBe(false); + resolve(); + } catch (error) { + reject(error); + } + } + ); + }); + }, 60_000); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/bigint/bigint.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/bigint/bigint.test.ts new file mode 100644 index 00000000000..cf9935a749a --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/bigint/bigint.test.ts @@ -0,0 +1,24 @@ +import { bigint } from "../../../../src/core/schemas/builders/bigint"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("bigint", () => { + itSchema("converts between raw string and parsed bigint", bigint(), { + raw: "123456789012345678901234567890123456789012345678901234567890", + parsed: BigInt("123456789012345678901234567890123456789012345678901234567890"), + }); + + itValidateParse("non-string", bigint(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateJson("non-bigint", bigint(), "hello", [ + { + message: 'Expected bigint. Received "hello".', + path: [], + }, + ]); +});