diff --git a/.fern/metadata.json b/.fern/metadata.json index a31e8da1b..92156190e 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -15,5 +15,5 @@ "inline-path-parameters": true, "use-discriminated-unions": true }, - "sdkVersion": "44.0.0" + "sdkVersion": "44.1.0-rc.0" } \ No newline at end of file diff --git a/.fernignore b/.fernignore index 802860489..29aa5de4d 100644 --- a/.fernignore +++ b/.fernignore @@ -14,10 +14,12 @@ SquareWithLegacy.sln src/Square.sln src/Square/Square.Custom.props src/Square/WebhooksHelper.cs +src/Square/Reporting/ReportingHelper.cs src/Square/Core/Public/SquareApiException.cs src/Square.Test/WebhooksHelperTests.cs src/Square.Test/Unit/SquareErrorTest.cs src/Square.Test/Unit/WebhooksHelperTests.cs +src/Square.Test/Unit/ReportingHelperTests.cs src/Square.Test/Unit/MockServer/BatchDeleteTest.cs src/Square.Test/Integration src/Square.Test/Files diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4302c1055..a10fcafc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,7 @@ jobs: continue-on-error: true env: TEST_SQUARE_TOKEN: ${{ secrets.TEST_SQUARE_TOKEN }} + TEST_SQUARE_REPORTING: ${{ secrets.TEST_SQUARE_REPORTING }} - name: Publish to nuget.org if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') && steps.build.outcome == 'success' diff --git a/README.md b/README.md index 91efc0dda..66af432f9 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,75 @@ public static async Task CheckWebhooksEvent( In .NET 6 and above, there are also overloads using spans for allocation free webhook verification. +## Reporting API + +The [Square Reporting API](https://developer.squareup.com/docs/reporting-api/overview) (beta) +exposes a Cube-based analytics layer through two endpoints. Call `GetMetadataAsync` to discover +the queryable schema (cubes, measures, dimensions, and segments), then `LoadAsync` to run a query: + +```csharp +using Square; + +var client = new SquareClient(); + +// Discover what you can query. +var metadata = await client.Reporting.GetMetadataAsync(); + +// Run a query. +var response = await client.Reporting.LoadAsync( + new LoadRequest + { + Query = new Query { Measures = new List { "Orders.count" } }, + } +); +``` + +### Polling with `LoadAndWaitAsync` + +`/reporting/v1/load` is asynchronous: while a query is still being computed it returns an HTTP 200 +whose body is `{ "error": "Continue wait" }` instead of results. The expected behavior is to re-send +the identical request, with backoff, until results arrive. + +`LoadAndWaitAsync` owns that retry loop for you and returns the resolved `LoadResponse` — it never +hands back the "Continue wait" sentinel: + +```csharp +using Square; + +var response = await client.Reporting.LoadAndWaitAsync( + new LoadRequest + { + Query = new Query { Measures = new List { "Orders.count" } }, + } +); + +// The resolved payload is exposed on the flat `Data` property. +var data = response.Data; +// ... +``` + +The backoff is configurable, and the loop honors a `CancellationToken`: + +```csharp +var response = await client.Reporting.LoadAndWaitAsync( + request, + new LoadAndWaitOptions + { + MaxAttempts = 20, // give up after this many polls + InitialDelay = TimeSpan.FromSeconds(2), // delay before the first retry + MaxDelay = TimeSpan.FromSeconds(20), // upper bound on the backoff + BackoffFactor = 2, // multiplier applied after each attempt + }, + cancellationToken +); +``` + +If the query does not resolve within `MaxAttempts`, a `SquareException` is thrown. The same logic is +also available as a static helper, `ReportingHelper.LoadAndWaitAsync(client.Reporting, request, ...)`. + +> **Note:** The Reporting API is served only from production (`connect.squareup.com/reporting`); it is +> not available in the sandbox environment. + ## Legacy SDK While the new SDK has a lot of improvements, we at Square understand that it takes time to upgrade when there are breaking changes. diff --git a/reference.md b/reference.md index fc43fb964..2f57cbe28 100644 --- a/reference.md +++ b/reference.md @@ -12050,6 +12050,100 @@ await client.Vendors.UpdateAsync( + + + + +## Reporting +
client.Reporting.GetMetadataAsync() -> MetadataResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Reporting.GetMetadataAsync(); +``` +
+
+
+
+ + +
+
+
+ +
client.Reporting.LoadAsync(LoadRequest { ... }) -> LoadResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Reporting.LoadAsync(new LoadRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `LoadRequest` + +
+
+
+
+ +
diff --git a/src/Square.Test/Integration/ReportingTests.cs b/src/Square.Test/Integration/ReportingTests.cs new file mode 100644 index 000000000..7af4feee3 --- /dev/null +++ b/src/Square.Test/Integration/ReportingTests.cs @@ -0,0 +1,121 @@ +using System.Text.Json; +using NUnit.Framework; +using Square; + +namespace Square.Test.Integration; + +// The Reporting API is a beta, bespoke offering served ONLY from production +// (connect.squareup.com/reporting) — it is not routed on sandbox (returns 404 there). +// Validating it live therefore needs a production, reporting-provisioned access token, +// supplied via TEST_SQUARE_REPORTING (distinct from the sandbox TEST_SQUARE_TOKEN used by +// the rest of the integration suite). This suite is gated behind TEST_SQUARE_REPORTING and +// skips by default — keeping CI green. The endpoints are read-only (schema discovery + +// queries). The polling *logic* is covered without a live account in +// Unit/ReportingHelperTests.cs. +// +// Run it against a real prod account: +// TEST_SQUARE_REPORTING= \ +// dotnet test --filter FullyQualifiedName~Integration.ReportingTests +// # override the host with TEST_SQUARE_BASE_URL= if reporting moves. +[TestFixture] +public class ReportingTests +{ + private SquareClient _client = null!; + + [SetUp] + public void SetUp() + { + var token = Environment.GetEnvironmentVariable("TEST_SQUARE_REPORTING"); + if (string.IsNullOrEmpty(token)) + { + Assert.Ignore( + "Set TEST_SQUARE_REPORTING to a production, reporting-provisioned access token to run the reporting integration suite." + ); + } + // Reporting only exists on production; allow overriding the host via TEST_SQUARE_BASE_URL. + var baseUrl = + Environment.GetEnvironmentVariable("TEST_SQUARE_BASE_URL") ?? SquareEnvironment.Production; + TestContext.Out.WriteLine( + $"[reporting] base URL: {baseUrl} -> {baseUrl}/reporting/v1/{{meta,load}}" + ); + _client = new SquareClient(token!, new ClientOptions { BaseUrl = baseUrl }); + } + + // Resolves the first queryable measure from the live schema, e.g. "Orders.count". + private async Task FirstMeasureNameAsync() + { + var metadata = await _client.Reporting.GetMetadataAsync(); + var measure = metadata.Cubes?.FirstOrDefault()?.Measures.FirstOrDefault()?.Name; + if (string.IsNullOrEmpty(measure)) + { + throw new Exception( + "No cubes/measures are available on the reporting schema for this account." + ); + } + return measure!; + } + + [Test] + public async Task GetMetadataReturnsTheQueryableSchema() + { + var metadata = await _client.Reporting.GetMetadataAsync(); + + Assert.That(metadata.Cubes, Is.Not.Null); + Assert.That(metadata.Cubes, Is.Not.Empty); + + // Surface the live schema so a developer can see what is queryable. + var summary = (metadata.Cubes ?? new List()) + .Take(5) + .Select(cube => new + { + cube = cube.Name, + measures = cube.Measures.Take(5).Select(measure => measure.Name).ToList(), + }); + TestContext.Out.WriteLine( + "Reporting schema (first 5 cubes): " + + JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true }) + ); + } + + [Test] + public async Task LoadReturnsResultsOrTheContinueWaitSentinel() + { + var measure = await FirstMeasureNameAsync(); + var response = await _client.Reporting.LoadAsync( + new LoadRequest { Query = new Query { Measures = new List { measure } } } + ); + + if (response.AdditionalProperties.TryGetValue("error", out var sentinel)) + { + // Documented async behavior: a still-processing query comes back as HTTP 200 + // with { "error": "Continue wait" } instead of results. + Assert.That(sentinel.GetString(), Is.EqualTo("Continue wait")); + } + else + { + // Flat LoadResponse: a resolved query carries its payload on `Data`. + Assert.That(response.Data, Is.Not.Null); + } + } + + [Test] + public async Task LoadAndWaitResolvesAQueryEndToEndWithoutSurfacingContinueWait() + { + var measure = await FirstMeasureNameAsync(); + + var response = await _client.Reporting.LoadAndWaitAsync( + new LoadRequest { Query = new Query { Measures = new List { measure } } }, + new LoadAndWaitOptions + { + MaxAttempts = 20, + InitialDelay = TimeSpan.FromSeconds(2), + MaxDelay = TimeSpan.FromSeconds(20), + } + ); + + // The polling helper must never hand back the raw "Continue wait" sentinel. + Assert.That(response.AdditionalProperties.ContainsKey("error"), Is.False); + // Flat LoadResponse: the resolved query carries its payload on `Data`. + Assert.That(response.Data, Is.Not.Null); + } +} diff --git a/src/Square.Test/Unit/MockServer/GetMetadataTest.cs b/src/Square.Test/Unit/MockServer/GetMetadataTest.cs new file mode 100644 index 000000000..5327c86f2 --- /dev/null +++ b/src/Square.Test/Unit/MockServer/GetMetadataTest.cs @@ -0,0 +1,96 @@ +using NUnit.Framework; +using Square; +using Square.Core; + +namespace Square.Test.Unit.MockServer; + +[TestFixture] +public class GetMetadataTest : BaseMockServerTest +{ + [NUnit.Framework.Test] + public async Task MockServerTest() + { + const string mockResponse = """ + { + "cubes": [ + { + "name": "name", + "title": "title", + "type": "cube", + "meta": { + "key": "value" + }, + "description": "description", + "measures": [ + { + "name": "name", + "type": "type" + } + ], + "dimensions": [ + { + "name": "name", + "type": "type" + } + ], + "segments": [ + { + "name": "name", + "title": "title", + "shortTitle": "shortTitle" + } + ], + "joins": [ + { + "name": "name", + "relationship": "relationship" + } + ], + "folders": [ + { + "name": "name", + "members": [ + "members" + ] + } + ], + "nestedFolders": [ + { + "name": "name", + "members": [ + "members" + ] + } + ], + "hierarchies": [ + { + "name": "name", + "levels": [ + "levels" + ] + } + ] + } + ], + "compilerId": "compilerId" + } + """; + + Server + .Given( + WireMock.RequestBuilders.Request.Create().WithPath("/reporting/v1/meta").UsingGet() + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Reporting.GetMetadataAsync(); + Assert.That( + response, + Is.EqualTo(JsonUtils.Deserialize(mockResponse)).UsingDefaults() + ); + } +} diff --git a/src/Square.Test/Unit/MockServer/LoadTest.cs b/src/Square.Test/Unit/MockServer/LoadTest.cs new file mode 100644 index 000000000..747b576cf --- /dev/null +++ b/src/Square.Test/Unit/MockServer/LoadTest.cs @@ -0,0 +1,80 @@ +using NUnit.Framework; +using Square; +using Square.Core; + +namespace Square.Test.Unit.MockServer; + +[TestFixture] +public class LoadTest : BaseMockServerTest +{ + [NUnit.Framework.Test] + public async Task MockServerTest() + { + const string requestJson = """ + {} + """; + + const string mockResponse = """ + { + "dataSource": "dataSource", + "annotation": { + "measures": { + "key": "value" + }, + "dimensions": { + "key": "value" + }, + "segments": { + "key": "value" + }, + "timeDimensions": { + "key": "value" + } + }, + "data": [ + { + "key": "value" + } + ], + "lastRefreshTime": "lastRefreshTime", + "query": { + "key": "value" + }, + "slowQuery": true, + "external": true, + "dbType": "dbType", + "refreshKeyValues": [ + { + "key": "value" + } + ], + "pivotQuery": { + "key": "value" + }, + "queryType": "queryType" + } + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/reporting/v1/load") + .WithHeader("Content-Type", "application/json") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Reporting.LoadAsync(new LoadRequest()); + Assert.That( + response, + Is.EqualTo(JsonUtils.Deserialize(mockResponse)).UsingDefaults() + ); + } +} diff --git a/src/Square.Test/Unit/ReportingHelperTests.cs b/src/Square.Test/Unit/ReportingHelperTests.cs new file mode 100644 index 000000000..310991820 --- /dev/null +++ b/src/Square.Test/Unit/ReportingHelperTests.cs @@ -0,0 +1,169 @@ +using NUnit.Framework; +using Square; +using Square.Core; + +namespace Square.Test.Unit; + +/// +/// The Reporting API answers a still-processing /v1/load query with an HTTP 200 +/// whose body is { "error": "Continue wait" }. +/// owns the retry loop around that sentinel. These tests exercise that loop without a +/// network by scripting Reporting.LoadAsync, plus one test that proves the sentinel +/// actually survives the generated client's deserialization. +/// +[TestFixture] +public class ReportingHelperTests +{ + private const string ContinueWait = "Continue wait"; + + /// An stub whose LoadAsync returns a scripted sequence. + private sealed class FakeReportingClient : IReportingClient + { + private readonly IReadOnlyList _sequence; + private int _i; + + public FakeReportingClient(params LoadResponse[] sequence) => _sequence = sequence; + + public int CallCount => _i; + + public Task LoadAsync( + LoadRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var response = _sequence[Math.Min(_i, _sequence.Count - 1)]; + _i++; + return Task.FromResult(response); + } + + public Task GetMetadataAsync( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) => throw new NotSupportedException(); + } + + private static LoadResponse ContinueWaitResponse() => + JsonUtils.Deserialize($"{{\"error\":\"{ContinueWait}\"}}")!; + + // A resolved response is simply any LoadResponse lacking the "Continue wait" sentinel. + private static LoadResponse ResolvedResponse() => new(); + + [Test] + public async Task PollsPastContinueWaitAndReturnsResolvedResult() + { + var resolved = ResolvedResponse(); + var fake = new FakeReportingClient( + ContinueWaitResponse(), + ContinueWaitResponse(), + resolved + ); + + var response = await ReportingHelper.LoadAndWaitAsync( + fake, + new LoadRequest(), + new LoadAndWaitOptions + { + InitialDelay = TimeSpan.FromMilliseconds(1), + MaxDelay = TimeSpan.FromMilliseconds(1), + MaxAttempts = 5, + } + ); + + Assert.Multiple(() => + { + // The helper must poll through the sentinels and hand back the resolved response. + Assert.That(response, Is.SameAs(resolved)); + Assert.That(response.AdditionalProperties.ContainsKey("error"), Is.False); + Assert.That(fake.CallCount, Is.EqualTo(3)); + }); + } + + [Test] + public async Task ReturnsImmediatelyWhenFirstResponseHasResults() + { + var resolved = ResolvedResponse(); + var fake = new FakeReportingClient(resolved); + + var response = await ReportingHelper.LoadAndWaitAsync( + fake, + options: new LoadAndWaitOptions { InitialDelay = TimeSpan.FromMilliseconds(1) } + ); + + Assert.Multiple(() => + { + Assert.That(response, Is.SameAs(resolved)); + Assert.That(fake.CallCount, Is.EqualTo(1)); + }); + } + + [Test] + public void ThrowsOnceMaxAttemptsIsExhaustedWhileStillContinueWait() + { + var fake = new FakeReportingClient(ContinueWaitResponse()); // never resolves + + var ex = Assert.ThrowsAsync( + async () => + await ReportingHelper.LoadAndWaitAsync( + fake, + new LoadRequest(), + new LoadAndWaitOptions + { + InitialDelay = TimeSpan.FromMilliseconds(1), + MaxDelay = TimeSpan.FromMilliseconds(1), + MaxAttempts = 3, + } + ) + ); + + Assert.Multiple(() => + { + Assert.That(ex!.Message, Does.Contain("did not complete after 3 attempts")); + Assert.That(fake.CallCount, Is.EqualTo(3)); + }); + } + + [Test] + public void RejectsPromptlyWhenCancellationTokenFires() + { + var fake = new FakeReportingClient(ContinueWaitResponse()); // would otherwise poll forever + using var cts = new CancellationTokenSource(); + + var pending = ReportingHelper.LoadAndWaitAsync( + fake, + new LoadRequest(), + new LoadAndWaitOptions + { + InitialDelay = TimeSpan.FromSeconds(30), + MaxAttempts = 10, + }, + cts.Token + ); + cts.Cancel(); + + // Task.Delay surfaces a TaskCanceledException, which derives from + // OperationCanceledException — match the base type, not the exact type. + Assert.That( + async () => await pending, + Throws.InstanceOf() + ); + } + + [Test] + public void RealDeserializerContinueWaitBodyIsARetrySignalNotAResult() + { + // The crux of the design: the generated LoadResponse deserializes with unknown-key + // passthrough (via [JsonExtensionData]), so the `error` sentinel survives onto + // AdditionalProperties while the flat `data` payload stays absent. If this ever stops + // being true, LoadAndWaitAsync would mistake "Continue wait" for a real result. + var parsed = JsonUtils.Deserialize($"{{\"error\":\"{ContinueWait}\"}}")!; + + Assert.Multiple(() => + { + Assert.That(parsed.AdditionalProperties.TryGetValue("error", out var error), Is.True); + Assert.That(error.GetString(), Is.EqualTo(ContinueWait)); + Assert.That(parsed.Data, Is.Null); + }); + } +} diff --git a/src/Square/Core/Public/Version.cs b/src/Square/Core/Public/Version.cs index 9fe804267..80664b5bd 100644 --- a/src/Square/Core/Public/Version.cs +++ b/src/Square/Core/Public/Version.cs @@ -3,5 +3,5 @@ namespace Square; [Serializable] internal class Version { - public const string Current = "44.0.0"; + public const string Current = "44.1.0-rc.0"; } diff --git a/src/Square/ISquareClient.cs b/src/Square/ISquareClient.cs index b165c01c9..1f865272c 100644 --- a/src/Square/ISquareClient.cs +++ b/src/Square/ISquareClient.cs @@ -38,6 +38,7 @@ public partial interface ISquareClient public TerminalClient Terminal { get; } public TransferOrdersClient TransferOrders { get; } public VendorsClient Vendors { get; } + public ReportingClient Reporting { get; } public CashDrawersClient CashDrawers { get; } public WebhooksClient Webhooks { get; } } diff --git a/src/Square/Reporting/IReportingClient.cs b/src/Square/Reporting/IReportingClient.cs new file mode 100644 index 000000000..8410e2083 --- /dev/null +++ b/src/Square/Reporting/IReportingClient.cs @@ -0,0 +1,21 @@ +namespace Square; + +public partial interface IReportingClient +{ + /// + /// Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`. + /// + Task GetMetadataAsync( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + /// + /// Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready. + /// + Task LoadAsync( + LoadRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ); +} diff --git a/src/Square/Reporting/ReportingClient.cs b/src/Square/Reporting/ReportingClient.cs new file mode 100644 index 000000000..35df698f9 --- /dev/null +++ b/src/Square/Reporting/ReportingClient.cs @@ -0,0 +1,109 @@ +using System.Text.Json; +using Square.Core; + +namespace Square; + +public partial class ReportingClient : IReportingClient +{ + private RawClient _client; + + internal ReportingClient(RawClient client) + { + _client = client; + } + + /// + /// Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`. + /// + /// + /// await client.Reporting.GetMetadataAsync(); + /// + public async Task GetMetadataAsync( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var response = await _client + .SendRequestAsync( + new JsonRequest + { + BaseUrl = _client.Options.BaseUrl, + Method = HttpMethod.Get, + Path = "reporting/v1/meta", + Options = options, + }, + cancellationToken + ) + .ConfigureAwait(false); + if (response.StatusCode is >= 200 and < 400) + { + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + try + { + return JsonUtils.Deserialize(responseBody)!; + } + catch (JsonException e) + { + throw new SquareException("Failed to deserialize response", e); + } + } + + { + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + throw new SquareApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } + } + + /// + /// Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready. + /// + /// + /// await client.Reporting.LoadAsync(new LoadRequest()); + /// + public async Task LoadAsync( + LoadRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var response = await _client + .SendRequestAsync( + new JsonRequest + { + BaseUrl = _client.Options.BaseUrl, + Method = HttpMethod.Post, + Path = "reporting/v1/load", + Body = request, + ContentType = "application/json", + Options = options, + }, + cancellationToken + ) + .ConfigureAwait(false); + if (response.StatusCode is >= 200 and < 400) + { + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + try + { + return JsonUtils.Deserialize(responseBody)!; + } + catch (JsonException e) + { + throw new SquareException("Failed to deserialize response", e); + } + } + + { + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + throw new SquareApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } + } +} diff --git a/src/Square/Reporting/ReportingHelper.cs b/src/Square/Reporting/ReportingHelper.cs new file mode 100644 index 000000000..62298e0a4 --- /dev/null +++ b/src/Square/Reporting/ReportingHelper.cs @@ -0,0 +1,161 @@ +using System.Text.Json; + +namespace Square; + +/// +/// Polling configuration for . +/// +public class LoadAndWaitOptions +{ + /// Maximum poll attempts before giving up. Default 20. + public int MaxAttempts { get; set; } = 20; + + /// Delay before the first retry. Default 2 seconds. + public TimeSpan InitialDelay { get; set; } = TimeSpan.FromSeconds(2); + + /// Upper bound on the backoff delay. Default 20 seconds. + public TimeSpan MaxDelay { get; set; } = TimeSpan.FromSeconds(20); + + /// Multiplier applied to the delay after each attempt. Default 2. + public double BackoffFactor { get; set; } = 2; + + /// Forwarded to each underlying Reporting.LoadAsync call. + public RequestOptions? RequestOptions { get; set; } +} + +/// +/// Utility code to help with the +/// Square Reporting API. +/// +/// +/// The /reporting/v1/load endpoint is asynchronous: a query that is still being +/// computed comes back as an HTTP 200 whose body is { "error": "Continue wait" } +/// rather than the results. Clients are expected to re-send the identical request, with +/// backoff, until real results arrive. This helper owns that retry loop. +/// +/// +public static class ReportingHelper +{ + /// + /// Sentinel returned by the Reporting API on an HTTP 200 while a /v1/load + /// query is still processing. It is NOT an error — the request should be retried. + /// + private const string ContinueWait = "Continue wait"; + + /// + /// Runs a reporting query and transparently polls until it resolves, returning the + /// final . Re-sends the identical request with exponential + /// backoff while the API answers "Continue wait". + /// + /// A configured reporting client, e.g. client.Reporting. + /// The reporting query (same shape as Reporting.LoadAsync). + /// Polling/backoff configuration. + /// Aborts the poll loop (and any in-flight wait) when signalled. + /// The resolved (never the "Continue wait" sentinel). + /// + /// Thrown if the query does not resolve within . + /// + /// + /// Thrown if is signalled while polling. + /// + /// + /// + /// + /// var response = await ReportingHelper.LoadAndWaitAsync( + /// client.Reporting, + /// new LoadRequest { Query = new Query { Measures = new List<string> { "Orders.count" } } } + /// ); + /// + /// + /// + public static async Task LoadAndWaitAsync( + IReportingClient reporting, + LoadRequest? request = null, + LoadAndWaitOptions? options = null, + CancellationToken cancellationToken = default + ) + { + if (reporting is null) + { + throw new ArgumentNullException(nameof(reporting)); + } + + request ??= new LoadRequest(); + options ??= new LoadAndWaitOptions(); + if (options.MaxAttempts < 1) + { + throw new ArgumentOutOfRangeException( + nameof(options), + "MaxAttempts must be at least 1." + ); + } + + var delay = options.InitialDelay; + for (var attempt = 1; attempt <= options.MaxAttempts; attempt++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var response = await reporting + .LoadAsync(request, options.RequestOptions, cancellationToken) + .ConfigureAwait(false); + + if (!IsContinueWait(response)) + { + return response; + } + + if (attempt == options.MaxAttempts) + { + break; + } + + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + delay = TimeSpan.FromTicks( + Math.Min( + (long)(delay.Ticks * options.BackoffFactor), + options.MaxDelay.Ticks + ) + ); + } + + throw new SquareException( + $"Reporting query did not complete after {options.MaxAttempts} attempts (\"{ContinueWait}\")." + ); + } + + /// + /// A "Continue wait" body deserializes into a (validation is + /// skipped) with the error field preserved on + /// and results absent. That's the signal to retry. + /// + private static bool IsContinueWait(LoadResponse response) + { + return response.AdditionalProperties.TryGetValue("error", out var error) + && error.ValueKind == JsonValueKind.String + && error.GetString() == ContinueWait; + } +} + +public partial class ReportingClient +{ + /// + /// Runs a reporting query and transparently polls until it resolves, returning the + /// final . Convenience wrapper over + /// — re-sends the identical request with + /// exponential backoff while the Reporting API answers "Continue wait". + /// + /// The reporting query (same shape as ). + /// Polling/backoff configuration. + /// Aborts the poll loop (and any in-flight wait) when signalled. + /// The resolved (never the "Continue wait" sentinel). + /// + /// var response = await client.Reporting.LoadAndWaitAsync( + /// new LoadRequest { Query = new Query { Measures = new List<string> { "Orders.count" } } } + /// ); + /// + public Task LoadAndWaitAsync( + LoadRequest? request = null, + LoadAndWaitOptions? options = null, + CancellationToken cancellationToken = default + ) => ReportingHelper.LoadAndWaitAsync(this, request, options, cancellationToken); +} diff --git a/src/Square/Reporting/Requests/LoadRequest.cs b/src/Square/Reporting/Requests/LoadRequest.cs new file mode 100644 index 000000000..811a20221 --- /dev/null +++ b/src/Square/Reporting/Requests/LoadRequest.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record LoadRequest +{ + [JsonPropertyName("queryType")] + public string? QueryType { get; set; } + + [JsonPropertyName("cache")] + public CacheMode? Cache { get; set; } + + [JsonPropertyName("query")] + public Query? Query { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Square.Custom.props b/src/Square/Square.Custom.props index 2a798cef0..c08fa69e2 100644 --- a/src/Square/Square.Custom.props +++ b/src/Square/Square.Custom.props @@ -21,6 +21,14 @@ true true snupkg + + $(Version.Substring(0, $(Version.IndexOf('-')))) + $(Version.Substring(0, $(Version.IndexOf('-')))) diff --git a/src/Square/Square.csproj b/src/Square/Square.csproj index 7a215fd0b..73def9285 100644 --- a/src/Square/Square.csproj +++ b/src/Square/Square.csproj @@ -5,7 +5,7 @@ enable 12 enable - 44.0.0 + 44.1.0-rc.0 $(Version) $(Version) README.md diff --git a/src/Square/SquareClient.cs b/src/Square/SquareClient.cs index ce190f7e3..5e3d40f64 100644 --- a/src/Square/SquareClient.cs +++ b/src/Square/SquareClient.cs @@ -22,7 +22,7 @@ public SquareClient(string? token = null, ClientOptions? clientOptions = null) { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "Square" }, { "X-Fern-SDK-Version", Version.Current }, - { "User-Agent", "Square/44.0.0" }, + { "User-Agent", "Square/44.1.0-rc.0" }, } ); clientOptions ??= new ClientOptions(); @@ -71,6 +71,7 @@ public SquareClient(string? token = null, ClientOptions? clientOptions = null) Terminal = new TerminalClient(_client); TransferOrders = new TransferOrdersClient(_client); Vendors = new VendorsClient(_client); + Reporting = new ReportingClient(_client); CashDrawers = new CashDrawersClient(_client); Webhooks = new WebhooksClient(_client); } @@ -141,6 +142,8 @@ public SquareClient(string? token = null, ClientOptions? clientOptions = null) public VendorsClient Vendors { get; } + public ReportingClient Reporting { get; } + public CashDrawersClient CashDrawers { get; } public WebhooksClient Webhooks { get; } diff --git a/src/Square/Types/CacheMode.cs b/src/Square/Types/CacheMode.cs new file mode 100644 index 000000000..8a681cd45 --- /dev/null +++ b/src/Square/Types/CacheMode.cs @@ -0,0 +1,71 @@ +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct CacheMode : IStringEnum +{ + public static readonly CacheMode StaleIfSlow = new(Values.StaleIfSlow); + + public static readonly CacheMode StaleWhileRevalidate = new(Values.StaleWhileRevalidate); + + public static readonly CacheMode MustRevalidate = new(Values.MustRevalidate); + + public static readonly CacheMode NoCache = new(Values.NoCache); + + public CacheMode(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static CacheMode FromCustom(string value) + { + return new CacheMode(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(CacheMode value1, string value2) => value1.Value.Equals(value2); + + public static bool operator !=(CacheMode value1, string value2) => !value1.Value.Equals(value2); + + public static explicit operator string(CacheMode value) => value.Value; + + public static explicit operator CacheMode(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string StaleIfSlow = "stale-if-slow"; + + public const string StaleWhileRevalidate = "stale-while-revalidate"; + + public const string MustRevalidate = "must-revalidate"; + + public const string NoCache = "no-cache"; + } +} diff --git a/src/Square/Types/Cube.cs b/src/Square/Types/Cube.cs new file mode 100644 index 000000000..ad54366c9 --- /dev/null +++ b/src/Square/Types/Cube.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record Cube : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("type")] + public required CubeType Type { get; set; } + + [JsonPropertyName("meta")] + public Dictionary? Meta { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("measures")] + public IEnumerable Measures { get; set; } = new List(); + + [JsonPropertyName("dimensions")] + public IEnumerable Dimensions { get; set; } = new List(); + + [JsonPropertyName("segments")] + public IEnumerable Segments { get; set; } = new List(); + + [JsonPropertyName("joins")] + public IEnumerable? Joins { get; set; } + + [JsonPropertyName("folders")] + public IEnumerable? Folders { get; set; } + + [JsonPropertyName("nestedFolders")] + public IEnumerable? NestedFolders { get; set; } + + [JsonPropertyName("hierarchies")] + public IEnumerable? Hierarchies { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/CubeJoin.cs b/src/Square/Types/CubeJoin.cs new file mode 100644 index 000000000..af5eb7055 --- /dev/null +++ b/src/Square/Types/CubeJoin.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record CubeJoin : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("relationship")] + public required string Relationship { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/CubeType.cs b/src/Square/Types/CubeType.cs new file mode 100644 index 000000000..3fdc0b6c1 --- /dev/null +++ b/src/Square/Types/CubeType.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct CubeType : IStringEnum +{ + public static readonly CubeType Cube = new(Values.Cube); + + public static readonly CubeType View = new(Values.View); + + public CubeType(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static CubeType FromCustom(string value) + { + return new CubeType(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(CubeType value1, string value2) => value1.Value.Equals(value2); + + public static bool operator !=(CubeType value1, string value2) => !value1.Value.Equals(value2); + + public static explicit operator string(CubeType value) => value.Value; + + public static explicit operator CubeType(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string Cube = "cube"; + + public const string View = "view"; + } +} diff --git a/src/Square/Types/CustomNumericFormat.cs b/src/Square/Types/CustomNumericFormat.cs new file mode 100644 index 000000000..d24fe8617 --- /dev/null +++ b/src/Square/Types/CustomNumericFormat.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +/// +/// Custom numeric format for numeric measures and dimensions +/// +[Serializable] +public record CustomNumericFormat : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// Type of the format (must be 'custom-numeric') + /// + [JsonPropertyName("type")] + public string Type { get; set; } = "custom-numeric"; + + /// + /// d3-format specifier string (e.g., '.2f', ',.0f', '$,.2f', '.0%', '.2s'). See https://d3js.org/d3-format + /// + [JsonPropertyName("value")] + public required string Value { get; set; } + + /// + /// Name of the predefined format (e.g., 'percent_2', 'currency_1'). Present only when a named format was used. + /// + [JsonPropertyName("alias")] + public string? Alias { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/CustomTimeFormat.cs b/src/Square/Types/CustomTimeFormat.cs new file mode 100644 index 000000000..3def42b11 --- /dev/null +++ b/src/Square/Types/CustomTimeFormat.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +/// +/// Custom time format for time dimensions +/// +[Serializable] +public record CustomTimeFormat : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// Type of the format (must be 'custom-time') + /// + [JsonPropertyName("type")] + public string Type { get; set; } = "custom-time"; + + /// + /// POSIX strftime format string (IEEE Std 1003.1 / POSIX.1) with d3-time-format extensions (e.g., '%Y-%m-%d', '%d/%m/%Y %H:%M:%S'). See https://pubs.opengroup.org/onlinepubs/009695399/functions/strptime.html and https://d3js.org/d3-time-format + /// + [JsonPropertyName("value")] + public required string Value { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/Dimension.cs b/src/Square/Types/Dimension.cs new file mode 100644 index 000000000..abe4c3d30 --- /dev/null +++ b/src/Square/Types/Dimension.cs @@ -0,0 +1,79 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; +using Square.Core; + +namespace Square; + +[Serializable] +public record Dimension : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("shortTitle")] + public string? ShortTitle { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("type")] + public required string Type { get; set; } + + /// + /// When dimension is defined in View, it keeps the original path: Cube.dimension + /// + [JsonPropertyName("aliasMember")] + public string? AliasMember { get; set; } + + [JsonPropertyName("granularities")] + public IEnumerable? Granularities { get; set; } + + [JsonPropertyName("meta")] + public Dictionary? Meta { get; set; } + + [JsonPropertyName("format")] + public OneOf< + SimpleFormat, + LinkFormat, + CustomTimeFormat, + CustomNumericFormat + >? Format { get; set; } + + [JsonPropertyName("formatDescription")] + public FormatDescription? FormatDescription { get; set; } + + /// + /// ISO 4217 currency code in uppercase (3 characters, e.g. USD, EUR) + /// + [JsonPropertyName("currency")] + public string? Currency { get; set; } + + [JsonPropertyName("order")] + public DimensionOrder? Order { get; set; } + + /// + /// Key reference for the dimension + /// + [JsonPropertyName("key")] + public string? Key { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/DimensionGranularity.cs b/src/Square/Types/DimensionGranularity.cs new file mode 100644 index 000000000..81b77af90 --- /dev/null +++ b/src/Square/Types/DimensionGranularity.cs @@ -0,0 +1,43 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record DimensionGranularity : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("interval")] + public string? Interval { get; set; } + + [JsonPropertyName("sql")] + public string? Sql { get; set; } + + [JsonPropertyName("offset")] + public string? Offset { get; set; } + + [JsonPropertyName("origin")] + public string? Origin { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/DimensionOrder.cs b/src/Square/Types/DimensionOrder.cs new file mode 100644 index 000000000..7bc0731de --- /dev/null +++ b/src/Square/Types/DimensionOrder.cs @@ -0,0 +1,65 @@ +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct DimensionOrder : IStringEnum +{ + public static readonly DimensionOrder Asc = new(Values.Asc); + + public static readonly DimensionOrder Desc = new(Values.Desc); + + public DimensionOrder(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static DimensionOrder FromCustom(string value) + { + return new DimensionOrder(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(DimensionOrder value1, string value2) => + value1.Value.Equals(value2); + + public static bool operator !=(DimensionOrder value1, string value2) => + !value1.Value.Equals(value2); + + public static explicit operator string(DimensionOrder value) => value.Value; + + public static explicit operator DimensionOrder(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string Asc = "asc"; + + public const string Desc = "desc"; + } +} diff --git a/src/Square/Types/Folder.cs b/src/Square/Types/Folder.cs new file mode 100644 index 000000000..4558def06 --- /dev/null +++ b/src/Square/Types/Folder.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record Folder : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("members")] + public IEnumerable Members { get; set; } = new List(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/FormatDescription.cs b/src/Square/Types/FormatDescription.cs new file mode 100644 index 000000000..07f92e006 --- /dev/null +++ b/src/Square/Types/FormatDescription.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +/// +/// Resolved format description with the predefined name and d3-format specifier +/// +[Serializable] +public record FormatDescription : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// Predefined format name (e.g., 'percent_2', 'currency_1') or a base name like 'number', or 'custom' for ad-hoc specifiers + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + /// d3-format specifier string (e.g., '.2f', ',.0f', '$,.2f'). See https://d3js.org/d3-format + /// + [JsonPropertyName("specifier")] + public required string Specifier { get; set; } + + /// + /// ISO 4217 currency code in uppercase (e.g. USD, EUR). Present when a currency format is used. + /// + [JsonPropertyName("currency")] + public string? Currency { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/Hierarchy.cs b/src/Square/Types/Hierarchy.cs new file mode 100644 index 000000000..a2d41385f --- /dev/null +++ b/src/Square/Types/Hierarchy.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record Hierarchy : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + /// When hierarchy is defined in Cube, it keeps the original path: Cube.hierarchy + /// + [JsonPropertyName("aliasMember")] + public string? AliasMember { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("levels")] + public IEnumerable Levels { get; set; } = new List(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/JoinSubquery.cs b/src/Square/Types/JoinSubquery.cs new file mode 100644 index 000000000..2c28d5f03 --- /dev/null +++ b/src/Square/Types/JoinSubquery.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record JoinSubquery : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("sql")] + public required string Sql { get; set; } + + [JsonPropertyName("on")] + public required string On { get; set; } + + [JsonPropertyName("joinType")] + public required string JoinType { get; set; } + + [JsonPropertyName("alias")] + public required string Alias { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/LinkFormat.cs b/src/Square/Types/LinkFormat.cs new file mode 100644 index 000000000..6511254b6 --- /dev/null +++ b/src/Square/Types/LinkFormat.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +/// +/// Link format with label and type +/// +[Serializable] +public record LinkFormat : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// Label for the link + /// + [JsonPropertyName("label")] + public required string Label { get; set; } + + /// + /// Type of the format (must be 'link') + /// + [JsonPropertyName("type")] + public string Type { get; set; } = "link"; + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/LoadResponse.cs b/src/Square/Types/LoadResponse.cs new file mode 100644 index 000000000..6929afa83 --- /dev/null +++ b/src/Square/Types/LoadResponse.cs @@ -0,0 +1,63 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; +using Square.Core; + +namespace Square; + +[Serializable] +public record LoadResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("dataSource")] + public string? DataSource { get; set; } + + [JsonPropertyName("annotation")] + public LoadResultAnnotation? Annotation { get; set; } + + [JsonPropertyName("data")] + public OneOf< + IEnumerable>, + LoadResultDataCompact, + LoadResultDataColumnar + >? Data { get; set; } + + [JsonPropertyName("lastRefreshTime")] + public string? LastRefreshTime { get; set; } + + [JsonPropertyName("query")] + public Dictionary? Query { get; set; } + + [JsonPropertyName("slowQuery")] + public bool? SlowQuery { get; set; } + + [JsonPropertyName("external")] + public bool? External { get; set; } + + [JsonPropertyName("dbType")] + public string? DbType { get; set; } + + [JsonPropertyName("refreshKeyValues")] + public IEnumerable>? RefreshKeyValues { get; set; } + + [JsonPropertyName("pivotQuery")] + public Dictionary? PivotQuery { get; set; } + + [JsonPropertyName("queryType")] + public string? QueryType { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/LoadResultAnnotation.cs b/src/Square/Types/LoadResultAnnotation.cs new file mode 100644 index 000000000..5f8f95351 --- /dev/null +++ b/src/Square/Types/LoadResultAnnotation.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record LoadResultAnnotation : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("measures")] + public Dictionary Measures { get; set; } = new Dictionary(); + + [JsonPropertyName("dimensions")] + public Dictionary Dimensions { get; set; } = new Dictionary(); + + [JsonPropertyName("segments")] + public Dictionary Segments { get; set; } = new Dictionary(); + + [JsonPropertyName("timeDimensions")] + public Dictionary TimeDimensions { get; set; } = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/LoadResultDataColumnar.cs b/src/Square/Types/LoadResultDataColumnar.cs new file mode 100644 index 000000000..467ef3b67 --- /dev/null +++ b/src/Square/Types/LoadResultDataColumnar.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +/// +/// Columnar data format - members list paired with one primitive array per column. Returned when `responseFormat=columnar` is requested. +/// +[Serializable] +public record LoadResultDataColumnar : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// Ordered list of member names. Element `i` of `columns` holds the values for `members[i]` across all rows. + /// + [JsonPropertyName("members")] + public IEnumerable Members { get; set; } = new List(); + + /// + /// One array per member, in the same order as `members`. Each inner array contains the primitive value of that member for every row (null, boolean, number, string). + /// + [JsonPropertyName("columns")] + public IEnumerable> Columns { get; set; } = new List>(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/LoadResultDataCompact.cs b/src/Square/Types/LoadResultDataCompact.cs new file mode 100644 index 000000000..ffc1b5b92 --- /dev/null +++ b/src/Square/Types/LoadResultDataCompact.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +/// +/// Compact data format - a single object with the members list and a dataset of primitive arrays. Returned when `responseFormat=compact` is requested. +/// +[Serializable] +public record LoadResultDataCompact : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// Ordered list of member names that correspond to each cell position in `dataset` rows. + /// + [JsonPropertyName("members")] + public IEnumerable Members { get; set; } = new List(); + + /// + /// Array of rows, where each row is an array of primitive values (null, boolean, number, string) aligned with `members`. + /// + [JsonPropertyName("dataset")] + public IEnumerable> Dataset { get; set; } = new List>(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/Measure.cs b/src/Square/Types/Measure.cs new file mode 100644 index 000000000..d7c7e72fb --- /dev/null +++ b/src/Square/Types/Measure.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; +using Square.Core; + +namespace Square; + +[Serializable] +public record Measure : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("shortTitle")] + public string? ShortTitle { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("type")] + public required string Type { get; set; } + + [JsonPropertyName("aggType")] + public string? AggType { get; set; } + + [JsonPropertyName("meta")] + public Dictionary? Meta { get; set; } + + [JsonPropertyName("format")] + public OneOf< + SimpleFormat, + LinkFormat, + CustomTimeFormat, + CustomNumericFormat + >? Format { get; set; } + + [JsonPropertyName("formatDescription")] + public FormatDescription? FormatDescription { get; set; } + + /// + /// ISO 4217 currency code in uppercase (3 characters, e.g. USD, EUR) + /// + [JsonPropertyName("currency")] + public string? Currency { get; set; } + + /// + /// When measure is defined in View, it keeps the original path: Cube.measure + /// + [JsonPropertyName("aliasMember")] + public string? AliasMember { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/MetadataResponse.cs b/src/Square/Types/MetadataResponse.cs new file mode 100644 index 000000000..947bab36f --- /dev/null +++ b/src/Square/Types/MetadataResponse.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record MetadataResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("cubes")] + public IEnumerable? Cubes { get; set; } + + [JsonPropertyName("compilerId")] + public string? CompilerId { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/NestedFolder.cs b/src/Square/Types/NestedFolder.cs new file mode 100644 index 000000000..7f98a8931 --- /dev/null +++ b/src/Square/Types/NestedFolder.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record NestedFolder : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("members")] + public IEnumerable Members { get; set; } = new List(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/Query.cs b/src/Square/Types/Query.cs new file mode 100644 index 000000000..1eb76a676 --- /dev/null +++ b/src/Square/Types/Query.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; +using Square.Core; + +namespace Square; + +[Serializable] +public record Query : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("measures")] + public IEnumerable? Measures { get; set; } + + [JsonPropertyName("dimensions")] + public IEnumerable? Dimensions { get; set; } + + [JsonPropertyName("segments")] + public IEnumerable? Segments { get; set; } + + [JsonPropertyName("timeDimensions")] + public IEnumerable? TimeDimensions { get; set; } + + [JsonPropertyName("order")] + public IEnumerable>? Order { get; set; } + + [JsonPropertyName("limit")] + public int? Limit { get; set; } + + [JsonPropertyName("offset")] + public int? Offset { get; set; } + + [JsonPropertyName("filters")] + public IEnumerable< + OneOf + >? Filters { get; set; } + + [JsonPropertyName("ungrouped")] + public bool? Ungrouped { get; set; } + + [JsonPropertyName("subqueryJoins")] + public IEnumerable? SubqueryJoins { get; set; } + + [JsonPropertyName("joinHints")] + public IEnumerable>? JoinHints { get; set; } + + [JsonPropertyName("timezone")] + public string? Timezone { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/QueryFilterAnd.cs b/src/Square/Types/QueryFilterAnd.cs new file mode 100644 index 000000000..0c184a438 --- /dev/null +++ b/src/Square/Types/QueryFilterAnd.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record QueryFilterAnd : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("and")] + public IEnumerable> And { get; set; } = + new List>(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/QueryFilterCondition.cs b/src/Square/Types/QueryFilterCondition.cs new file mode 100644 index 000000000..8bbf9c664 --- /dev/null +++ b/src/Square/Types/QueryFilterCondition.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record QueryFilterCondition : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("member")] + public required string Member { get; set; } + + [JsonPropertyName("operator")] + public required string Operator { get; set; } + + [JsonPropertyName("values")] + public IEnumerable? Values { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/QueryFilterOr.cs b/src/Square/Types/QueryFilterOr.cs new file mode 100644 index 000000000..5e572be53 --- /dev/null +++ b/src/Square/Types/QueryFilterOr.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record QueryFilterOr : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("or")] + public IEnumerable> Or { get; set; } = + new List>(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/ReportingError.cs b/src/Square/Types/ReportingError.cs new file mode 100644 index 000000000..92f97524b --- /dev/null +++ b/src/Square/Types/ReportingError.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +/// +/// Error envelope returned by the Reporting API. Note: a 200 response whose body is `{ "error": "Continue wait" }` is not a failure — it signals that a long-running query is still processing and the request should be retried. +/// +[Serializable] +public record ReportingError : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("error")] + public required string Error { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/Segment.cs b/src/Square/Types/Segment.cs new file mode 100644 index 000000000..acb8f00db --- /dev/null +++ b/src/Square/Types/Segment.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[Serializable] +public record Segment : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("shortTitle")] + public required string ShortTitle { get; set; } + + [JsonPropertyName("meta")] + public Dictionary? Meta { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Square/Types/SimpleFormat.cs b/src/Square/Types/SimpleFormat.cs new file mode 100644 index 000000000..8c45c2834 --- /dev/null +++ b/src/Square/Types/SimpleFormat.cs @@ -0,0 +1,81 @@ +using System.Text.Json.Serialization; +using Square.Core; + +namespace Square; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct SimpleFormat : IStringEnum +{ + public static readonly SimpleFormat Percent = new(Values.Percent); + + public static readonly SimpleFormat Currency = new(Values.Currency); + + public static readonly SimpleFormat Number = new(Values.Number); + + public static readonly SimpleFormat ImageUrl = new(Values.ImageUrl); + + public static readonly SimpleFormat Id = new(Values.Id); + + public static readonly SimpleFormat Link = new(Values.Link); + + public SimpleFormat(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static SimpleFormat FromCustom(string value) + { + return new SimpleFormat(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(SimpleFormat value1, string value2) => + value1.Value.Equals(value2); + + public static bool operator !=(SimpleFormat value1, string value2) => + !value1.Value.Equals(value2); + + public static explicit operator string(SimpleFormat value) => value.Value; + + public static explicit operator SimpleFormat(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string Percent = "percent"; + + public const string Currency = "currency"; + + public const string Number = "number"; + + public const string ImageUrl = "imageUrl"; + + public const string Id = "id"; + + public const string Link = "link"; + } +} diff --git a/src/Square/Types/TimeDimension.cs b/src/Square/Types/TimeDimension.cs new file mode 100644 index 000000000..565d1bd98 --- /dev/null +++ b/src/Square/Types/TimeDimension.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; +using Square.Core; + +namespace Square; + +[Serializable] +public record TimeDimension : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("dimension")] + public required string Dimension { get; set; } + + [JsonPropertyName("granularity")] + public string? Granularity { get; set; } + + [JsonPropertyName("dateRange")] + public OneOf, Dictionary>? DateRange { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +}