diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 000000000..426f823c9 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,76 @@ +# AGENTS.md — Refitter + +OpenAPI-to-Refit code generator. Multi-package solution: CLI tool, MSBuild task, C# Source Generator, and Core library. + +## Build & Test + +- Solution: `src/Refitter.slnx` +- SDK: `10.0.300` with `rollForward: latestFeature` (see `global.json`) +- Build: `dotnet build -c Release src/Refitter.slnx` +- Test: `dotnet test --solution src/Refitter.slnx -c Release` + +**Source Generator tests require a pre-build step.** Before running `dotnet test` on `Refitter.SourceGenerator.Tests`, the CI does: + +```bash +rm -rf src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated +dotnet restore src/Refitter.SourceGenerator.Tests/Refitter.SourceGenerator.Tests.csproj +dotnet msbuild src/Refitter.SourceGenerator.Tests/Refitter.SourceGenerator.Tests.csproj +``` + +Run these first if source generator tests fail with missing generated types. + +## Package Boundaries + +| Project | TFM | Role | +|---|---|---| +| `Refitter.Core` | `netstandard2.0` | Core generation logic (NSwag-based) | +| `Refitter` | `net8.0;net9.0;net10.0` | CLI tool (`PackAsTool`, container support) | +| `Refitter.MSBuild` | `netstandard2.0` | MSBuild task; **requires Refitter CLI binaries** to be built first because it copies `../Refitter/bin/$(Configuration)/net{8,9,10}.0/**/*` into the package | +| `Refitter.SourceGenerator` | `netstandard2.0` | Roslyn source generator; emits code in-memory via `AddSource()` (not to disk since v2.0.0) | +| `Refitter.Tests` | `net10.0` | Unit tests | +| `Refitter.SourceGenerator.Tests` | `net8.0;net10.0` | Source generator integration tests | + +## Testing + +- **Framework**: TUnit (not xUnit). Attributes: `[Test]`, `[Arguments(...)]`. +- **Runner**: `Microsoft.Testing.Platform` (configured in `global.json`). Both test projects set `OutputType=Exe` and `GenerateProgramFile=false`. +- **Unit test pattern**: Scenario tests under `Refitter.Tests.Scenarios` contain an OpenAPI spec as a `const string`, generate code via `RefitGenerator.CreateAsync(...).Generate()`, assert string patterns, and verify compilation with `BuildHelper.BuildCSharp(generatedCode)`. +- **Temp file helper**: `SwaggerFileHelper.CreateSwaggerFile(contents)` creates a temp YAML file; `CreateSwaggerJsonFile` for JSON. +- **Build helper**: `BuildHelper.BuildCSharp()` writes generated code to a temp project and invokes `dotnet build` to verify it compiles. It defaults to `net8.0` but supports `net9.0` and `net10.0`. +- **Source generator tests**: Reference the generator as an `Analyzer` with `ReferenceOutputAssembly=false`. They verify generated types exist at runtime and have Refit HTTP attributes. + +## CI / Smoke Tests + +- Build workflow: `.github/workflows/build.yml` +- Smoke tests: `.github/workflows/smoke-tests.yml` runs `test/smoke-tests.ps1` +- The smoke test script publishes the CLI from source, generates clients against dozens of OpenAPI specs (v2.0, v3.0, v3.1, v3.4), and compiles them against console apps in `test/ConsoleApp/`. + +## Code Style + +- `.editorconfig` is authoritative. +- 4-space indentation; `crlf` line endings for C# files per `.editorconfig`. +- `var` is discouraged (`csharp_style_var_* = false:silent`). +- `TreatWarningsAsErrors` is enabled on most production projects. + +## Key Conventions + +- **New features** must have unit tests in the `Refitter.Tests.Scenarios` namespace following the pattern: spec string → generate → assert → build. +- **New CLI arguments** must be documented in `README.md` and in the `.refitter` file format docs. +- **`.refitter` files** are the settings format for all three distribution forms (CLI, MSBuild, Source Generator). +- **Source generator `.refitter` files** are automatically included as `AdditionalFiles` via the package props. The test project explicitly excludes `PropertyNamingPolicy.refitter` during design-time builds (`Condition="'$(DesignTimeBuild)' != 'true'"`). +- **MSBuild** package includes a custom `.targets` file that runs `RefitterGenerateTask` before `BeforeCompile`. + +## Commit Discipline + +- **Commit as often as possible** in small, logical groups. Each commit should represent one coherent change (e.g., one file created, one bug fix, one adapter added). +- **Build and run tests before every commit.** The minimum check is `dotnet build -c Release src/Refitter.slnx` followed by `dotnet test --solution src/Refitter.slnx -c Release --no-build`. +- **Never commit broken code.** If tests fail, fix them before committing. If the fix requires additional changes, commit those together as a "fix: ..." commit. +- **Commit messages** follow the pattern: `type: description` where type is one of `feat`, `fix`, `refactor`, `docs`, `test`, `chore`. Keep the subject line under 50 characters when possible. + +## Documentation + +- API docs are generated with DocFX from `docs/docfx_project/docfx.json`. To build locally: + ```bash + dotnet tool update -g docfx + docfx docs/docfx_project/docfx.json + ``` diff --git a/src/Refitter.Core/ByEndpointInterfacePartitioning.cs b/src/Refitter.Core/ByEndpointInterfacePartitioning.cs new file mode 100644 index 000000000..60b9bcbea --- /dev/null +++ b/src/Refitter.Core/ByEndpointInterfacePartitioning.cs @@ -0,0 +1,38 @@ +using System.Text; +using NSwag; + +namespace Refitter.Core; + +internal class ByEndpointInterfacePartitioning( + RefitGeneratorSettings settings) : IInterfacePartitioning +{ + + public string GetGroupKey(OpenApiOperationInfo operation) => $"{operation.Path}#{operation.Verb}"; + + public string GetInterfaceName(string groupKey, string title, string baseOperationName) => + $"I{baseOperationName.CapitalizeFirstCharacter()}".Sanitize(); + + public string GetInterfaceNameSuffix() => "Endpoint"; + + public string GetMethodName(OpenApiOperationInfo operation, string interfaceName, string baseOperationName) + { + var methodName = !string.IsNullOrWhiteSpace(settings.OperationNameTemplate) + ? settings.OperationNameTemplate!.Replace("{operationName}", "Execute") + : "Execute"; + + return methodName; + } + + public string GetDynamicQuerystringParameterType(string interfaceName, string methodName) => + interfaceName.Substring(1).Replace("Endpoint", "QueryParams"); + + public bool IsSingleInterface => false; + + public void AppendInterfaceDocumentation( + OpenApiDocument document, + XmlDocumentationGenerator docGenerator, + string groupKey, + OpenApiOperationInfo representativeOperation, + StringBuilder code) => + docGenerator.AppendInterfaceDocumentationByEndpoint(representativeOperation.Operation, code); +} diff --git a/src/Refitter.Core/ByTagInterfacePartitioning.cs b/src/Refitter.Core/ByTagInterfacePartitioning.cs new file mode 100644 index 000000000..ad32c1e24 --- /dev/null +++ b/src/Refitter.Core/ByTagInterfacePartitioning.cs @@ -0,0 +1,56 @@ +using System.Text; +using NSwag; + +namespace Refitter.Core; + +internal class ByTagInterfacePartitioning : IInterfacePartitioning +{ + private readonly RefitGeneratorSettings settings; + private readonly string ungroupedTitle; + + public ByTagInterfacePartitioning(RefitGeneratorSettings settings, OpenApiDocument document) + { + this.settings = settings; + ungroupedTitle = settings.Naming.UseOpenApiTitle + ? (document.Info?.Title ?? "ApiClient").Sanitize() + : settings.Naming.InterfaceName; + ungroupedTitle = ungroupedTitle.CapitalizeFirstCharacter(); + } + + public string GetGroupKey(OpenApiOperationInfo operation) + { + var tag = operation.Operation.Tags.FirstOrDefault(); + return !string.IsNullOrWhiteSpace(tag) + ? tag.SanitizeControllerTag() + : ungroupedTitle; + } + + public string GetInterfaceName(string groupKey, string title, string baseOperationName) => + $"I{groupKey.CapitalizeFirstCharacter()}".Sanitize(); + + public string GetInterfaceNameSuffix() => "Api"; + + public string GetMethodName(OpenApiOperationInfo operation, string interfaceName, string baseOperationName) + { + var methodName = baseOperationName.CapitalizeFirstCharacter(); + if (settings.OperationNameTemplate?.Contains("{operationName}") ?? false) + { + methodName = settings.OperationNameTemplate!.Replace("{operationName}", methodName); + } + + return methodName; + } + + public string GetDynamicQuerystringParameterType(string interfaceName, string methodName) => + methodName + "QueryParams"; + + public bool IsSingleInterface => false; + + public void AppendInterfaceDocumentation( + OpenApiDocument document, + XmlDocumentationGenerator docGenerator, + string groupKey, + OpenApiOperationInfo representativeOperation, + StringBuilder code) => + docGenerator.AppendInterfaceDocumentationByTag(document, groupKey, code); +} diff --git a/src/Refitter.Core/IInterfacePartitioning.cs b/src/Refitter.Core/IInterfacePartitioning.cs new file mode 100644 index 000000000..80e31bd2f --- /dev/null +++ b/src/Refitter.Core/IInterfacePartitioning.cs @@ -0,0 +1,47 @@ +using System.Text; +using NSwag; + +namespace Refitter.Core; + +internal interface IInterfacePartitioning +{ + /// + /// Gets the group key for an operation. Operations with the same key are placed in the same interface. + /// + string GetGroupKey(OpenApiOperationInfo operation); + + /// + /// Gets the raw interface name for a group key. The InterfaceGenerator handles deduplication. + /// + string GetInterfaceName(string groupKey, string title, string baseOperationName); + + /// + /// Gets the raw method name for an operation. The InterfaceGenerator handles deduplication. + /// + string GetMethodName(OpenApiOperationInfo operation, string interfaceName, string baseOperationName); + + /// + /// Gets the dynamic querystring parameter type name for a method. + /// + string GetDynamicQuerystringParameterType(string interfaceName, string methodName); + + /// + /// Gets the suffix appended to the interface name for deduplication. + /// + string GetInterfaceNameSuffix(); + + /// + /// True if this partitioning generates exactly one interface. + /// + bool IsSingleInterface { get; } + + /// + /// Appends interface-level documentation to the StringBuilder. + /// + void AppendInterfaceDocumentation( + OpenApiDocument document, + XmlDocumentationGenerator docGenerator, + string groupKey, + OpenApiOperationInfo representativeOperation, + StringBuilder code); +} diff --git a/src/Refitter.Core/IRefitInterfaceGenerator.cs b/src/Refitter.Core/IRefitInterfaceGenerator.cs deleted file mode 100644 index c7a012fbd..000000000 --- a/src/Refitter.Core/IRefitInterfaceGenerator.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Refitter.Core; - -internal interface IRefitInterfaceGenerator -{ - IEnumerable GenerateCode(); -} diff --git a/src/Refitter.Core/InterfaceGenerator.cs b/src/Refitter.Core/InterfaceGenerator.cs new file mode 100644 index 000000000..645f7fd58 --- /dev/null +++ b/src/Refitter.Core/InterfaceGenerator.cs @@ -0,0 +1,442 @@ +using System.Text; +using System.Text.RegularExpressions; +using NSwag; +using NSwag.CodeGeneration.CSharp.Models; + +namespace Refitter.Core; + +internal class InterfaceGenerator +{ + private const string Separator = " "; + private static readonly Regex HttpResponseMessageTypeRegex = new("(Task|IObservable)", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); + private static readonly Regex ApiResponseTypeRegex = new("(Task|IObservable)<(I)?ApiResponse(<[\\w<>]+>)?>", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); + + private readonly RefitGeneratorSettings settings; + private readonly OpenApiDocument document; + private readonly CustomCSharpClientGenerator generator; + private readonly XmlDocumentationGenerator docGenerator; + + internal InterfaceGenerator( + RefitGeneratorSettings settings, + OpenApiDocument document, + CustomCSharpClientGenerator generator, + XmlDocumentationGenerator docGenerator) + { + this.settings = settings; + this.document = document; + this.generator = generator; + this.docGenerator = docGenerator; + generator.BaseSettings.OperationNameGenerator = new OperationNameGenerator(document, settings); + } + + public IEnumerable Generate(IInterfacePartitioning partitioning) + { + var operations = document.Paths + .SelectMany(path => path.Value, (path, op) => new OpenApiOperationInfo(path.Key, op.Key, op.Value)) + .ToList(); + + var groups = operations + .GroupBy(partitioning.GetGroupKey) + .ToList(); + + var knownInterfaceIdentifiers = new HashSet(); + var title = settings.Naming.UseOpenApiTitle && !string.IsNullOrWhiteSpace(document.Info?.Title) + ? document.Info!.Title.Sanitize() + : settings.Naming.InterfaceName; + + if (partitioning.IsSingleInterface) + { + var singleGroup = groups.Count > 0 ? groups[0].ToList() : new List(); + yield return GenerateSingleInterface(singleGroup, partitioning, title, knownInterfaceIdentifiers); + } + else + { + foreach (var group in groups) + { + var nonDeprecatedOperations = group + .Where(op => settings.GenerateDeprecatedOperations || !op.Operation.IsDeprecated) + .ToList(); + + if (nonDeprecatedOperations.Count == 0) + continue; + + foreach (var generatedCode in GenerateMultipleInterface(nonDeprecatedOperations, partitioning, title, knownInterfaceIdentifiers)) + { + yield return generatedCode; + } + } + } + } + + private GeneratedCode GenerateSingleInterface( + List operations, + IInterfacePartitioning partitioning, + string title, + HashSet knownInterfaceIdentifiers) + { + var baseOperationName = operations.Count > 0 ? GetBaseOperationName(operations[0]) : string.Empty; + var rawInterfaceName = partitioning.GetInterfaceName(string.Empty, title, baseOperationName); + var interfaceNameSuffix = partitioning.GetInterfaceNameSuffix(); + var interfaceName = IdentifierUtils.Counted(knownInterfaceIdentifiers, rawInterfaceName, interfaceNameSuffix); + + var code = new StringBuilder(); + var dynamicQuerystringParametersCodeBuilder = new StringBuilder(); + var representativeOperation = operations.Count > 0 + ? operations[0] + : new OpenApiOperationInfo(string.Empty, string.Empty, new OpenApiOperation()); + partitioning.AppendInterfaceDocumentation(document, docGenerator, string.Empty, representativeOperation, code); + var interfaceDeclaration = GenerateInterfaceDeclaration(interfaceName, partitioning.IsSingleInterface); + code.AppendLine(interfaceDeclaration); + code.AppendLine($"{Separator}{{"); + + var knownMethodIdentifiers = new HashSet(); + + foreach (var op in operations) + { + var (dynamicQuerystringParams, _) = GenerateMethod(op, interfaceName, partitioning, knownMethodIdentifiers, code); + if (!string.IsNullOrWhiteSpace(dynamicQuerystringParams)) + { + dynamicQuerystringParametersCodeBuilder.AppendLine(dynamicQuerystringParams); + } + } + + code.AppendLine($"{Separator}}}"); + code.AppendLine(dynamicQuerystringParametersCodeBuilder.ToString()); + + return new GeneratedCode(interfaceName, code.ToString()); + } + + private IEnumerable GenerateMultipleInterface( + List operations, + IInterfacePartitioning partitioning, + string title, + HashSet knownInterfaceIdentifiers) + { + var representativeOperation = operations[0]; + var baseOperationName = GetBaseOperationName(representativeOperation); + var groupKey = partitioning.GetGroupKey(representativeOperation); + var rawInterfaceName = partitioning.GetInterfaceName(groupKey, title, baseOperationName); + var interfaceNameSuffix = partitioning.GetInterfaceNameSuffix(); + var interfaceName = IdentifierUtils.Counted(knownInterfaceIdentifiers, rawInterfaceName, interfaceNameSuffix); + + var code = new StringBuilder(); + partitioning.AppendInterfaceDocumentation(document, docGenerator, groupKey, representativeOperation, code); + var interfaceDeclaration = GenerateInterfaceDeclaration(interfaceName, partitioning.IsSingleInterface); + code.AppendLine(interfaceDeclaration); + code.AppendLine($"{Separator}{{"); + + var knownMethodIdentifiers = new HashSet(); + + foreach (var op in operations) + { + var (dynamicQuerystringParams, dynamicQuerystringType) = GenerateMethod(op, interfaceName, partitioning, knownMethodIdentifiers, code); + if (!string.IsNullOrWhiteSpace(dynamicQuerystringParams)) + { + yield return new GeneratedCode(dynamicQuerystringType, dynamicQuerystringParams); + } + } + + code.AppendLine($"{Separator}}}"); + + yield return new GeneratedCode(interfaceName, code.ToString()); + } + + private (string DynamicQuerystringParameters, string DynamicQuerystringParameterType) GenerateMethod( + OpenApiOperationInfo op, + string interfaceName, + IInterfacePartitioning partitioning, + HashSet knownMethodIdentifiers, + StringBuilder code) + { + var operation = op.Operation; + + if (!settings.GenerateDeprecatedOperations && operation.IsDeprecated) + { + return (string.Empty, string.Empty); + } + + var returnType = GetTypeName(operation); + var verb = op.Verb.CapitalizeFirstCharacter(); + var baseOperationName = GetBaseOperationName(op); + var rawMethodName = partitioning.GetMethodName(op, interfaceName, baseOperationName); + var methodName = IdentifierUtils.Counted(knownMethodIdentifiers, rawMethodName); + knownMethodIdentifiers.Add(methodName); + + var dynamicQuerystringParameterType = partitioning.GetDynamicQuerystringParameterType(interfaceName, methodName); + var operationModel = generator.CreateOperationModel(operation); + var parameters = ParameterExtractor.GetParameters(operationModel, operation, settings, dynamicQuerystringParameterType, out var operationDynamicQuerystringParameters).ToList(); + + var hasDynamicQuerystringParameter = !string.IsNullOrWhiteSpace(operationDynamicQuerystringParameters); + var parametersString = string.Join(", ", parameters); + var hasApizrRequestOptionsParameter = settings.ApizrSettings?.WithRequestOptions == true; + var hasCancellationToken = settings.UseCancellationTokens && !hasApizrRequestOptionsParameter; + var isApiResponseType = IsApiResponseType(returnType); + + if (settings.GenerateXmlDocCodeComments) + { + docGenerator.AppendMethodDocumentation(operationModel, isApiResponseType, hasDynamicQuerystringParameter, hasApizrRequestOptionsParameter, hasCancellationToken, code); + } + + GenerateObsoleteAttribute(operation, code); + GenerateForMultipartFormData(operationModel, code); + GenerateHeaders(operation, operationModel, code); + + code.AppendLine($"{Separator}{Separator}[{verb}(\"{op.Path}\")]") + .AppendLine($"{Separator}{Separator}{returnType} {methodName}({parametersString});") + .AppendLine(); + + if (parametersString.Contains("?") && settings is { OptionalParameters: true, ApizrSettings: not null }) + { + if (settings.GenerateXmlDocCodeComments) + { + docGenerator.AppendMethodDocumentation(operationModel, isApiResponseType, false, hasApizrRequestOptionsParameter, hasCancellationToken, code); + } + + GenerateObsoleteAttribute(operation, code); + GenerateForMultipartFormData(operationModel, code); + GenerateHeaders(operation, operationModel, code); + + parametersString = string.Join(", ", parameters.Where(parameter => !parameter.Contains("?"))); + + code.AppendLine($"{Separator}{Separator}[{verb}(\"{op.Path}\")]") + .AppendLine($"{Separator}{Separator}{returnType} {methodName}({parametersString});") + .AppendLine(); + } + + return (operationDynamicQuerystringParameters ?? string.Empty, dynamicQuerystringParameterType); + } + + private string GetBaseOperationName(OpenApiOperationInfo op) + { + return generator + .BaseSettings + .OperationNameGenerator + .GetOperationName(document, op.Path, op.Verb, op.Operation); + } + + private string GetTypeName(OpenApiOperation operation) + { + if (settings.ResponseTypeOverride.TryGetValue(operation.OperationId, out var type)) + { + return type is null or "void" + ? GetAsyncOperationType(true) + : $"{GetAsyncOperationType(false)}<{WellKnownNamespaces.TrimImportedNamespaces(type)}>"; + } + + if (IsFileStreamResponse(operation)) + { + return $"{GetAsyncOperationType(false)}"; + } + + var successCodes = new[] { "200", "201", "203", "206" }; + var returnTypeParameter = successCodes + .Where(operation.Responses.ContainsKey) + .Select(code => GetTypeName(code, operation)) + .FirstOrDefault(); + + if (returnTypeParameter == null && operation.Responses.ContainsKey("2XX")) + { + returnTypeParameter = GetTypeName("2XX", operation); + } + + if (returnTypeParameter == null && operation.Responses.ContainsKey("default")) + { + returnTypeParameter = GetTypeName("default", operation); + } + + return GetReturnType(returnTypeParameter); + } + + private static bool IsFileStreamResponse(OpenApiOperation operation) + { + var successCodes = new[] { "200", "201", "203", "206", "2XX" }; + + foreach (var code in successCodes) + { + if (!operation.Responses.TryGetValue(code, out var apiResponse)) + continue; + + var response = apiResponse.ActualResponse; + + if (response.Content?.Any() != true) + continue; + + foreach (var contentEntry in response.Content) + { + if (IsFileContentType(contentEntry.Key)) + { + var schema = contentEntry.Value?.Schema; + if (schema?.Format == "binary" || schema?.Type == NJsonSchema.JsonObjectType.File) + return true; + } + } + } + + return false; + } + + private static bool IsFileContentType(string contentType) + { + return + contentType.StartsWith("application/octet-stream", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("application/vnd", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("application/zip", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("application/gzip", StringComparison.OrdinalIgnoreCase) || + (contentType.StartsWith("application/x-", StringComparison.OrdinalIgnoreCase) && + !contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)); + } + + private string GetTypeName(string code, OpenApiOperation operation) + { + var schema = operation.Responses[code].ActualResponse.Schema; + var typeName = generator.GetTypeName(schema, false, null); + + if (!string.IsNullOrWhiteSpace(settings.CodeGeneratorSettings?.ArrayType) && + schema?.Type == NJsonSchema.JsonObjectType.Array) + { + typeName = typeName + .Replace("ICollection", settings.CodeGeneratorSettings!.ArrayType) + .Replace("IEnumerable", settings.CodeGeneratorSettings!.ArrayType); + } + + return typeName; + } + + private string GetReturnType(string? returnTypeParameter) + { + return returnTypeParameter is null or "void" + ? GetDefaultReturnType() + : GetConfiguredReturnType(returnTypeParameter); + } + + private string GetDefaultReturnType() + { + var asyncType = GetAsyncOperationType(true); + return settings.ReturnIApiResponse + ? $"{asyncType}" + : asyncType; + } + + private string GetConfiguredReturnType(string returnTypeParameter) + { + var asyncType = GetAsyncOperationType(false); + return settings.ReturnIApiResponse + ? $"{asyncType}>" + : $"{asyncType}<{WellKnownNamespaces.TrimImportedNamespaces(returnTypeParameter)}>"; + } + + private string GetAsyncOperationType(bool withVoidReturnType) + { + var type = withVoidReturnType ? "" : string.Empty; + return settings.ReturnIObservable + ? "IObservable" + type + : "Task"; + } + + private static bool IsApiResponseType(string typeName) + { + return HttpResponseMessageTypeRegex.IsMatch(typeName) || ApiResponseTypeRegex.IsMatch(typeName); + } + + private static void GenerateObsoleteAttribute(OpenApiOperation operation, StringBuilder code) + { + if (operation.IsDeprecated) + { + code.AppendLine($"{Separator}{Separator}[System.Obsolete]"); + } + } + + private static void GenerateForMultipartFormData(CSharpOperationModel operationModel, StringBuilder code) + { + if (operationModel.Consumes.Contains("multipart/form-data")) + { + code.AppendLine($"{Separator}{Separator}[Multipart]"); + } + } + + private void GenerateHeaders( + OpenApiOperation operation, + CSharpOperationModel operationModel, + StringBuilder code) + { + var headers = new List(); + + if (settings.AddAcceptHeaders && document.SchemaType is >= NJsonSchema.SchemaType.OpenApi3) + { + var uniqueContentTypes = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var response in operation.Responses.Values) + { + if (response.Content == null) + continue; + + foreach (var contentType in response.Content.Keys) + { + uniqueContentTypes.Add(contentType); + } + } + + if (uniqueContentTypes.Any()) + { + headers.Add($"\"Accept: {string.Join(", ", uniqueContentTypes)}\""); + } + } + + if (settings.AddContentTypeHeaders && document.SchemaType is >= NJsonSchema.SchemaType.OpenApi3) + { + var uniqueContentTypes = operation.RequestBody?.Content.Keys ?? Array.Empty(); + var contentType = + uniqueContentTypes.FirstOrDefault(c => c.Equals("application/json", StringComparison.OrdinalIgnoreCase)) ?? + uniqueContentTypes.FirstOrDefault(); + + if (!string.IsNullOrWhiteSpace(contentType) && !operationModel.Consumes.Contains("multipart/form-data")) + { + headers.Add($"\"Content-Type: {contentType}\""); + } + } + + if (settings.AuthenticationHeaderStyle == AuthenticationHeaderStyle.Method) + { + foreach (var securitySchemeName in operationModel.Security.SelectMany(x => x.Keys)) + { + if ((settings.SecurityScheme != null && securitySchemeName != settings.SecurityScheme) || + !document.SecurityDefinitions.TryGetValue(securitySchemeName, out var securityScheme)) + { + continue; + } + + if (securityScheme is { Type: OpenApiSecuritySchemeType.Http, Scheme: var scheme } + && string.Equals(scheme, "bearer", StringComparison.OrdinalIgnoreCase)) + { + headers.Add("\"Authorization: Bearer\""); + } + } + } + + if (headers.Any()) + { + code.AppendLine($"{Separator}{Separator}[Headers({string.Join(", ", headers)})]"); + } + } + + private string GenerateInterfaceDeclaration(string interfaceName, bool isSingleInterface) + { + var inheritance = isSingleInterface && settings.GenerateDisposableClients + ? " : IDisposable" + : null; + + var modifier = settings.TypeAccessibility.ToString().ToLowerInvariant(); + return $""" + {Separator}{GetGeneratedCodeAttribute()} + {Separator}{modifier} partial interface {interfaceName}{inheritance} + """; + } + + private string GetGeneratedCodeAttribute() => + $""" + [System.CodeDom.Compiler.GeneratedCode("Refitter", "{GetType().Assembly.GetName().Version}")] + """; +} diff --git a/src/Refitter.Core/OpenApiOperationInfo.cs b/src/Refitter.Core/OpenApiOperationInfo.cs new file mode 100644 index 000000000..81c2b80bc --- /dev/null +++ b/src/Refitter.Core/OpenApiOperationInfo.cs @@ -0,0 +1,5 @@ +using NSwag; + +namespace Refitter.Core; + +internal record OpenApiOperationInfo(string Path, string Verb, OpenApiOperation Operation); diff --git a/src/Refitter.Core/RefitGenerator.cs b/src/Refitter.Core/RefitGenerator.cs index f1119b153..a36bf8f02 100644 --- a/src/Refitter.Core/RefitGenerator.cs +++ b/src/Refitter.Core/RefitGenerator.cs @@ -153,7 +153,7 @@ public string Generate() // (pre-generation) operation IDs. GenerateFile() auto-populates operation IDs // with globally unique names which would prevent the switch to the path segments // generator, causing unnecessary numeric suffixes in ByTag mode. - var interfaceGenerator = CreateInterfaceGenerator(generator, docGenerator); + var interfaceGenerator = new InterfaceGenerator(settings, document, generator, docGenerator); var contracts = generator.GenerateFile(); contracts = SanitizeGeneratedContracts(contracts); @@ -210,7 +210,7 @@ public GeneratorOutput GenerateMultipleFiles() // (pre-generation) operation IDs. GenerateFile() auto-populates operation IDs // with globally unique names which would prevent the switch to the path segments // generator, causing unnecessary numeric suffixes in ByTag mode. - var interfaceGenerator = CreateInterfaceGenerator(generator, docGenerator); + var interfaceGenerator = new InterfaceGenerator(settings, document, generator, docGenerator); var contracts = generator.GenerateFile(); contracts = SanitizeGeneratedContracts(contracts); @@ -271,19 +271,13 @@ public GeneratorOutput GenerateMultipleFiles() return new GeneratorOutput(generatedFiles); } - /// - /// Creates the appropriate interface generator based on settings. - /// This must be called before GenerateFile() to ensure operation ID detection works correctly. - /// - private IRefitInterfaceGenerator CreateInterfaceGenerator( - CustomCSharpClientGenerator generator, - XmlDocumentationGenerator docGenerator) + private IInterfacePartitioning GetInterfacePartitioning() { return settings.MultipleInterfaces switch { - MultipleInterfaces.ByEndpoint => new RefitMultipleInterfaceGenerator(settings, document, generator, docGenerator), - MultipleInterfaces.ByTag => new RefitMultipleInterfaceByTagGenerator(settings, document, generator, docGenerator), - _ => new RefitInterfaceGenerator(settings, document, generator, docGenerator), + MultipleInterfaces.ByEndpoint => new ByEndpointInterfacePartitioning(settings), + MultipleInterfaces.ByTag => new ByTagInterfacePartitioning(settings, document), + _ => new SingleInterfacePartitioning(settings), }; } @@ -368,7 +362,7 @@ private string GenerateJsonSerializerContext(string contracts) => /// /// The interface generator used to generate the client code. /// The generated client code as a string. - private IReadOnlyCollection GenerateClient(IRefitInterfaceGenerator interfaceGenerator) + private IReadOnlyCollection GenerateClient(InterfaceGenerator interfaceGenerator) { var code = new StringBuilder(); GenerateAutoGeneratedHeader(code); @@ -390,7 +384,8 @@ private IReadOnlyCollection GenerateClient(IRefitInterfaceGenerat code.AppendLine("#nullable enable annotations"); code.AppendLine(); - var refitInterfaces = interfaceGenerator.GenerateCode(); + var partitioning = GetInterfacePartitioning(); + var refitInterfaces = interfaceGenerator.Generate(partitioning); var generatedCodes = refitInterfaces as GeneratedCode[] ?? refitInterfaces.ToArray(); if (settings.GenerateMultipleFiles) diff --git a/src/Refitter.Core/RefitInterfaceGenerator.cs b/src/Refitter.Core/RefitInterfaceGenerator.cs deleted file mode 100644 index bdfcb8aa9..000000000 --- a/src/Refitter.Core/RefitInterfaceGenerator.cs +++ /dev/null @@ -1,390 +0,0 @@ -using System.Text; -using System.Text.RegularExpressions; -using NSwag; -using NSwag.CodeGeneration.CSharp.Models; - -namespace Refitter.Core; - -internal class RefitInterfaceGenerator : IRefitInterfaceGenerator -{ - protected const string Separator = " "; - private static readonly Regex HttpResponseMessageTypeRegex = new("(Task|IObservable)", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); - private static readonly Regex ApiResponseTypeRegex = new("(Task|IObservable)<(I)?ApiResponse(<[\\w<>]+>)?>", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); - - protected readonly RefitGeneratorSettings settings; - protected readonly OpenApiDocument document; - protected readonly CustomCSharpClientGenerator generator; - protected readonly XmlDocumentationGenerator docGenerator; - - internal RefitInterfaceGenerator( - RefitGeneratorSettings settings, - OpenApiDocument document, - CustomCSharpClientGenerator generator, - XmlDocumentationGenerator docGenerator) - { - this.settings = settings; - this.document = document; - this.generator = generator; - this.docGenerator = docGenerator; - generator.BaseSettings.OperationNameGenerator = new OperationNameGenerator(document, settings); - } - - public virtual IEnumerable GenerateCode() - { - var interfaceDeclaration = GenerateInterfaceDeclaration(out var interfaceName); - yield return new GeneratedCode( - interfaceName, - $$""" - {{interfaceDeclaration}} - {{Separator}}{ - {{GenerateInterfaceBody(out var dynamicQuerystringParameters)}} - {{Separator}}} - {{dynamicQuerystringParameters}} - """); - } - - private string GenerateInterfaceBody(out string? dynamicQuerystringParameters) - { - var code = new StringBuilder(); - var dynamicQuerystringParametersCodeBuilder = new StringBuilder(); - var knownIdentifiers = new HashSet(); - foreach (var kv in document.Paths) - { - foreach (var operations in kv.Value) - { - var operation = operations.Value; - - if (!settings.GenerateDeprecatedOperations && operation.IsDeprecated) - { - continue; - } - - var returnType = GetTypeName(operation); - var verb = operations.Key.CapitalizeFirstCharacter(); - var operationName = IdentifierUtils.Counted(knownIdentifiers, GenerateOperationName(kv.Key, verb, operation)); - knownIdentifiers.Add(operationName); - var dynamicQuerystringParameterType = operationName + "QueryParams"; - - var operationModel = generator.CreateOperationModel(operation); - var parameters = ParameterExtractor.GetParameters(operationModel, operation, settings, dynamicQuerystringParameterType, out var operationDynamicQuerystringParameters).ToList(); - - var hasDynamicQuerystringParameter = !string.IsNullOrWhiteSpace(operationDynamicQuerystringParameters); - if (hasDynamicQuerystringParameter) - dynamicQuerystringParametersCodeBuilder.AppendLine(operationDynamicQuerystringParameters); - - var parametersString = string.Join(", ", parameters); - var hasApizrRequestOptionsParameter = settings.ApizrSettings?.WithRequestOptions == true; - var hasCancellationToken = settings.UseCancellationTokens && !hasApizrRequestOptionsParameter; - var isApiResponseType = IsApiResponseType(returnType); - - if (settings.GenerateXmlDocCodeComments) - { - this.docGenerator.AppendMethodDocumentation(operationModel, isApiResponseType, hasDynamicQuerystringParameter, hasApizrRequestOptionsParameter, hasCancellationToken, code); - } - - GenerateObsoleteAttribute(operation, code); - GenerateForMultipartFormData(operationModel, code); - GenerateHeaders(operations, operationModel, code); - - code.AppendLine($"{Separator}{Separator}[{verb}(\"{kv.Key}\")]") - .AppendLine($"{Separator}{Separator}{returnType} {operationName}({parametersString});") - .AppendLine(); - - if (parametersString.Contains("?") && settings is { OptionalParameters: true, ApizrSettings: not null }) - { - if (settings.GenerateXmlDocCodeComments) - { - this.docGenerator.AppendMethodDocumentation(operationModel, isApiResponseType, false, hasApizrRequestOptionsParameter, hasCancellationToken, code); - } - GenerateObsoleteAttribute(operation, code); - GenerateForMultipartFormData(operationModel, code); - GenerateHeaders(operations, operationModel, code); - - parametersString = string.Join(", ", parameters.Where(parameter => !parameter.Contains("?"))); - - code.AppendLine($"{Separator}{Separator}[{verb}(\"{kv.Key}\")]") - .AppendLine($"{Separator}{Separator}{returnType} {operationName}({parametersString});") - .AppendLine(); - } - } - } - - dynamicQuerystringParameters = dynamicQuerystringParametersCodeBuilder.ToString(); - - return code.ToString(); - } - - protected string GetTypeName(OpenApiOperation operation) - { - if (settings.ResponseTypeOverride.TryGetValue(operation.OperationId, out var type)) - { - return type is null or "void" ? GetAsyncOperationType(true) : $"{GetAsyncOperationType(false)}<{WellKnownNamespaces.TrimImportedNamespaces(type)}>"; - } - - // Check if response is a file stream - if (IsFileStreamResponse(operation)) - { - return $"{GetAsyncOperationType(false)}"; - } - - // First check for explicit success status codes - var successCodes = new[] { "200", "201", "203", "206" }; - var returnTypeParameter = successCodes - .Where(operation.Responses.ContainsKey) - .Select(code => GetTypeName(code, operation)) - .FirstOrDefault(); - - // If no explicit success codes found, check for 2XX range - if (returnTypeParameter == null && operation.Responses.ContainsKey("2XX")) - { - returnTypeParameter = GetTypeName("2XX", operation); - } - - // If no success codes or ranges found, check for default response - if (returnTypeParameter == null && operation.Responses.ContainsKey("default")) - { - returnTypeParameter = GetTypeName("default", operation); - } - - return GetReturnType(returnTypeParameter); - } - - /// - /// Determines if the operation response is a file stream (binary content). - /// - /// The OpenAPI operation to check. - /// True if the response is a file stream, false otherwise. - private static bool IsFileStreamResponse(OpenApiOperation operation) - { - var successCodes = new[] { "200", "201", "203", "206", "2XX" }; - - foreach (var code in successCodes) - { - if (!operation.Responses.TryGetValue(code, out var apiResponse)) - continue; - - var response = apiResponse.ActualResponse; - - if (response.Content?.Any() != true) - continue; - - foreach (var contentEntry in response.Content) - { - if (IsFileContentType(contentEntry.Key)) - { - var schema = contentEntry.Value?.Schema; - if (schema?.Format == "binary" || schema?.Type == NJsonSchema.JsonObjectType.File) - return true; - } - } - } - - return false; - } - - private static bool IsFileContentType(string contentType) - { - return - contentType.StartsWith("application/octet-stream", StringComparison.OrdinalIgnoreCase) || - contentType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase) || - contentType.StartsWith("application/vnd", StringComparison.OrdinalIgnoreCase) || - contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || - contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase) || - contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || - contentType.StartsWith("application/zip", StringComparison.OrdinalIgnoreCase) || - contentType.StartsWith("application/gzip", StringComparison.OrdinalIgnoreCase) || - (contentType.StartsWith("application/x-", StringComparison.OrdinalIgnoreCase) && - !contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)); - } - - private string GetTypeName(string code, OpenApiOperation operation) - { - var schema = operation.Responses[code].ActualResponse.Schema; - var typeName = generator.GetTypeName(schema, false, null); - - if (!string.IsNullOrWhiteSpace(settings.CodeGeneratorSettings?.ArrayType) && - schema?.Type == NJsonSchema.JsonObjectType.Array) - { - typeName = typeName - .Replace("ICollection", settings.CodeGeneratorSettings!.ArrayType) - .Replace("IEnumerable", settings.CodeGeneratorSettings!.ArrayType); - } - - return typeName; - } - - protected string GenerateOperationName( - string path, - string verb, - OpenApiOperation operation, - bool capitalizeFirstCharacter = false) - { - const string operationNamePlaceholder = "{operationName}"; - - var operationName = generator - .BaseSettings - .OperationNameGenerator - .GetOperationName(document, path, verb, operation); - - if (capitalizeFirstCharacter) - operationName = operationName.CapitalizeFirstCharacter(); - - if (settings.OperationNameTemplate?.Contains(operationNamePlaceholder) ?? false) - { - operationName = settings.OperationNameTemplate! - .Replace(operationNamePlaceholder, operationName); - } - - return operationName; - } - - protected static void GenerateForMultipartFormData(CSharpOperationModel operationModel, StringBuilder code) - { - if (operationModel.Consumes.Contains("multipart/form-data")) - { - code.AppendLine($"{Separator}{Separator}[Multipart]"); - } - } - - protected void GenerateHeaders( - KeyValuePair operations, - CSharpOperationModel operationModel, - StringBuilder code) - { - var headers = new List(); - - if (settings.AddAcceptHeaders && document.SchemaType is >= NJsonSchema.SchemaType.OpenApi3) - { - //Generate header "Accept" - var uniqueContentTypes = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var response in operations.Value.Responses.Values) - { - if (response.Content == null) - continue; - - foreach (var contentType in response.Content.Keys) - { - uniqueContentTypes.Add(contentType); - } - } - - if (uniqueContentTypes.Any()) - { - headers.Add($"\"Accept: {string.Join(", ", uniqueContentTypes)}\""); - } - } - - if (settings.AddContentTypeHeaders && document.SchemaType is >= NJsonSchema.SchemaType.OpenApi3) - { - var uniqueContentTypes = operations.Value.RequestBody?.Content.Keys ?? Array.Empty(); - var contentType = - uniqueContentTypes.FirstOrDefault(c => c.Equals("application/json", StringComparison.OrdinalIgnoreCase)) ?? - uniqueContentTypes.FirstOrDefault(); - - if (!string.IsNullOrWhiteSpace(contentType) && !operationModel.Consumes.Contains("multipart/form-data")) - { - headers.Add($"\"Content-Type: {contentType}\""); - } - } - - if (settings.AuthenticationHeaderStyle == AuthenticationHeaderStyle.Method) - { - foreach (var securitySchemeName in operationModel.Security.SelectMany(x => x.Keys)) - { - if ((settings.SecurityScheme != null && securitySchemeName != settings.SecurityScheme) || - !document.SecurityDefinitions.TryGetValue(securitySchemeName, out var securityScheme)) - { - continue; - } - - if (securityScheme is { Type: OpenApiSecuritySchemeType.Http, Scheme: var scheme } - && string.Equals(scheme, "bearer", StringComparison.OrdinalIgnoreCase)) - { - headers.Add("\"Authorization: Bearer\""); - } - } - } - - if (headers.Any()) - { - code.AppendLine($"{Separator}{Separator}[Headers({string.Join(", ", headers)})]"); - } - } - - protected string GetReturnType(string? returnTypeParameter) - { - return returnTypeParameter is null or "void" - ? GetDefaultReturnType() - : GetConfiguredReturnType(returnTypeParameter); - } - - private string GetDefaultReturnType() - { - var asyncType = GetAsyncOperationType(true); - return settings.ReturnIApiResponse - ? $"{asyncType}" - : asyncType; - } - - /// - /// Checks if the given return type is derived from ApiResponse or its interface. - /// - /// The name of the type to check. - /// True if the type is an ApiResponse Task or similar, false otherwise. - protected static bool IsApiResponseType(string typeName) - { - return HttpResponseMessageTypeRegex.IsMatch(typeName) || ApiResponseTypeRegex.IsMatch(typeName); - } - - private string GetConfiguredReturnType(string returnTypeParameter) - { - var asyncType = GetAsyncOperationType(false); - return settings.ReturnIApiResponse - ? $"{asyncType}>" - : $"{asyncType}<{WellKnownNamespaces.TrimImportedNamespaces(returnTypeParameter)}>"; - } - - private string GetAsyncOperationType(bool withVoidReturnType) - { - var type = withVoidReturnType ? "" : string.Empty; - return settings.ReturnIObservable - ? "IObservable" + type - : "Task"; - } - - protected void GenerateObsoleteAttribute(OpenApiOperation operation, StringBuilder code) - { - if (operation.IsDeprecated) - { - code.AppendLine($"{Separator}{Separator}[System.Obsolete]"); - } - } - - private string GenerateInterfaceDeclaration(out string interfaceName) - { - var title = settings.Naming.UseOpenApiTitle - ? (document.Info?.Title ?? NamingSettings.DefaultInterfaceName) - : settings.Naming.InterfaceName; - - // Sanitize after prefixing to prevent I@keyword pattern (#1053) - interfaceName = $"I{title.CapitalizeFirstCharacter()}".Sanitize(); - - var inheritance = settings.GenerateDisposableClients - ? " : IDisposable" - : null; - - var modifier = settings.TypeAccessibility.ToString().ToLowerInvariant(); - var code = new StringBuilder(); - docGenerator.AppendSingleInterfaceDocumentation(document, code); - code.Append($""" - {Separator}{GetGeneratedCodeAttribute()} - {Separator}{modifier} partial interface {interfaceName}{inheritance} - """); - return code.ToString(); - } - - protected string GetGeneratedCodeAttribute() => - $""" - [System.CodeDom.Compiler.GeneratedCode("Refitter", "{GetType().Assembly.GetName().Version}")] - """; -} diff --git a/src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs b/src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs deleted file mode 100644 index bb398548b..000000000 --- a/src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System.Text; -using NSwag; - -namespace Refitter.Core; - -internal class RefitMultipleInterfaceByTagGenerator : RefitInterfaceGenerator -{ - private readonly HashSet knownIdentifiers = new(); - private Dictionary>? _methodIdentifiersByInterface; - - internal RefitMultipleInterfaceByTagGenerator( - RefitGeneratorSettings settings, - OpenApiDocument document, - CustomCSharpClientGenerator generator, - XmlDocumentationGenerator docGenerator) - : base(settings, document, generator, docGenerator) - { - } - - public override IEnumerable GenerateCode() - { - var ungroupedTitle = settings.Naming.UseOpenApiTitle - ? IdentifierUtils.Sanitize(document.Info?.Title ?? "ApiClient") - : settings.Naming.InterfaceName; - ungroupedTitle = ungroupedTitle.CapitalizeFirstCharacter(); - - var byGroup = document.Paths - .SelectMany(x => x.Value, (k, v) => (PathItem: k, Operation: v)) - .GroupBy(x => GetGroupName(x.Operation.Value, ungroupedTitle), (k, v) => new { Key = k, Combined = v }); - - Dictionary interfacesByGroup = new(); - Dictionary interfacesNamesByGroup = new(); - - _methodIdentifiersByInterface = new(); - - foreach (var kv in byGroup) - { - foreach (var op in kv.Combined) - { - var operations = op.Operation; - var operation = operations.Value; - - if (!settings.GenerateDeprecatedOperations && operation.IsDeprecated) - { - continue; - } - - var returnType = GetTypeName(operation); - var verb = operations.Key.CapitalizeFirstCharacter(); - - string interfaceName; - if (!interfacesByGroup.TryGetValue(kv.Key, out var sb)) - { - interfacesByGroup[kv.Key] = sb = new StringBuilder(); - this.docGenerator.AppendInterfaceDocumentationByTag(document, kv.Key, sb); - - interfaceName = GetInterfaceName(kv.Key); - sb.AppendLine($$""" - {{GenerateInterfaceDeclaration(interfaceName)}} - {{Separator}}{ - """); - - interfacesNamesByGroup[kv.Key] = interfaceName; - } - else - { - interfaceName = interfacesNamesByGroup[kv.Key]; - } - - var operationName = GetOperationName(interfaceName, op.PathItem.Key, operations.Key, operation); - var dynamicQuerystringParameterType = operationName + "QueryParams"; - var operationModel = generator.CreateOperationModel(operation); - var parameters = ParameterExtractor - .GetParameters( - operationModel, - operation, - settings, - dynamicQuerystringParameterType, - out var operationDynamicQuerystringParameters) - .ToList(); - - var hasDynamicQuerystringParameter = !string.IsNullOrWhiteSpace(operationDynamicQuerystringParameters); - if (hasDynamicQuerystringParameter) - yield return new GeneratedCode( - dynamicQuerystringParameterType, - operationDynamicQuerystringParameters!); - - var parametersString = string.Join(", ", parameters); - var hasApizrRequestOptionsParameter = settings.ApizrSettings?.WithRequestOptions == true; - var hasCancellationToken = settings.UseCancellationTokens && !hasApizrRequestOptionsParameter; - - this.docGenerator.AppendMethodDocumentation(operationModel, IsApiResponseType(returnType), hasDynamicQuerystringParameter, hasApizrRequestOptionsParameter, hasCancellationToken, sb); - GenerateObsoleteAttribute(operation, sb); - GenerateForMultipartFormData(operationModel, sb); - GenerateHeaders(operations, operationModel, sb); - - sb.AppendLine($"{Separator}{Separator}[{verb}(\"{op.PathItem.Key}\")]") - .AppendLine($"{Separator}{Separator}{returnType} {operationName}({parametersString});") - .AppendLine(); - - if (parametersString.Contains("?") && settings is { OptionalParameters: true, ApizrSettings: not null }) - { - this.docGenerator.AppendMethodDocumentation(operationModel, IsApiResponseType(returnType), false, hasApizrRequestOptionsParameter, hasCancellationToken, sb); - GenerateObsoleteAttribute(operation, sb); - GenerateForMultipartFormData(operationModel, sb); - GenerateHeaders(operations, operationModel, sb); - - parametersString = string.Join(", ", parameters.Where(parameter => !parameter.Contains("?"))); - - sb.AppendLine($"{Separator}{Separator}[{verb}(\"{op.PathItem.Key}\")]") - .AppendLine($"{Separator}{Separator}{returnType} {operationName}({parametersString});") - .AppendLine(); - } - } - } - - foreach (var keyValuePair in interfacesByGroup) - { - var code = new StringBuilder(); - var key = keyValuePair.Key; - var value = keyValuePair.Value; - - while (char.IsWhiteSpace(value[value.Length - 1])) - { - value.Length--; - } - - code.AppendLine(value.ToString()); - code.AppendLine($"{Separator}}}"); - - yield return new GeneratedCode( - interfacesNamesByGroup[key], - code.ToString()); - } - } - - private string GetGroupName(OpenApiOperation operation, string ungroupedTitle) - { - if (operation.Tags.FirstOrDefault() is string group && !string.IsNullOrWhiteSpace(group)) - { - return group.SanitizeControllerTag(); - } - - return ungroupedTitle; - } - - private string GetInterfaceName(string name) - { - var generatedName = IdentifierUtils.Counted( - knownIdentifiers, - $"I{name.CapitalizeFirstCharacter()}", - suffix: "Api" - ); - - knownIdentifiers.Add(generatedName); - return generatedName; - } - - private string GetOperationName( - string interfaceName, - string name, - string verb, - OpenApiOperation operation) - { - // Initialize per-interface tracking if needed - if (!_methodIdentifiersByInterface!.ContainsKey(interfaceName)) - { - _methodIdentifiersByInterface[interfaceName] = new HashSet(); - } - - var interfaceIdentifiers = _methodIdentifiersByInterface[interfaceName]; - var generatedName = IdentifierUtils.Counted( - interfaceIdentifiers, - GenerateOperationName(name, verb, operation, capitalizeFirstCharacter: true), - parent: interfaceName); - - interfaceIdentifiers.Add(generatedName); - return generatedName; - } - - private string GenerateInterfaceDeclaration(string name) - { - var modifier = settings.TypeAccessibility.ToString().ToLowerInvariant(); - return $""" - {Separator}{GetGeneratedCodeAttribute()} - {Separator}{modifier} partial interface {name} - """; - } -} diff --git a/src/Refitter.Core/RefitMultipleInterfaceGenerator.cs b/src/Refitter.Core/RefitMultipleInterfaceGenerator.cs deleted file mode 100644 index 80ff3c199..000000000 --- a/src/Refitter.Core/RefitMultipleInterfaceGenerator.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Text; -using NSwag; - -namespace Refitter.Core; - -internal class RefitMultipleInterfaceGenerator : RefitInterfaceGenerator -{ - private readonly HashSet knownIdentifiers = new(); - - internal RefitMultipleInterfaceGenerator( - RefitGeneratorSettings settings, - OpenApiDocument document, - CustomCSharpClientGenerator generator, - XmlDocumentationGenerator docGenerator) - : base(settings, document, generator, docGenerator) - { - } - - public override IEnumerable GenerateCode() - { - foreach (var kv in document.Paths) - { - foreach (var operations in kv.Value) - { - var operation = operations.Value; - - if (!settings.GenerateDeprecatedOperations && operation.IsDeprecated) - { - continue; - } - - var returnType = GetTypeName(operation); - var verb = operations.Key.CapitalizeFirstCharacter(); - var methodName = !string.IsNullOrWhiteSpace(settings.OperationNameTemplate) - ? settings.OperationNameTemplate!.Replace("{operationName}", "Execute") - : "Execute"; - var code = new StringBuilder(); - - this.docGenerator.AppendInterfaceDocumentationByEndpoint(operation, code); - - var interfaceName = GetInterfaceName(kv, verb, operation); - code.AppendLine($$""" - {{GenerateInterfaceDeclaration(interfaceName)}} - {{Separator}}{ - """); - - var operationModel = generator.CreateOperationModel(operation); - var dynamicQuerystringParameterType = interfaceName.Substring(1).Replace("Endpoint", "QueryParams"); - var parameters = ParameterExtractor.GetParameters(operationModel, operation, settings, dynamicQuerystringParameterType, out var methodDynamicQuerystringParameters).ToList(); - - var hasDynamicQuerystringParameter = !string.IsNullOrWhiteSpace(methodDynamicQuerystringParameters); - if (hasDynamicQuerystringParameter) - yield return new GeneratedCode( - dynamicQuerystringParameterType, - methodDynamicQuerystringParameters!); - - var parametersString = string.Join(", ", parameters); - var hasApizrRequestOptionsParameter = settings.ApizrSettings?.WithRequestOptions == true; - var hasCancellationToken = settings.UseCancellationTokens && !hasApizrRequestOptionsParameter; - - this.docGenerator.AppendMethodDocumentation(operationModel, IsApiResponseType(returnType), hasDynamicQuerystringParameter, hasApizrRequestOptionsParameter, hasCancellationToken, code); - GenerateObsoleteAttribute(operation, code); - GenerateForMultipartFormData(operationModel, code); - GenerateHeaders(operations, operationModel, code); - - code.AppendLine($"{Separator}{Separator}[{verb}(\"{kv.Key}\")]") - .AppendLine($"{Separator}{Separator}{returnType} {methodName}({parametersString});") - .AppendLine($"{Separator}}}"); - - if (parametersString.Contains("?") && settings is { OptionalParameters: true, ApizrSettings: not null }) - { - this.docGenerator.AppendMethodDocumentation(operationModel, IsApiResponseType(returnType), false, hasApizrRequestOptionsParameter, hasCancellationToken, code); - GenerateObsoleteAttribute(operation, code); - GenerateForMultipartFormData(operationModel, code); - GenerateHeaders(operations, operationModel, code); - - parametersString = string.Join(", ", parameters.Where(parameter => !parameter.Contains("?"))); - - code.AppendLine($"{Separator}{Separator}[{verb}(\"{kv.Key}\")]") - .AppendLine($"{Separator}{Separator}{returnType} {methodName}({parametersString});") - .AppendLine(); - } - - yield return new GeneratedCode(interfaceName, code.ToString()); - } - } - } - - private string GetInterfaceName( - KeyValuePair kv, - string verb, - OpenApiOperation operation) - { - var name = IdentifierUtils.Counted( - knownIdentifiers, - "I" + generator - .BaseSettings - .OperationNameGenerator - .GetOperationName(document, kv.Key, verb, operation).CapitalizeFirstCharacter(), - suffix: "Endpoint"); - knownIdentifiers.Add(name); - return name; - } - - private string GenerateInterfaceDeclaration(string name) - { - var modifier = settings.TypeAccessibility.ToString().ToLowerInvariant(); - return $""" - {Separator}{GetGeneratedCodeAttribute()} - {Separator}{modifier} partial interface {name} - """; - } -} diff --git a/src/Refitter.Core/SingleInterfacePartitioning.cs b/src/Refitter.Core/SingleInterfacePartitioning.cs new file mode 100644 index 000000000..b989278a4 --- /dev/null +++ b/src/Refitter.Core/SingleInterfacePartitioning.cs @@ -0,0 +1,40 @@ +using System.Text; +using NSwag; + +namespace Refitter.Core; + +internal class SingleInterfacePartitioning( + RefitGeneratorSettings settings) : IInterfacePartitioning +{ + + public string GetGroupKey(OpenApiOperationInfo operation) => string.Empty; + + public string GetInterfaceName(string groupKey, string title, string baseOperationName) => + $"I{title.CapitalizeFirstCharacter()}".Sanitize(); + + public string GetInterfaceNameSuffix() => string.Empty; + + public string GetMethodName(OpenApiOperationInfo operation, string interfaceName, string baseOperationName) + { + var methodName = baseOperationName; + if (settings.OperationNameTemplate?.Contains("{operationName}") ?? false) + { + methodName = settings.OperationNameTemplate!.Replace("{operationName}", methodName); + } + + return methodName; + } + + public string GetDynamicQuerystringParameterType(string interfaceName, string methodName) => + methodName + "QueryParams"; + + public bool IsSingleInterface => true; + + public void AppendInterfaceDocumentation( + OpenApiDocument document, + XmlDocumentationGenerator docGenerator, + string groupKey, + OpenApiOperationInfo representativeOperation, + StringBuilder code) => + docGenerator.AppendSingleInterfaceDocumentation(document, code); +} diff --git a/src/Refitter.Core/XmlDocumentationGenerator.cs b/src/Refitter.Core/XmlDocumentationGenerator.cs index a233e594f..2753a1ce3 100644 --- a/src/Refitter.Core/XmlDocumentationGenerator.cs +++ b/src/Refitter.Core/XmlDocumentationGenerator.cs @@ -10,19 +10,8 @@ namespace Refitter.Core; /// public class XmlDocumentationGenerator { - /// - /// The global code generation settings. - /// - private readonly RefitGeneratorSettings _settings; - - /// - /// The whitespace to use for a single level of indentation. - /// + private readonly RefitGeneratorSettings settings; private const string Separator = " "; - - /// - /// The name of the XML documentation tag used for summaries. - /// private const string SummaryTag = "summary"; /// @@ -31,7 +20,7 @@ public class XmlDocumentationGenerator /// The code generation settings to use. internal XmlDocumentationGenerator(RefitGeneratorSettings settings) { - this._settings = settings; + this.settings = settings; } /// @@ -42,7 +31,7 @@ internal XmlDocumentationGenerator(RefitGeneratorSettings settings) /// The builder to append the documentation to. public void AppendInterfaceDocumentationByTag(OpenApiDocument document, string tag, StringBuilder code) { - if (!_settings.GenerateXmlDocCodeComments) + if (!settings.GenerateXmlDocCodeComments) { return; } @@ -60,7 +49,7 @@ public void AppendInterfaceDocumentationByTag(OpenApiDocument document, string t /// The builder to append the documentation to. public void AppendInterfaceDocumentationByEndpoint(OpenApiOperation endpoint, StringBuilder code) { - if (!_settings.GenerateXmlDocCodeComments) + if (!settings.GenerateXmlDocCodeComments) { return; } @@ -80,7 +69,7 @@ public void AppendInterfaceDocumentationByEndpoint(OpenApiOperation endpoint, St /// The builder to append the documentation to. public void AppendSingleInterfaceDocumentation(OpenApiDocument document, StringBuilder code) { - if (!_settings.GenerateXmlDocCodeComments) + if (!settings.GenerateXmlDocCodeComments) { return; } @@ -109,7 +98,7 @@ public void AppendMethodDocumentation( bool hasCancellationToken, StringBuilder code) { - if (!_settings.GenerateXmlDocCodeComments) + if (!settings.GenerateXmlDocCodeComments) return; if (!string.IsNullOrWhiteSpace(method.Summary)) @@ -309,7 +298,7 @@ private string BuildResponseDescription(string text, IEnumerable { internal const string GeneratedFileMarker = "GeneratedFile: "; - private RefitGeneratorSettings? _cachedSettings; - private IGenerationReporter _reporter = new RichGenerationReporter(); + private RefitGeneratorSettings? cachedSettings; + private IGenerationReporter reporter = new RichGenerationReporter(); private static IGenerationReporter CreateReporter(Settings settings) => settings.SimpleOutput @@ -34,7 +34,7 @@ protected override ValidationResult Validate(CommandContext context, Settings se var validationResult = SettingsValidator.Validate(settings, out var refitSettings); if (refitSettings != null) { - _cachedSettings = refitSettings; + cachedSettings = refitSettings; } // Detect conflict: both CLI and settings file specify non-default jsonLibraryVersion @@ -54,15 +54,15 @@ protected override async Task ExecuteAsync(CommandContext context, Settings { RefitGeneratorSettings refitGeneratorSettings; - _reporter = CreateReporter(settings); + reporter = CreateReporter(settings); try { // Use cached settings from Validate() if available - if (_cachedSettings != null) + if (cachedSettings != null) { - refitGeneratorSettings = _cachedSettings; - _cachedSettings = null; // Clear cache after use + refitGeneratorSettings = cachedSettings; + cachedSettings = null; // Clear cache after use } else if (!string.IsNullOrWhiteSpace(settings.SettingsFilePath)) { @@ -87,14 +87,14 @@ protected override async Task ExecuteAsync(CommandContext context, Settings version += " (local build)"; // Header with branding - _reporter.ReportHeader(version); + reporter.ReportHeader(version); // Support information var supportKey = settings.NoLogging ? "Unavailable when logging is disabled" : SupportInformation.GetSupportKey(); - _reporter.ReportSupportKey(supportKey); + reporter.ReportSupportKey(supportKey); if (context.Arguments.Any(a => a.Equals("--version", StringComparison.OrdinalIgnoreCase)) || context.Arguments.Any(a => a.Equals("-v", StringComparison.OrdinalIgnoreCase))) @@ -114,13 +114,13 @@ protected override async Task ExecuteAsync(CommandContext context, Settings if (refitGeneratorSettings.OpenApiPaths == null || refitGeneratorSettings.OpenApiPaths.Length == 0) { var specPath = refitGeneratorSettings.OpenApiPath!; - await ValidateOpenApiSpec(specPath, _reporter); + await ValidateOpenApiSpec(specPath, reporter); } else { foreach (var specPath in refitGeneratorSettings.OpenApiPaths) { - await ValidateOpenApiSpec(specPath, _reporter); + await ValidateOpenApiSpec(specPath, reporter); } } } @@ -140,16 +140,16 @@ protected override async Task ExecuteAsync(CommandContext context, Settings if (refitGeneratorSettings.IncludePathMatches.Length > 0 && generator.OpenApiDocument.Paths.Count == 0) { - _reporter.ReportAllPathsFilteredWarning(refitGeneratorSettings.IncludePathMatches); + reporter.ReportAllPathsFilteredWarning(refitGeneratorSettings.IncludePathMatches); } // Success summary with performance metrics stopwatch.Stop(); - _reporter.ReportSuccess(stopwatch.Elapsed, refitGeneratorSettings.GenerateMultipleFiles); + reporter.ReportSuccess(stopwatch.Elapsed, refitGeneratorSettings.GenerateMultipleFiles); if (!settings.NoBanner) { - _reporter.ReportDonationBanner(); + reporter.ReportDonationBanner(); } ShowWarnings(refitGeneratorSettings, settings); @@ -158,24 +158,24 @@ protected override async Task ExecuteAsync(CommandContext context, Settings catch (Exception exception) { // Error summary panel - _reporter.ReportGenerationFailed(); + reporter.ReportGenerationFailed(); if (exception is OpenApiUnsupportedSpecVersionException unsupportedSpecVersionException) { - _reporter.ReportUnsupportedVersion(unsupportedSpecVersionException.SpecificationVersion); + reporter.ReportUnsupportedVersion(unsupportedSpecVersionException.SpecificationVersion); } if (exception is not OpenApiValidationException) { - _reporter.ReportExceptionDetails(exception); + reporter.ReportExceptionDetails(exception); } if (!settings.SkipValidation) { - _reporter.ReportSkipValidationSuggestion(); + reporter.ReportSkipValidationSuggestion(); } - _reporter.ReportSupportHelp(); + reporter.ReportSupportHelp(); await Analytics.LogError(exception, settings); return exception.HResult; @@ -244,7 +244,7 @@ private async Task WriteSingleFile( Settings settings, RefitGeneratorSettings refitGeneratorSettings) { - await _reporter.ReportSingleFileGenerationProgressAsync(); + await reporter.ReportSingleFileGenerationProgressAsync(); var code = generator.Generate().ReplaceLineEndings(); var planned = OutputPlanner.PlanSingleFile(settings, refitGeneratorSettings, code); @@ -254,10 +254,10 @@ private async Task WriteSingleFile( var sizeFormatted = FormatFileSize(code.Length); var lines = code.Split('\n').Length; - _reporter.ReportSingleFileOutput(fileName, directory, sizeFormatted, lines); + reporter.ReportSingleFileOutput(fileName, directory, sizeFormatted, lines); await FileWriter.WriteAsync(planned); - _reporter.ReportFileWritten(planned.Path); + reporter.ReportFileWritten(planned.Path); } private static string FormatFileSize(long bytes) @@ -279,13 +279,13 @@ private async Task WriteMultipleFiles( Settings settings, RefitGeneratorSettings refitGeneratorSettings) { - var generatorOutput = await _reporter.GenerateMultipleFilesWithProgressAsync(generator.GenerateMultipleFiles); + var generatorOutput = await reporter.GenerateMultipleFilesWithProgressAsync(generator.GenerateMultipleFiles); var planned = OutputPlanner.PlanMultipleFiles(settings, refitGeneratorSettings, generatorOutput); var totalSize = 0L; var totalLines = 0; - var report = _reporter.BeginMultiFileOutput(); + var report = reporter.BeginMultiFileOutput(); for (var i = 0; i < generatorOutput.Files.Count; i++) { @@ -302,7 +302,7 @@ private async Task WriteMultipleFiles( totalLines += lines; await FileWriter.WriteAsync(plannedFile); - _reporter.ReportFileWritten(plannedFile.Path); + reporter.ReportFileWritten(plannedFile.Path); } report.Complete(generatorOutput.Files.Count, FormatFileSize(totalSize), totalLines); @@ -333,7 +333,7 @@ private void ShowWarnings(RefitGeneratorSettings refitGeneratorSettings, Setting if (warnings.Any()) { - _reporter.ReportConfigurationWarnings(warnings); + reporter.ReportConfigurationWarnings(warnings); } } private static string GetOutputPath(Settings settings, RefitGeneratorSettings refitGeneratorSettings) => diff --git a/test/OpenAPI/v3.0/bot.components.yaml b/test/OpenAPI/v3.0/bot.components.yaml deleted file mode 100644 index ef9527418..000000000 --- a/test/OpenAPI/v3.0/bot.components.yaml +++ /dev/null @@ -1,818 +0,0 @@ -openapi: 3.0.3 -info: - title: Bot OpenAPI - version: v1.0.0 -paths: - # 这里只是放置个 path 配置避免格式检查报错 -# 对象定义 -components: - # path 参数统一定义在这里 - parameters: - PathGuildID: - name: guild_id - description: 频道ID - in: path - required: true - schema: - $ref: '#/components/schemas/BaseGuildID' - PathChannelID: - name: channel_id - description: 子频道ID - in: path - required: true - schema: - $ref: '#/components/schemas/BaseChannelID' - PathUserID: - name: user_id - description: 成员ID - in: path - required: true - schema: - $ref: '#/components/schemas/BaseUserID' - PathRoleID: - name: role_id - description: 身份组ID - in: path - required: true - schema: - $ref: '#/components/schemas/BaseRoleID' - PathMessageID: - name: message_id - description: 消息ID - in: path - required: true - schema: - $ref: '#/components/schemas/BaseMessageID' - PathScheduleID: - name: schedule_id - description: 日程ID - in: path - required: true - schema: - $ref: '#/components/schemas/BaseScheduleID' - schemas: - BaseGuildID: - type: string - format: int64 - description: 频道ID - BaseChannelID: - type: string - format: int64 - description: 子频道ID - BaseChannelCategoryID: - type: string - format: int64 - description: 子频道分组ID,仅子频道支持分组 - BaseUserID: - type: string - format: int64 - description: 用户ID - BaseRoleID: - type: string - format: int64 - description: 身份组ID - BaseMessageID: - type: string - description: 消息ID - BaseSequenceInChannel: - type: string - description: | - 子频道消息 seq,用于消息间的排序,seq 在同一子频道中按从先到后的顺序递增,不同的子频道之间消息无法排序 - BaseScheduleID: - type: string - format: int64 - description: 日程ID - Guild: - description: | - 频道对象,频道对象中所涉及的 ID 类数据,都仅在机器人场景流通,与真实的 ID 无关。 - 请不要理解为真实的 ID - type: object - properties: - id: - $ref: '#/components/schemas/BaseGuildID' - name: - type: string - description: 频道名称 - icon: - type: string - description: 频道头像地址 - owner_id: - $ref: '#/components/schemas/BaseUserID' - owner: - type: boolean - description: 当前人是否是创建人 - member_count: - type: integer - description: 成员数 - format: int32 - max_members: - type: integer - description: 最大成员数 - format: int32 - description: - type: string - description: 描述 - joined_at: - type: string - description: 加入时间 - User: - description: | - 用户对象中所涉及的 ID 类数据,都仅在机器人场景流通,与真实的 ID 无关。 - 请不要理解为真实的 ID - type: object - properties: - id: - $ref: '#/components/schemas/BaseUserID' - username: - type: string - description: 用户名 - avatar: - type: string - description: 用户头像地址 - bot: - type: boolean - description: 是否是机器人 - union_openid: - type: string - description: | - 特殊关联应用的 openid,需要特殊申请并配置后才会返回。如需申请,请联系平台运营人员 - union_user_account: - type: string - description: | - 机器人关联的互联应用的用户信息,与union_openid关联的应用是同一个。 - 如需申请,请联系平台运营人员 - Channel: - description: 子频道对象,子频道对象中所涉及的 ID 类数据,都仅在机器人场景流通,与真实的 ID 无关。请不要理解为真实的 ID - type: object - properties: - id: - $ref: '#/components/schemas/BaseChannelID' - guild_id: - $ref: '#/components/schemas/BaseGuildID' - name: - type: string - description: 子频道名称 - type: - $ref: '#/components/schemas/ChannelType' - sub_type: - $ref: '#/components/schemas/ChannelSubType' - position: - type: integer - description: 排序值 - parent_id: - type: string - description: 所属分组 id,仅对子频道有效,对 子频道分组(ChannelType=4) 无效 - owner_id: - $ref: '#/components/schemas/BaseUserID' - private_type: - $ref: '#/components/schemas/PrivateType' - speak_permission: - $ref: '#/components/schemas/SpeakPermission' - application_id: - $ref: '#/components/schemas/Application' - Member: - description: 成员对象 - type: object - properties: - user: - $ref: '#/components/schemas/User' - nick: - type: string - description: 用户的昵称 - roles: - type: array - items: - $ref: '#/components/schemas/BaseRoleID' - description: 用户在频道内的身份组ID - joined_at: - type: string - format: date-time - description: '用户加入频道的时间, ISO8601格式' - Role: - type: object - properties: - id: - $ref: '#/components/schemas/BaseRoleID' - name: - type: string - description: 名称 - color: - type: integer - description: ARGB的HEX十六进制颜色值转换后的十进制数值 - hoist: - type: integer - description: '是否在成员列表中单独展示: 0-否, 1-是' - number: - type: integer - description: 人数 - member_limit: - type: integer - description: 成员上限 - format: uint32 - description: 频道身份组对象 - ChannelPermissions: - description: | - 子频道权限对象, 权限是QQ频道管理频道成员的一种方式,管理员可以对不同的人、不同的子频道设置特定的权限。 - * 用户的权限包括个人权限和身份组权限两部分,最终生效是取两种权限的并集。 - * 权限使用位图表示,传递时序列化为十进制数值字符串。如权限值为0x6FFF,会被序列化为十进制'28671' - type: object - properties: - channel_id: - $ref: '#/components/schemas/BaseChannelID' - user_id: - $ref: '#/components/schemas/BaseUserID' - role_id: - $ref: '#/components/schemas/BaseRoleID' - permissions: - $ref: '#/components/schemas/Permissions' - Message: - type: object - description: 消息对象 - properties: - id: - $ref: '#/components/schemas/BaseMessageID' - channel_id: - $ref: '#/components/schemas/BaseChannelID' - guild_id: - $ref: '#/components/schemas/BaseGuildID' - content: - type: string - description: 消息内容 - timestamp: - type: string - format: date-time - description: '消息创建时间,ISO8601 timestamp' - edited_timestamp: - type: string - format: date-time - description: '消息编辑时间,ISO8601 timestamp' - mention_everyone: - type: boolean - description: 是否是@全员消息 - author: - $ref: '#/components/schemas/User' - attachments: - type: array - description: 附件 - items: - $ref: '#/components/schemas/MessageAttachment' - embeds: - type: array - description: embed - items: - $ref: '#/components/schemas/MessageEmbed' - mentions: - type: array - description: 消息中@的人 - items: - $ref: '#/components/schemas/User' - member: - $ref: '#/components/schemas/Member' - ark: - $ref: '#/components/schemas/MessageArk' - seq: - type: integer - description: | - 用于消息间的排序,seq 在同一子频道中按从先到后的顺序递增,不同的子频道之前消息无法排序 - seq_in_channel: - $ref: "#/components/schemas/BaseSequenceInChannel" - message_reference: - $ref: "#/components/schemas/MessageReference" - MessageReference: - type: object - description: 引用消息对象 - properties: - message_id: - $ref: '#/components/schemas/BaseMessageID' - ignore_get_message_error: - type: boolean - description: 是否忽略获取引用消息详情错误,默认否,仅在发送引用消息的时候使用 - MessageAttachment: - type: object - description: 消息附件 - properties: - url: - type: string - description: 下载地址 - MessageEmbed: - type: object - description: embed消息 - properties: - title: - type: string - description: 标题 - prompt: - type: string - description: 消息弹窗内容 - thumbnail: - $ref: '#/components/schemas/MessageEmbedThumbnail' - fields: - type: array - description: embed 字段数据 - items: - $ref: '#/components/schemas/MessageEmbedField' - MessageEmbedThumbnail: - type: object - description: 消息封面 - properties: - url: - type: string - description: 图片地址 - MessageEmbedField: - type: object - description: embed字段 - properties: - name: - type: string - description: 字段 - MessageArk: - type: object - description: ark消息 - properties: - template_id: - type: integer - description: ark模板id(需要先申请) - kv: - type: array - description: kv值列表 - items: - $ref: '#/components/schemas/MessageArkKv' - MessageArkKv: - description: ARK消息KV - type: object - properties: - key: - type: string - description: key - value: - type: string - description: value - obj: - type: array - description: ark obj类型的列表 - items: - $ref: '#/components/schemas/MessageArkObj' - MessageArkObj: - type: object - description: MessageArkObj - properties: - obj_kv: - type: array - description: ark objkv列表 - items: - $ref: '#/components/schemas/MessageArkObjKv' - MessageArkObjKv: - type: object - description: MessageArkObjKv - properties: - key: - type: string - description: key - value: - type: string - description: value - MessageAudited: - type: object - description: 消息审核对象 - properties: - audit_id: - type: string - description: 消息审核 id - message_id: - type: string - description: 消息 id,只有审核通过事件才会有值 - guild_id: - type: string - description: 频道 id - channel_id: - type: string - description: 子频道 id - audit_time: - type: string - format: date-time - description: '消息审核时间,ISO8601 timestamp' - create_time: - type: string - format: date-time - description: '消息创建时间,ISO8601 timestamp' - seq_in_channel: - $ref: "#/components/schemas/BaseSequenceInChannel" - DMS: - type: object - description: 私信会话对象 - properties: - guild_id: - type: string - description: 私信会话关联的频道ID - format: int64 - channel_id: - type: string - description: 私信会话关联的子频道ID - create_time: - type: string - format: int64 - description: 创建私信会话时间戳 - Announces: - type: object - description: 公告对象 - properties: - guild_id: - $ref: '#/components/schemas/BaseGuildID' - channel_id: - $ref: '#/components/schemas/BaseChannelID' - message_id: - $ref: '#/components/schemas/BaseMessageID' - Schedule: - type: object - description: 日程对象 - properties: - id: - $ref: '#/components/schemas/BaseScheduleID' - name: - type: string - description: 日程名称 - description: - type: string - description: 日程描述 - start_timestamp: - type: string - description: 日程开始时间戳(ms) - format: int64 - end_timestamp: - type: string - description: 日程结束时间戳(ms) - format: int64 - creator: - $ref: '#/components/schemas/Member' - jump_channel_id: - description: 日程开始时跳转到的子频道 id - type: string - format: int64 - remind_type: - $ref: '#/components/schemas/RemindType' - Emoji: - type: object - description: 表情对象 - properties: - id: - $ref: '#/components/schemas/EmojiID' - type: - $ref: '#/components/schemas/EmojiType' - MessageReaction: - type: object - description: 表情表态对象 - properties: - user_id: - $ref: '#/components/schemas/BaseUserID' - guild_id: - $ref: '#/components/schemas/BaseGuildID' - channel_id: - $ref: '#/components/schemas/BaseChannelID' - target: - $ref: '#/components/schemas/ReactionTarget' - emoji: - $ref: '#/components/schemas/Emoji' - ReactionTarget: - type: object - description: 表情表态的目标对象 - properties: - id: - type: string - description: 表态对象ID - type: - $ref: '#/components/schemas/ReactionTargetType' - AudioControl: - type: object - description: 语音对象 - properties: - audio_url: - type: string - description: 音频数据的url status为0时传 - text: - type: string - description: 状态文本(比如:简单爱-周杰伦),可选,status为0时传,其他操作不传 - status: - $ref: '#/components/schemas/AudioControlStatus' - APIPermission: - type: object - description: 接口权限对象 - properties: - path: - type: string - description: 'API 接口名,例如 /guilds/{guild_id}/members/{user_id}' - method: - type: string - description: 请求方法,例如 GET - desc: - type: string - description: API 接口名称,例如 获取频道信 - auth_status: - type: integer - description: 授权状态,auth_stats 为 1 时已授权 - APIPermissionDemand: - type: object - description: 接口权限需求对象,用于往目标频道的子频道发送申请接口权限消息 - properties: - guild_id: - $ref: '#/components/schemas/BaseGuildID' - channel_id: - $ref: '#/components/schemas/BaseChannelID' - api_identify: - $ref: '#/components/schemas/APIPermissionDemandIdentify' - title: - type: string - description: 接口权限链接中的接口权限描述信息 - desc: - type: string - description: 接口权限链接中的机器人可使用功能的描述信息 - APIPermissionDemandIdentify: - type: object - description: 接口权限需求标识对象 - properties: - path: - type: string - description: 'API 接口名,例如 /guilds/{guild_id}/members/{user_id}' - name: - type: string - description: 请求方法,例如 GET - ChannelCreate: - type: object - description: 创建子频道请求对象 - properties: - name: - type: string - description: 子频道名称 - type: - $ref: '#/components/schemas/ChannelType' - sub_type: - $ref: '#/components/schemas/ChannelSubType' - position: - type: integer - description: 排序值 - parent_id: - $ref: '#/components/schemas/BaseChannelCategoryID' - private_type: - $ref: '#/components/schemas/PrivateType' - private_user_ids: - type: array - items: - $ref: '#/components/schemas/BaseUserID' - description: 子频道私密类型成员 ID - required: - - name - - type - - sub_type - ChannelUpdate: - type: object - description: 修子频道请求对象 - properties: - name: - type: string - description: 子频道名称 - type: - $ref: '#/components/schemas/ChannelType' - sub_type: - $ref: '#/components/schemas/ChannelSubType' - position: - type: integer - description: 排序值 - parent_id: - $ref: '#/components/schemas/BaseChannelCategoryID' - private_type: - $ref: '#/components/schemas/PrivateType' - GuildRole: - type: object - description: 频道身份组对象 - properties: - id: - $ref: '#/components/schemas/BaseGuildID' - name: - type: string - description: 名称 - color: - type: number - description: ARGB 的 HEX 十六进制颜色值转换后的十进制数值(例:4294927682) - hoist: - type: number - description: 是否在成员列表中单独展示, 0-否, 1-是 - number: - type: number - description: 人数 - member_limit: - type: number - description: 成员上限 - MessageSend: - type: object - description: 用户发送消息的数据对象 - properties: - content: - type: string - description: 消息内容,文本内容,支持内嵌格式 - embed: - $ref: "#/components/schemas/MessageEmbed" - ark: - $ref: "#/components/schemas/MessageArk" - message_reference: - $ref: "#/components/schemas/MessageReference" - image: - type: string - description: 图片url地址,平台会转存该图片,用于下发图片消息 - msg_id: - type: string - description: 要回复的消息id(Message.id), 在 AT_CREATE_MESSAGE 事件中获取,携带之后此条回复视为被动消息 - minProperties: 1 - ScheduleCreate: - type: object - description: 日程创建对象 - properties: - name: - type: string - description: 日程名称 - description: - type: string - description: 日程描述 - start_timestamp: - type: string - description: 日程开始时间戳(ms) - format: int64 - end_timestamp: - type: string - description: 日程结束时间戳(ms) - format: int64 - creator: - $ref: '#/components/schemas/Member' - jump_channel_id: - type: string - description: 日程开始时跳转到的子频道 id - format: int64 - remind_type: - type: string - description: 日程提醒类型 - required: - - name - - start_timestamp - - end_timestamp - - remind_type - ScheduleUpdate: - type: object - description: 日程更新对象 - properties: - name: - type: string - description: 日程名称 - description: - type: string - description: 日程描述 - start_timestamp: - type: string - description: 日程开始时间戳(ms) - format: int64 - end_timestamp: - type: string - description: 日程结束时间戳(ms) - format: int64 - creator: - $ref: '#/components/schemas/Member' - jump_channel_id: - type: string - description: 日程开始时跳转到的子频道 id - format: int64 - remind_type: - type: string - description: 日程提醒类型 - SessionStartLimit: - type: object - description: 创建 Session 限制信息 - properties: - total: - type: integer - description: 每 24 小时可创建 Session 数 - remaining: - type: integer - description: 目前还可以创建的 Session 数 - reset_after: - type: integer - description: 重置计数的剩余时间(ms) - max_concurrency: - type: integer - description: 每 5s 可以创建的 Session 数 - DefaultRoleIDs: - type: string - description: | - 统默认生成下列身份组ID: - * `1` - 全体成员 - * `2` - 管理员 - * `4` - 群主/创建者 - * `5` - 子频道管理员 - enum: [1,2,4,5] - ChannelType: - type: integer - description: | - 子频道类型: - * `0` - 文字子频道 - * `1` - 保留,不可用 - * `2` - 语音子频道 - * `3` - 保留,不可用 - * `4` - 子频道分组 - * `10005` - 直播子频道 - * `10006` - 应用子频道 - * `10007` - 论坛子频道 - | 注:由于QQ频道还在持续的迭代中,经常会有新的子频道类型增加,文档更新不一定及时,开发者识别 ChannelType 时,请注意相关的未知 ID 的处理。 - enum: [0,1,2,3,4,10005,10006,10007] - ChannelSubType: - type: integer - description: | - 子频道子类型: - * `0` - 闲聊 - * `1` - 公告 - * `2` - 攻略 - * `3` - 开黑 - | 目前只有文字子频道具有 ChannelSubType 二级分类,同时二级分类也可能会不断增加,开发者也需要注意对未知 ID 的处理 - enum: [0,1,2,3] - PrivateType: - type: integer - description: | - 子频道私密类型: - * `0` - 公开频道 - * `1` - 群主管理员可见 - * `2` - 群主管理员+指定成员,可使用 修改子频道权限接口 指定成员 - enum: [0,1,2] - SpeakPermission: - type: integer - description: | - 子频道发言权限: - * `0` - 无效类型 - * `1` - 所有人 - * `2` - 群主管理员+指定成员,可使用 修改子频道权限接口 指定成员 - enum: [0,1,2] - Application: - type: string - description: | - 应用子频道的应用类型: - * `1000000` - 王者开黑大厅 - * `1000001` - 互动小游戏 - * `1000010` - 腾讯投票 - * `1000051` - 飞车开黑大厅 - * `1000050` - 日程提醒 - * `1000070` - CoDM 开黑大厅 - * `1010000` - 和平精英开黑大厅 - | 由于QQ频道还在持续的迭代中,应用子频道的 application_id 还会持续新增,后续会不定期补充到文档中 - Permissions: - type: string - description: | - * 用户/角色拥有的(子)频道权限 - * 权限是QQ频道管理频道成员的一种方式,管理员可以对不同的人、不同的子频道设置特定的权限。用户的权限包括个人权限和身份组权限两部分,最终生效是取两种权限的并集。 - * 权限使用位图表示,传递时序列化为十进制数值字符串。如权限值为0x6FFF,会被序列化为十进制"28671"。 - | 权限 | 值 | 描述 | - | --- | --- | --- | - | 可查看子频道 | 0x0000000001 (1 << 0) | 支持`指定成员`可见类型,支持`身份组`可见类型 | - | 可管理子频道 | 0x0000000002 (1 << 1) | 创建者、管理员、子频道管理员都具有此权限 | - | 可发言子频道 | 0x0000000004 (1 << 2) | 支持`指定成员`发言类型,支持`身份组`发言类型 | - RemindType: - type: string - description: | - 日程提醒类型: - * `0` - 不提醒 - * `1` - 开始时提醒 - * `2` - 开始前 5 分钟提醒 - * `3` - 开始前 15 分钟提醒 - * `4` - 开始前 30 分钟提醒 - * `5` - 开始前 60 分钟提醒 - AudioControlStatus: - type: integer - description: | - 播放状态: - * `0` - 开始播放操作 - * `1` - 暂停播放操作 - * `2` - 继续播放操作 - * `3` - 停止播放操作 - ReactionTargetType: - type: string - description: | - 表态对象类型: - * `0` - 消息 - * `1` - 帖子 - * `2` - 评论 - * `3` - 回复 - EmojiType: - type: integer - description: | - 表情类型 : - * `1` - 系统表情 - * `2` - emoji表情 - EmojiID: - type: string - description: | - emoji 表情列表,请访问 [表情列表](https://bot.q.qq.com/wiki/develop/api/openapi/emoji/model.html#emoji-%E5%88%97%E8%A1%A8) - PinsMessage: - type: object - description: 精华消息对象 - properties: - guild_id: - $ref: '#/components/schemas/BaseGuildID' - channel_id: - $ref: '#/components/schemas/BaseChannelID' - message_ids: - type: array - items: - $ref: '#/components/schemas/BaseMessageID' diff --git a/test/OpenAPI/v3.0/bot.paths.yaml b/test/OpenAPI/v3.0/bot.paths.yaml deleted file mode 100644 index c0c47c86f..000000000 --- a/test/OpenAPI/v3.0/bot.paths.yaml +++ /dev/null @@ -1,1157 +0,0 @@ -openapi: 3.0.3 -info: - title: Bot OpenAPI - description: | - QQ 频道机器人通过开放的平台承载机器人的定制化功能,让开发者获得更畅快的开发体验。 - version: v1.0.0 - contact: - name: 加入开发者频道 - url: https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&inviteCode=1MVRbV&from=246610&biz=ka -servers: - - url: https://sandbox.api.sgroup.qq.com - description: | - 沙箱环境 - 沙箱环境调用 openapi 仅能操作沙箱频道 - - url: https://api.sgroup.qq.com - description: 正式环境 -security: - - bot_auth: [] -paths: - /guilds/{guild_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - get: - summary: 获取频道详情 - description: 用于获取 guild_id 指定的频道的详情 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/guild/get_guild.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Guild' - tags: - - GuildManagements - /users/@me: - get: - summary: 获取用户详情 - description: 用于获取当前用户(机器人)详情 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/user/me.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/User' - tags: - - UserRelations - /users/@me/guilds: - parameters: - - name: before - in: query - required: false - schema: - type: string - description: 读此 guild id 之前的数据,before 设置时, 先反序,再分页 - - name: after - in: query - required: false - schema: - type: string - description: 读取此 id 之后的数据 - - name: limit - in: query - required: false - schema: - type: number - default: 100 - description: 每次拉取多少条数据,最大不超过 100,默认 100 - get: - summary: 获取用户频道列表 - description: | - 用于获取当前用户(机器人)所加入的频道列表,支持分页,参数 before、after 同时存在时,以 before 为准。 - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/user/guilds.html' - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/Guild' - tags: - - UserRelations - /guilds/{guild_id}/channels: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - get: - summary: 获取子频道列表 - description: 用于获取 guild_id 指定的频道下的子频道列表 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/channel/get_channels.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/Channel' - tags: - - GuildManagements - post: - summary: 创建子频道 - description: | - 用于在 guild_id 指定的频道下创建一个子频道。 - * 要求操作人具有管理频道的权限,如果是机器人,则需要将机器人设置为管理员。 - * 创建成功后,返回创建成功的子频道对象,同时会触发一个频道创建的事件通知。 - * `公域机器人暂不支持申请,仅私域机器人可用,选择私域机器人后默认开通。` - * `注意: 开通后需要先将机器人从频道移除,然后重新添加,方可生效。` - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/channel/post_channels.html - requestBody: - required: true - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/ChannelCreate' - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/Channel' - tags: - - GuildManagements - /channels/{channel_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - get: - summary: 获取子频道详情 - description: 用于获取 channel_id 指定的子频道的详情 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/channel/get_channel.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Channel' - tags: - - GuildManagements - patch: - summary: 修改子频道 - description: | - 用于修改 channel_id 指定的子频道的信息。 - * 要求操作人具有管理子频道的权限,如果是机器人,则需要将机器人设置为管理员。 - * 修改成功后,会触发子频道更新事件。 - * `公域机器人暂不支持申请,仅私域机器人可用,选择私域机器人后默认开通。` - * `注意: 开通后需要先将机器人从频道移除,然后重新添加,方可生效。` - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/channel/patch_channel.html - requestBody: - required: true - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/ChannelUpdate' - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Channel' - tags: - - GuildManagements - delete: - summary: 删除子频道 - description: | - 用于删除 channel_id 指定的子频道。 - * 要求操作人具有管理子频道的权限,如果是机器人,则需要将机器人设置为管理员。 - * 修改成功后,会触发子频道删除事件。 - * `公域机器人暂不支持申请,仅私域机器人可用,选择私域机器人后默认开通。` - * `注意: 开通后需要先将机器人从频道移除,然后重新添加,方可生效。` - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/channel/delete_channel.html' - responses: - 200: - description: 成功 - tags: - - GuildManagements - /guilds/{guild_id}/members: - get: - summary: 获取频道成员列表 - description: | - 用于获取 guild_id 指定的频道中所有成员的详情列表,支持分页。 - * `公域机器人暂不支持申请,仅私域机器人可用,选择私域机器人后默认开通。` - * `注意: 开通后需要先将机器人从频道移除,然后重新添加,方可生效。` - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/member/get_members.html - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - - name: after - in: query - required: false - schema: - type: string - description: 上一次回包中最后一个member的user id, 如果是第一次请求填 0,默认为 0 - - name: limit - in: query - required: false - schema: - type: number - default: 1 - description: 分页大小,1-400,默认是 1。成员较多的频道尽量使用较大的limit值,以减少请求数 - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/Member' - tags: - - UserRelations - /guilds/{guild_id}/members/{user_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - - $ref: 'bot.components.yaml#/components/parameters/PathUserID' - get: - summary: 获取成员详情 - description: 用于获取 guild_id 指定的频道中 user_id 对应成员的详细信息 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/member/get_member.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Member' - tags: - - UserRelations - delete: - summary: 删除频道成员 - description: | - 用于删除 guild_id 指定的频道下的成员 user_id。 - * 需要使用的 token 对应的用户具备踢人权限。如果是机器人,要求被添加为管理员。 - * 操作成功后,会触发频道成员删除事件。 - * 无法移除身份为管理员的成员 - * `公域机器人暂不支持申请,仅私域机器人可用,选择私域机器人后默认开通。` - * `注意: 开通后需要先将机器人从频道移除,然后重新添加,方可生效。` - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/member/delete_member.html - requestBody: - required: false - content: - application/json: - schema: - properties: - add_blacklist: - type: boolean - description: 删除成员的同时,将该用户添加到频道黑名单中 - responses: - 204: - description: 成功 - tags: - - UserRelations - /guilds/{guild_id}/roles: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - get: - summary: 获取频道身份组列表 - description: 用于获取 guild_id指定的频道下的身份组列表 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/guild/get_guild_roles.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: object - properties: - guild_id: - type: string - description: 频道 ID - roles: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/Role' - description: 频道身份组对象列表 - role_num_limit: - type: string - description: 默认分组上限 - tags: [UserRelations] - post: - summary: 创建频道身份组 - description: | - 用于在guild_id 指定的频道下创建一个身份组。 - * 需要使用的 token 对应的用户具备创建身份组权限。如果是机器人,要求被添加为管理员。 - * 参数为非必填,但至少需要传其中之一,默认为空或 0。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/guild/post_guild_role.html - requestBody: - required: true - content: - application/json: - schema: - properties: - name: - type: string - description: 名称 - color: - type: number - description: ARGB 的 HEX 十六进制颜色值转换后的十进制数值(例:4294927682) - hoist: - type: number - description: 在成员列表中单独展示,0-否, 1-是 - required: - - name - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: object - properties: - role_id: - type: string - description: 频道身份组 ID - role: - $ref: 'bot.components.yaml#/components/schemas/GuildRole' - tags: [UserRelations] - - /guilds/{guild_id}/roles/{role_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - - $ref: 'bot.components.yaml#/components/parameters/PathRoleID' - patch: - summary: 修改频道身份组 - description: | - 用于修改频道 guild_id 下 role_id 指定的身份组。 - * 需要使用的 token 对应的用户具备修改身份组权限。如果是机器人,要求被添加为管理员。 - * 接口会修改传入的字段,不传入的默认不会修改,至少要传入一个参数。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/guild/patch_guild_role.html - requestBody: - required: true - content: - application/json: - schema: - properties: - name: - type: string - description: 名称 - color: - type: number - description: ARGB 的 HEX 十六进制颜色值转换后的十进制数值(例:4294927682) - hoist: - type: number - description: 在成员列表中单独展示,0-否, 1-是 - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: object - properties: - guild_id: - type: string - description: 频道 ID - role_id: - type: string - description: 频道身份组 ID - role: - $ref: 'bot.components.yaml#/components/schemas/GuildRole' - tags: [UserRelations] - delete: - summary: 删除频道身份组 - description: | - 用于删除频道guild_id下 role_id 对应的身份组。 - * 需要使用的 token 对应的用户具备`删除身份组权限`。如果是机器人,要求被添加为管理员。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/guild/delete_guild_role.html - responses: - 204: - description: 成功 - tags: [UserRelations] - - /guilds/{guild_id}/members/{user_id}/roles/{role_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - - $ref: 'bot.components.yaml#/components/parameters/PathRoleID' - - $ref: 'bot.components.yaml#/components/parameters/PathUserID' - put: - summary: 创建频道身份组成员 - description: | - 用于将频道guild_id下的用户 user_id 添加到身份组 role_id 。 - * 需要使用的 token 对应的用户具备增加身份组成员权限。如果是机器人,要求被添加为管理员。 - * 如果要增加的身份组 ID 是5-子频道管理员,需要增加 channel 对象来指定具体是哪个子频道。 - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/guild/put_guild_member_role.html' - requestBody: - required: true - content: - application/json: - schema: - properties: - id: - type: string - description: 子频道 id - responses: - 204: - description: 成功 - tags: [UserRelations] - delete: - summary: 删除频道身份组成员 - description: | - 用于将 用户 user_id 从 频道 guild_id 的 role_id 身份组中移除。 - * 需要使用的 token 对应的用户具备删除身份组成员权限。如果是机器人,要求被添加为管理员。 - * 如果要删除的身份组 ID 是5-子频道管理员,需要增加 channel 对象来指定具体是哪个子频道。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/guild/delete_guild_member_role.html - requestBody: - required: true - content: - application/json: - schema: - properties: - id: - type: string - description: 子频道 id - responses: - 204: - description: 成功 - tags: [UserRelations] - - /channels/{channel_id}/members/{user_id}/permissions: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - - $ref: 'bot.components.yaml#/components/parameters/PathUserID' - get: - summary: 获取子频道用户权限 - description: | - 用于获取 子频道channel_id 下用户 user_id 的权限。 - * 获取子频道用户权限。 - * 要求操作人具有`管理子频道的权限`,如果是机器人,则需要将机器人设置为管理员。 - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/channel_permissions/get_channel_permissions.html' - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/ChannelPermissions' - tags: - - Permissions - put: - summary: 修改子频道权限 - description: | - 用于修改子频道 channel_id 下用户 user_id 的权限。 - * 要求操作人具有管理子频道的权限,如果是机器人,则需要将机器人设置为管理员。 - * 参数包括add和remove两个字段,分别表示授予的权限以及删除的权限。要授予用户权限即把add对应位置 1,删除用户权限即把remove对应位置 1。当两个字段同一位都为 1,表现为删除权限。 - * `本接口不支持修改可管理子频道权限。` - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/channel_permissions/put_channel_permissions.html - requestBody: - required: true - content: - application/json: - schema: - properties: - add: - type: string - description: 字符串形式的位图表示赋予用户的权限 - remove: - type: string - description: 字符串形式的位图表示删除用户的权限 - responses: - 204: - description: 成功 - tags: - - Permissions - /channels/{channel_id}/roles/{role_id}/permissions: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - - $ref: 'bot.components.yaml#/components/parameters/PathRoleID' - get: - summary: 获取子频道身份组权限 - description: | - 用于获取子频道 channel_id 下身份组 role_id 的权限。 - * 要求操作人具有管理子频道的权限,如果是机器人,则需要将机器人设置为管理员。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/channel_permissions/get_channel_roles_permissions.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/ChannelPermissions' - tags: - - Permissions - put: - summary: 修改子频道身份组权限 - description: | - 用于修改子频道 channel_id 下身份组 role_id 的权限。 - * 要求操作人具有管理子频道的权限,如果是机器人,则需要将机器人设置为管理员。 - * 参数包括add和remove两个字段,分别表示授予的权限以及删除的权限。要授予身份组权限即把add对应位置 1,删除身份组权限即把remove对应位置 1。当两个字段同一位都为 1,表现为删除权限。 - * `本接口不支持修改可管理子频道权限。` - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/channel_permissions/put_channel_roles_permissions.html - requestBody: - required: true - content: - application/json: - schema: - properties: - add: - type: string - description: 字符串形式的位图表示赋予用户的权限 - remove: - type: string - description: 字符串形式的位图表示删除用户的权限 - responses: - 204: - description: 成功 - tags: - - Permissions - /channels/{channel_id}/messages/{message_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - - $ref: 'bot.components.yaml#/components/parameters/PathMessageID' - get: - summary: 获取指定消息 - description: 用于获取子频道 channel_id 下的消息 message_id 的详情。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/message/get_message_of_id.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Message' - tags: - - Messages - /channels/{channel_id}/messages: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - post: - summary: 发送消息 - description: | - 用于向 channel_id 指定的子频道发送消息。 - * 要求操作人在该子频道具有发送消息的权限。 - * 主动推送消息,默认每天往每个频道可推送的消息数是 `20` 条,超过会被限制。 - * 主动推送消息在每个频道中,每天可以往 `2` 个子频道推送消息。超过后会被限制。 - * 不论主动消息还是被动消息,在一个子频道中,每 `1s` 只能发送 `5` 条消息。 - * 被动回复消息有效期为 `5` 分钟。超时会报错。 - * 发送消息接口要求机器人接口需要连接到 websocket 上保持在线状态 - * 有关主动消息审核,可以通过 Intents 中审核事件 MESSAGE_AUDIT 返回 MessageAudited 对象获取结果。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/message/post_messages.html - requestBody: - required: true - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/MessageSend' - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Message' - tags: - - Messages - /users/@me/dms: - post: - summary: 创建私信会话 - description: | - 用于机器人和在同一个频道内的成员创建私信会话。 - * 机器人和用户存在共同频道才能创建私信会话。 - * 创建成功后,返回创建成功的频道 id ,子频道 id 和创建时间。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/dms/post_dms.html - requestBody: - required: true - content: - application/json: - schema: - properties: - recipient_id: - type: string - description: 接收者 id - source_guild_id: - type: string - description: 源频道 id - required: - - recipient_id - - source_guild_id - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/DMS' - tags: - - Messages - /dms/{guild_id}/messages: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - post: - summary: 发送私信 - description: | - 用于发送私信消息,前提是已经创建了私信会话。 - * 私信的 guild_id 在创建私信会话时以及私信消息事件中获取。 - * 私信场景下,每个机器人每天可以对一个用户发 `2` 条主动消息。 - * 私信场景下,每个机器人每天累计可以发 `200` 条主动消息。 - * 私信场景下,被动消息没有条数限制。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/dms/post_dms_messages.html - requestBody: - required: true - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/MessageSend' - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/Message' - tags: - - Messages - /guilds/{guild_id}/mute: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - patch: - summary: 禁言全员 - description: | - 用于将频道的全体成员(非管理员)禁言。 - * 需要使用的 token 对应的用户具备管理员权限。如果是机器人,要求被添加为管理员。 - * 该接口同样可用于解除禁言,具体使用见解除全员禁言。 - * `该接口同样支持解除全员禁言,将mute_end_timestamp或mute_seconds传值为字符串'0'即可`。 - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/guild/patch_guild_mute.html' - requestBody: - required: true - content: - application/json: - schema: - properties: - mute_end_timestamp: - type: string - description: 禁言到期时间戳,绝对时间戳,单位:秒(与 mute_seconds 字段同时赋值的话,以该字段为准) - mute_seconds: - type: string - description: 禁言多少秒(两个字段二选一,默认以 mute_end_timestamp 为准) - minProperties: 1 - responses: - 204: - description: 成功 - tags: - - Permissions - /guilds/{guild_id}/members/{user_id}/mute: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - - $ref: 'bot.components.yaml#/components/parameters/PathUserID' - patch: - summary: 禁言指定成员 - description: | - 用于禁言频道 guild_id 下的成员 user_id。 - * 需要使用的 token 对应的用户具备管理员权限。如果是机器人,要求被添加为管理员。 - * `该接口同样可用于解除禁言,具体使用见解除指定成员禁言`。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/guild/patch_guild_member_mute.html - requestBody: - required: true - content: - application/json: - schema: - properties: - mute_end_timestamp: - type: string - description: 禁言到期时间戳,绝对时间戳,单位:秒(与 mute_seconds 字段同时赋值的话,以该字段为准) - mute_seconds: - type: string - description: 禁言多少秒(两个字段二选一,默认以 mute_end_timestamp 为准) - minProperties: 1 - responses: - 204: - description: 成功 - tags: - - Permissions - /guilds/{guild_id}/announces: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - post: - summary: 创建频道公告 - description: | - 用于将频道内的某条消息设置为频道全局公告 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/announces/post_guild_announces.html - requestBody: - required: true - content: - application/json: - schema: - properties: - message_id: - type: string - description: 消息 id - channel_id: - type: string - description: 子频道 id - required: - - message_id - - channel_id - responses: - 204: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Announces' - tags: [GuildManagements] - /guilds/{guild_id}/announces/{message_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - - $ref: 'bot.components.yaml#/components/parameters/PathMessageID' - delete: - summary: 删除频道公告 - description: | - 用于删除频道 guild_id 下 message_id 指定的全局公告。 - * message_id 有值时,会校验 message_id 合法性,若不校验校验 message_id,请将 message_id 设置为 all。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/announces/delete_guild_announces.html - responses: - 204: - description: 成功 - tags: [GuildManagements] - /channels/{channel_id}/announces: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - post: - summary: 创建子频道公告 - description: 用于将子频道 channel_id 内的某条消息设置为子频道公告。 - deprecated: true - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/announces/post_channel_announces.html - requestBody: - required: true - content: - application/json: - schema: - properties: - message_id: - type: string - description: 消息 id - required: - - message_id - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Announces' - tags: [GuildManagements] - - /channels/{channel_id}/announces/{message_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - - $ref: 'bot.components.yaml#/components/parameters/PathMessageID' - delete: - summary: 删除子频道公告 - description: | - 用于删除子频道 channel_id 下 message_id 指定的子频道公告。 - * message_id 有值时,会校验 message_id 合法性,若不校验校验 message_id,请将 message_id 设置为 all。 - deprecated: true - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/announces/delete_channel_announces.html' - responses: - 204: - description: 成功 - tags: [GuildManagements] - - /channels/{channel_id}/schedules: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - get: - summary: 获取频道日程列表 - description: | - 用于获取channel_id指定的子频道中当天的日程列表。 - * 若带了参数 since,则返回结束时间在 since 之后的日程列表;若未带参数 since,则默认返回当天的日程列表。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/schedule/get_schedules.html - requestBody: - required: true - content: - application/json: - schema: - properties: - since: - type: integer - description: 起始时间戳(ms) - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/Schedule' - tags: - - Applications - post: - summary: 创建日程 - description: | - 用于在 channel_id 指定的日程子频道下创建一个日程。 - * 要求操作人具有管理频道的权限,如果是机器人,则需要将机器人设置为管理员。 - * 创建成功后,返回创建成功的日程对象。 - * 创建操作频次限制: 单个管理员每天限`10`次, 单个频道每天`100`次。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/schedule/post_schedule.html - requestBody: - required: true - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/ScheduleCreate' - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Schedule' - tags: - - Applications - /channels/{channel_id}/schedules/{schedule_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - - $ref: 'bot.components.yaml#/components/parameters/PathScheduleID' - get: - summary: 获取日程详情 - description: 获取日程子频道 channel_id 下 schedule_id 指定的的日程的详情。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/schedule/get_schedule.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Schedule' - tags: - - Applications - patch: - summary: 修改日程 - description: | - 用于修改日程子频道 channel_id 下 schedule_id 指定的日程的详情。 - * 要求操作人具有管理频道的权限,如果是机器人,则需要将机器人设置为管理员。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/schedule/patch_schedule.html - requestBody: - required: true - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/ScheduleUpdate' - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/Schedule' - tags: - - Applications - delete: - summary: 删除日程 - description: | - 用于删除日程子频道 channel_id 下 schedule_id 指定的日程。 - * 要求操作人具有`管理频道的权限`,如果是机器人,则需要将机器人设置为管理员。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/schedule/delete_schedule.html - responses: - 204: - description: 成功 - tags: - - Applications - /channels/{channel_id}/audio: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - post: - summary: 音频控制 - description: | - 用于控制子频道 channel_id 下的音频。 - * 音频接口:仅限音频类机器人才能使用,后续会根据机器人类型自动开通接口权限,现如需调用,需联系平台申请权限。 - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/audio/audio_control.html' - requestBody: - required: true - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/AudioControl' - responses: - 200: - description: 成功 - tags: - - Audio - /guilds/{guild_id}/api_permission: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - get: - summary: 获取频道可用权限列表 - description: | - 用于获取机器人在频道 guild_id 内可以使用的权限列表。 - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/api_permissions/get_guild_api_permission.html' - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/APIPermission' - tags: - - BotAPI - /guilds/{guild_id}/api_permission/demand: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathGuildID' - post: - summary: 创建频道 API 接口权限授权链接 - description: | - 用于创建 API 接口权限授权链接,该链接指向guild_id对应的频道 。 - * 每天只能在一个频道内发 `3` 条(默认值)频道权限授权链接。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/api_permissions/post_api_permission_demand.html - requestBody: - required: true - content: - application/json: - schema: - properties: - channel_id: - type: string - description: 授权链接发送的子频道 id - api_identify: - $ref: 'bot.components.yaml#/components/schemas/APIPermissionDemandIdentify' - desc: - type: string - description: 机器人申请对应的 API 接口权限后可以使用功能的描述 - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: array - items: - $ref: 'bot.components.yaml#/components/schemas/APIPermissionDemand' - tags: - - BotAPI - /gateway: - get: - summary: 获取通用 WSS 接入点 - description: 用于获取 WSS 接入地址,通过该地址可建立 websocket 长连接。 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/wss/url_get.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: object - properties: - url: - type: string - description: websocket 的地址 - tags: - - BotAPI - /gateway/bot: - get: - summary: 获取带分片 WSS 接入点 - description: | - 用于获取 WSS 接入地址及相关信息,通过该地址可建立 websocket 长连接。相关信息包括: - * 建议的分片数。 - * 目前连接数使用情况。 - externalDocs: - url: 'https://bot.q.qq.com/wiki/develop/api/openapi/wss/shard_url_get.html' - responses: - 200: - description: 成功 - content: - application/json: - schema: - type: object - properties: - url: - type: string - description: websocket 的地址 - shards: - type: integer - description: 建议的 shard 数 - session_start_limit: - $ref: 'bot.components.yaml#/components/schemas/SessionStartLimit' - tags: - - BotAPI - /channels/{channel_id}/messages/{message_id}/reactions/{type}/{id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - - $ref: 'bot.components.yaml#/components/parameters/PathMessageID' - - name: type - in: path - required: true - schema: - $ref: 'bot.components.yaml#/components/schemas/EmojiType' - description: 表情类型 - - name: id - in: path - required: true - schema: - $ref: 'bot.components.yaml#/components/schemas/EmojiID' - description: 表情ID - put: - tags: - - Messages - summary: 对消息 `message_id` 进行表情表态 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/reaction/put_message_reaction.html - responses: - 204: - description: 成功 - delete: - tags: - - Messages - summary: 删除自己对消息 `message_id` 的表情表态 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/reaction/delete_own_message_reaction.html - responses: - 204: - description: 成功 - /channels/{channel_id}/pins/{message_id}: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - - $ref: 'bot.components.yaml#/components/parameters/PathMessageID' - put: - tags: - - Messages - summary: 将消息 `message_id` 添加到精华消息中 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/pins/put_pins_message.html - responses: - 200: - description: 成功 - content: - applicztion/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/PinsMessage' - delete: - tags: - - Messages - summary: 将消息 `message_id` 从精华消息中删除 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/pins/delete_pins_message.html - responses: - 204: - description: 成功 - /channels/{channel_id}/pins: - parameters: - - $ref: 'bot.components.yaml#/components/parameters/PathChannelID' - get: - tags: [Messages] - summary: 获取精华消息列表 - externalDocs: - url: https://bot.q.qq.com/wiki/develop/api/openapi/pins/get_pins_message.html - responses: - 200: - description: 成功 - content: - application/json: - schema: - $ref: 'bot.components.yaml#/components/schemas/PinsMessage' - - -#================ -# 接口分类说明,不与 wiki 的分类对齐,而是按照实际的功能大类进行归类 -tags: - - name: Messages - description: 频道消息、私信等相关接口 - - name: UserRelations - description: 用户关系链相关接口API,包括用户信息,成员信息,用户的频道列表等 - - name: GuildManagements - description: 频道、子频道信息管理类的接口 - - name: Permissions - description: 频道管理相关接口,如身份组管理,权限管理等 - - name: Applications - description: 垂直应用类接口,如日程功能 - - name: Audio - description: 音频相关API集合 - - name: Forums - description: 论坛类接口,操作论坛帖子,回复等 - - name: BotAPI - description: 机器人提供的基础接口 - - -# 文档地址 -externalDocs: - description: 'Reference: QQ 机器人文档' - url: https://bot.q.qq.com/wiki - -# 对象定义,parameters 和 schemes 在独立文件中定义 -components: - securitySchemes: - bot_auth: - description: 机器人 API 鉴权,格式为 `Bot {appID}.{appToken}` - type: apiKey - name: Authorization - in: header - responses: - 401: - description: | - 请求异常,请检查参数传递是否正确 - 403: - description: | - 权限检查失败,请检查如下几点: - - 头部的 token 是否正确 - - 频道主是否授予了机器人对应的权限 - - 频道ID等ID是否正确 - 500: - description: | - 内部逻辑处理失败,大部分情况下可以重试,如果重试仍然失败,请检查参数是否正确 - 502: - description: | - 内部处理超时,请稍后重试 diff --git a/test/OpenAPI/v3.0/hubspot-events.json b/test/OpenAPI/v3.0/hubspot-events.json deleted file mode 100644 index fa11b5e0a..000000000 --- a/test/OpenAPI/v3.0/hubspot-events.json +++ /dev/null @@ -1,220 +0,0 @@ -{ - "openapi" : "3.0.1", - "info" : { - "title" : "Custom Behavioral Events API", - "description" : "HTTP API for triggering instances of custom behavioral events", - "version" : "v3" - }, - "servers" : [ { - "url" : "https://api.hubapi.com/" - } ], - "tags" : [ { - "name" : "Behavioral_Events_Tracking" - } ], - "paths" : { - "/events/v3/send" : { - "post" : { - "tags" : [ "Behavioral_Events_Tracking" ], - "summary" : "Sends Custom Behavioral Event", - "description" : "Endpoint to send an instance of a behavioral event", - "operationId" : "post-/events/v3/send", - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/BehavioralEventHttpCompletionRequest" - } - } - }, - "required" : true - }, - "responses" : { - "204" : { - "description" : "No content", - "content" : { } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "hapikey" : [ ] - }, { - "private_apps_legacy" : [ "analytics.behavioral_events.send" ] - }, { - "oauth2_legacy" : [ "analytics.behavioral_events.send" ] - } ] - } - } - }, - "components" : { - "schemas" : { - "ErrorDetail" : { - "required" : [ "message" ], - "type" : "object", - "properties" : { - "message" : { - "type" : "string", - "description" : "A human readable message describing the error along with remediation steps where appropriate" - }, - "in" : { - "type" : "string", - "description" : "The name of the field or parameter in which the error was found." - }, - "code" : { - "type" : "string", - "description" : "The status code associated with the error detail" - }, - "subCategory" : { - "type" : "string", - "description" : "A specific category that contains more specific detail about the error" - }, - "context" : { - "type" : "object", - "additionalProperties" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "description" : "Context about the error condition", - "example" : { - "missingScopes" : [ "scope1", "scope2" ] - } - } - } - }, - "BehavioralEventHttpCompletionRequest" : { - "required" : [ "eventName", "properties" ], - "type" : "object", - "properties" : { - "utk" : { - "type" : "string", - "description" : "User token" - }, - "email" : { - "type" : "string", - "description" : "Email of visitor" - }, - "eventName" : { - "type" : "string", - "description" : "Internal name of the event-type to trigger" - }, - "properties" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Map of properties for the event in the format property internal name - property value" - }, - "occurredAt" : { - "type" : "string", - "description" : "The time when this event occurred (if any). If this isn't set, the current time will be used", - "format" : "date-time" - }, - "objectId" : { - "type" : "string", - "description" : "The object id that this event occurred on. Could be a contact id or a visitor id." - } - } - }, - "Error" : { - "required" : [ "category", "correlationId", "message" ], - "type" : "object", - "properties" : { - "message" : { - "type" : "string", - "description" : "A human readable message describing the error along with remediation steps where appropriate", - "example" : "An error occurred" - }, - "correlationId" : { - "type" : "string", - "description" : "A unique identifier for the request. Include this value with any error reports or support tickets", - "format" : "uuid", - "example" : "aeb5f871-7f07-4993-9211-075dc63e7cbf" - }, - "category" : { - "type" : "string", - "description" : "The error category" - }, - "subCategory" : { - "type" : "string", - "description" : "A specific category that contains more specific detail about the error" - }, - "errors" : { - "type" : "array", - "description" : "further information about the error", - "items" : { - "$ref" : "#/components/schemas/ErrorDetail" - } - }, - "context" : { - "type" : "object", - "additionalProperties" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "description" : "Context about the error condition", - "example" : { - "invalidPropertyName" : [ "propertyValue" ], - "missingScopes" : [ "scope1", "scope2" ] - } - }, - "links" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "A map of link names to associated URIs containing documentation about the error or recommended remediation steps" - } - }, - "example" : { - "message" : "Invalid input (details will vary based on the error)", - "correlationId" : "aeb5f871-7f07-4993-9211-075dc63e7cbf", - "category" : "VALIDATION_ERROR", - "links" : { - "knowledge-base" : "https://www.hubspot.com/products/service/knowledge-base" - } - } - } - }, - "responses" : { - "Error" : { - "description" : "An error occurred.", - "content" : { - "*/*" : { - "schema" : { - "$ref" : "#/components/schemas/Error" - } - } - } - } - }, - "securitySchemes" : { - "oauth2_legacy" : { - "type" : "oauth2", - "flows" : { - "authorizationCode" : { - "authorizationUrl" : "https://app.hubspot.com/oauth/authorize", - "tokenUrl" : "https://api.hubapi.com/oauth/v1/token", - "scopes" : { - "analytics.behavioral_events.send" : "Send Behavioral Event Completions" - } - } - } - }, - "hapikey" : { - "type" : "apiKey", - "name" : "hapikey", - "in" : "query" - }, - "private_apps_legacy" : { - "type" : "apiKey", - "name" : "private-app-legacy", - "in" : "header" - } - } - } -} \ No newline at end of file diff --git a/test/OpenAPI/v3.0/hubspot-webhooks.json b/test/OpenAPI/v3.0/hubspot-webhooks.json deleted file mode 100644 index b65b0e6dd..000000000 --- a/test/OpenAPI/v3.0/hubspot-webhooks.json +++ /dev/null @@ -1,829 +0,0 @@ -{ - "openapi" : "3.0.1", - "info" : { - "title" : "Webhooks API", - "description" : "Provides a way for apps to subscribe to certain change events in HubSpot. Once configured, apps will receive event payloads containing details about the changes at a specified target URL. There can only be one target URL for receiving event notifications per app.", - "version" : "v3" - }, - "servers" : [ { - "url" : "https://api.hubapi.com/" - } ], - "tags" : [ { - "name" : "Settings", - "description" : "Operations to manage app-level webhook settings." - }, { - "name" : "Subscriptions", - "description" : "Operations to manage event subscriptions." - } ], - "paths" : { - "/webhooks/v3/{appId}/settings" : { - "get" : { - "tags" : [ "Settings" ], - "operationId" : "get-/webhooks/v3/{appId}/settings_getAll", - "parameters" : [ { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "responses" : { - "200" : { - "description" : "successful operation", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SettingsResponse" - } - } - } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - }, - "put" : { - "tags" : [ "Settings" ], - "operationId" : "put-/webhooks/v3/{appId}/settings_configure", - "parameters" : [ { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SettingsChangeRequest" - } - } - }, - "required" : true - }, - "responses" : { - "200" : { - "description" : "successful operation", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SettingsResponse" - } - } - } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - }, - "delete" : { - "tags" : [ "Settings" ], - "operationId" : "delete-/webhooks/v3/{appId}/settings_clear", - "parameters" : [ { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "responses" : { - "204" : { - "description" : "No content", - "content" : { } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - } - }, - "/webhooks/v3/{appId}/subscriptions" : { - "get" : { - "tags" : [ "Subscriptions" ], - "operationId" : "get-/webhooks/v3/{appId}/subscriptions_getAll", - "parameters" : [ { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "responses" : { - "200" : { - "description" : "successful operation", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SubscriptionListResponse" - } - } - } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - }, - "post" : { - "tags" : [ "Subscriptions" ], - "operationId" : "post-/webhooks/v3/{appId}/subscriptions_create", - "parameters" : [ { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SubscriptionCreateRequest" - } - } - }, - "required" : true - }, - "responses" : { - "201" : { - "description" : "successful operation", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SubscriptionResponse" - } - } - } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - } - }, - "/webhooks/v3/{appId}/subscriptions/batch/update" : { - "post" : { - "tags" : [ "Subscriptions" ], - "operationId" : "post-/webhooks/v3/{appId}/subscriptions/batch/update_updateBatch", - "parameters" : [ { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/BatchInputSubscriptionBatchUpdateRequest" - } - } - }, - "required" : true - }, - "responses" : { - "200" : { - "description" : "successful operation", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/BatchResponseSubscriptionResponse" - } - } - } - }, - "207" : { - "description" : "multiple statuses", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/BatchResponseSubscriptionResponseWithErrors" - } - } - } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - } - }, - "/webhooks/v3/{appId}/subscriptions/{subscriptionId}" : { - "get" : { - "tags" : [ "Subscriptions" ], - "operationId" : "get-/webhooks/v3/{appId}/subscriptions/{subscriptionId}_getById", - "parameters" : [ { - "name" : "subscriptionId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - }, { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "responses" : { - "200" : { - "description" : "successful operation", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SubscriptionResponse" - } - } - } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - }, - "delete" : { - "tags" : [ "Subscriptions" ], - "operationId" : "delete-/webhooks/v3/{appId}/subscriptions/{subscriptionId}_archive", - "parameters" : [ { - "name" : "subscriptionId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - }, { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "responses" : { - "204" : { - "description" : "No content", - "content" : { } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - }, - "patch" : { - "tags" : [ "Subscriptions" ], - "operationId" : "patch-/webhooks/v3/{appId}/subscriptions/{subscriptionId}_update", - "parameters" : [ { - "name" : "subscriptionId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - }, { - "name" : "appId", - "in" : "path", - "required" : true, - "style" : "simple", - "explode" : false, - "schema" : { - "type" : "integer", - "format" : "int32" - } - } ], - "requestBody" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SubscriptionPatchRequest" - } - } - }, - "required" : true - }, - "responses" : { - "200" : { - "description" : "successful operation", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/SubscriptionResponse" - } - } - } - }, - "default" : { - "$ref" : "#/components/responses/Error" - } - }, - "security" : [ { - "developer_hapikey" : [ ] - } ] - } - } - }, - "components" : { - "schemas" : { - "ThrottlingSettings" : { - "required" : [ "maxConcurrentRequests", "period" ], - "type" : "object", - "properties" : { - "maxConcurrentRequests" : { - "type" : "integer", - "description" : "The maximum number of HTTP requests HubSpot will attempt to make to your app in a given time frame determined by `period`.", - "format" : "int32" - }, - "period" : { - "type" : "string", - "description" : "Time scale for this setting. Can be either `SECONDLY` (per second) or `ROLLING_MINUTE` (per minute).", - "enum" : [ "SECONDLY", "ROLLING_MINUTE" ] - } - }, - "description" : "Configuration details for webhook throttling." - }, - "StandardError" : { - "required" : [ "category", "context", "errors", "links", "message", "status" ], - "type" : "object", - "properties" : { - "status" : { - "type" : "string" - }, - "id" : { - "type" : "string" - }, - "category" : { - "$ref" : "#/components/schemas/ErrorCategory" - }, - "subCategory" : { - "type" : "object", - "properties" : { } - }, - "message" : { - "type" : "string" - }, - "errors" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/ErrorDetail" - } - }, - "context" : { - "type" : "object", - "additionalProperties" : { - "type" : "array", - "items" : { - "type" : "string" - } - } - }, - "links" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - } - } - } - }, - "BatchResponseSubscriptionResponse" : { - "required" : [ "completedAt", "results", "startedAt", "status" ], - "type" : "object", - "properties" : { - "status" : { - "type" : "string", - "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] - }, - "results" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/SubscriptionResponse" - } - }, - "requestedAt" : { - "type" : "string", - "format" : "date-time" - }, - "startedAt" : { - "type" : "string", - "format" : "date-time" - }, - "completedAt" : { - "type" : "string", - "format" : "date-time" - }, - "links" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - } - } - } - }, - "SubscriptionListResponse" : { - "required" : [ "results" ], - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "List of event subscriptions for your app", - "items" : { - "$ref" : "#/components/schemas/SubscriptionResponse" - } - } - }, - "description" : "List of event subscriptions for your app" - }, - "Error" : { - "required" : [ "category", "correlationId", "message" ], - "type" : "object", - "properties" : { - "message" : { - "type" : "string", - "description" : "A human readable message describing the error along with remediation steps where appropriate", - "example" : "An error occurred" - }, - "correlationId" : { - "type" : "string", - "description" : "A unique identifier for the request. Include this value with any error reports or support tickets", - "format" : "uuid", - "example" : "aeb5f871-7f07-4993-9211-075dc63e7cbf" - }, - "category" : { - "type" : "string", - "description" : "The error category" - }, - "subCategory" : { - "type" : "string", - "description" : "A specific category that contains more specific detail about the error" - }, - "errors" : { - "type" : "array", - "description" : "further information about the error", - "items" : { - "$ref" : "#/components/schemas/ErrorDetail" - } - }, - "context" : { - "type" : "object", - "additionalProperties" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "description" : "Context about the error condition", - "example" : { - "invalidPropertyName" : [ "propertyValue" ], - "missingScopes" : [ "scope1", "scope2" ] - } - }, - "links" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "A map of link names to associated URIs containing documentation about the error or recommended remediation steps" - } - }, - "example" : { - "message" : "Invalid input (details will vary based on the error)", - "correlationId" : "aeb5f871-7f07-4993-9211-075dc63e7cbf", - "category" : "VALIDATION_ERROR", - "links" : { - "knowledge-base" : "https://www.hubspot.com/products/service/knowledge-base" - } - } - }, - "SettingsChangeRequest" : { - "required" : [ "targetUrl", "throttling" ], - "type" : "object", - "properties" : { - "targetUrl" : { - "type" : "string", - "description" : "A publicly available URL for Hubspot to call where event payloads will be delivered. See [link-so-some-doc](#) for details about the format of these event payloads." - }, - "throttling" : { - "$ref" : "#/components/schemas/ThrottlingSettings" - } - }, - "description" : "New or updated webhook settings for an app.", - "example" : { - "targetUrl" : "https://www.example.com/hubspot/target", - "throttling" : { - "maxConcurrentRequests" : 10, - "period" : "SECONDLY" - } - } - }, - "SubscriptionPatchRequest" : { - "type" : "object", - "properties" : { - "active" : { - "type" : "boolean", - "description" : "Determines if the subscription is active or paused." - } - }, - "description" : "Updated details for the subscription.", - "example" : { - "active" : true - } - }, - "BatchResponseSubscriptionResponseWithErrors" : { - "required" : [ "completedAt", "results", "startedAt", "status" ], - "type" : "object", - "properties" : { - "status" : { - "type" : "string", - "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] - }, - "results" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/SubscriptionResponse" - } - }, - "numErrors" : { - "type" : "integer", - "format" : "int32" - }, - "errors" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/StandardError" - } - }, - "requestedAt" : { - "type" : "string", - "format" : "date-time" - }, - "startedAt" : { - "type" : "string", - "format" : "date-time" - }, - "completedAt" : { - "type" : "string", - "format" : "date-time" - }, - "links" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - } - } - } - }, - "ErrorDetail" : { - "required" : [ "message" ], - "type" : "object", - "properties" : { - "message" : { - "type" : "string", - "description" : "A human readable message describing the error along with remediation steps where appropriate" - }, - "in" : { - "type" : "string", - "description" : "The name of the field or parameter in which the error was found." - }, - "code" : { - "type" : "string", - "description" : "The status code associated with the error detail" - }, - "subCategory" : { - "type" : "string", - "description" : "A specific category that contains more specific detail about the error" - }, - "context" : { - "type" : "object", - "additionalProperties" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "description" : "Context about the error condition", - "example" : { - "missingScopes" : [ "scope1", "scope2" ] - } - } - } - }, - "SettingsResponse" : { - "required" : [ "createdAt", "targetUrl", "throttling" ], - "type" : "object", - "properties" : { - "targetUrl" : { - "type" : "string", - "description" : "A publicly available URL for Hubspot to call where event payloads will be delivered. See [link-so-some-doc](#) for details about the format of these event payloads." - }, - "throttling" : { - "$ref" : "#/components/schemas/ThrottlingSettings" - }, - "createdAt" : { - "type" : "string", - "description" : "When this subscription was created. Formatted as milliseconds from the [Unix epoch](#).", - "format" : "date-time" - }, - "updatedAt" : { - "type" : "string", - "description" : "When this subscription was last updated. Formatted as milliseconds from the [Unix epoch](#).", - "format" : "date-time" - } - }, - "description" : "Webhook settings for an app.", - "example" : { - "targetUrl" : "https://www.example.com/hubspot/target", - "throttling" : { - "maxConcurrentRequests" : 10, - "period" : "SECONDLY" - }, - "createdAt" : "2020-01-24T16:27:59Z", - "updatedAt" : "2020-01-24T16:32:43Z" - } - }, - "ErrorCategory" : { - "required" : [ "httpStatus", "name" ], - "type" : "object", - "properties" : { - "name" : { - "type" : "string" - }, - "httpStatus" : { - "type" : "string", - "enum" : [ "CONTINUE", "SWITCHING_PROTOCOLS", "PROCESSING", "OK", "CREATED", "ACCEPTED", "NON_AUTHORITATIVE_INFORMATION", "NO_CONTENT", "RESET_CONTENT", "PARTIAL_CONTENT", "MULTI_STATUS", "ALREADY_REPORTED", "IM_USED", "MULTIPLE_CHOICES", "MOVED_PERMANENTLY", "FOUND", "SEE_OTHER", "NOT_MODIFIED", "USE_PROXY", "TEMPORARY_REDIRECT", "PERMANENT_REDIRECT", "BAD_REQUEST", "UNAUTHORIZED", "PAYMENT_REQUIRED", "FORBIDDEN", "NOT_FOUND", "METHOD_NOT_ALLOWED", "NOT_ACCEPTABLE", "PROXY_AUTHENTICATION_REQUIRED", "REQUEST_TIMEOUT", "CONFLICT", "GONE", "LENGTH_REQUIRED", "PRECONDITION_FAILED", "REQUEST_ENTITY_TOO_LARGE", "REQUEST_URI_TOO_LONG", "UNSUPPORTED_MEDIA_TYPE", "REQUESTED_RANGE_NOT_SATISFIABLE", "EXPECTATION_FAILED", "IM_A_TEAPOT", "MISDIRECTED_REQUEST", "UNPROCESSABLE_ENTITY", "LOCKED", "FAILED_DEPENDENCY", "UPGRADE_REQUIRED", "PRECONDITION_REQUIRED", "TOO_MANY_REQUESTS", "REQUEST_HEADERS_FIELDS_TOO_LARGE", "INTERNAL_STALE_SERVICE_DISCOVERY", "UNAVAILABLE_FOR_LEGAL_REASONS", "MIGRATION_IN_PROGRESS", "INTERNAL_SERVER_ERROR", "NOT_IMPLEMENTED", "BAD_GATEWAY", "SERVICE_UNAVAILABLE", "GATEWAY_TIMEOUT", "HTTP_VERSION_NOT_SUPPORTED", "VARIANT_ALSO_NEGOTIATES", "INSUFFICIENT_STORAGE", "LOOP_DETECTED", "NOT_EXTENDED", "NETWORK_AUTHENTICATION_REQUIRED" ] - } - } - }, - "BatchInputSubscriptionBatchUpdateRequest" : { - "required" : [ "inputs" ], - "type" : "object", - "properties" : { - "inputs" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/SubscriptionBatchUpdateRequest" - } - } - } - }, - "SubscriptionBatchUpdateRequest" : { - "required" : [ "active", "id" ], - "type" : "object", - "properties" : { - "id" : { - "type" : "integer", - "format" : "int32" - }, - "active" : { - "type" : "boolean" - } - } - }, - "SubscriptionResponse" : { - "required" : [ "active", "createdAt", "eventType", "id" ], - "type" : "object", - "properties" : { - "eventType" : { - "type" : "string", - "description" : "Type of event to listen for. Can be one of `create`, `delete`, `deletedForPrivacy`, or `propertyChange`.", - "enum" : [ "contact.propertyChange", "company.propertyChange", "deal.propertyChange", "ticket.propertyChange", "product.propertyChange", "line_item.propertyChange", "contact.creation", "contact.deletion", "contact.privacyDeletion", "company.creation", "company.deletion", "deal.creation", "deal.deletion", "ticket.creation", "ticket.deletion", "product.creation", "product.deletion", "line_item.creation", "line_item.deletion", "conversation.creation", "conversation.deletion", "conversation.newMessage", "conversation.privacyDeletion", "conversation.propertyChange", "contact.merge", "company.merge", "deal.merge", "ticket.merge", "product.merge", "line_item.merge", "contact.restore", "company.restore", "deal.restore", "ticket.restore", "product.restore", "line_item.restore", "contact.associationChange", "company.associationChange", "deal.associationChange", "ticket.associationChange", "line_item.associationChange" ] - }, - "propertyName" : { - "type" : "string", - "description" : "The internal name of the property being monitored for changes. Only applies when `eventType` is `propertyChange`." - }, - "active" : { - "type" : "boolean", - "description" : "Determines if the subscription is active or paused." - }, - "id" : { - "type" : "string", - "description" : "The unique ID of the subscription." - }, - "createdAt" : { - "type" : "string", - "description" : "When this subscription was created. Formatted as milliseconds from the [Unix epoch](#).", - "format" : "date-time" - }, - "updatedAt" : { - "type" : "string", - "description" : "When this subscription was last updated. Formatted as milliseconds from the [Unix epoch](#).", - "format" : "date-time" - } - }, - "description" : "Complete details for an event subscription." - }, - "SubscriptionCreateRequest" : { - "required" : [ "eventType" ], - "type" : "object", - "properties" : { - "eventType" : { - "type" : "string", - "description" : "Type of event to listen for. Can be one of `create`, `delete`, `deletedForPrivacy`, or `propertyChange`.", - "enum" : [ "contact.propertyChange", "company.propertyChange", "deal.propertyChange", "ticket.propertyChange", "product.propertyChange", "line_item.propertyChange", "contact.creation", "contact.deletion", "contact.privacyDeletion", "company.creation", "company.deletion", "deal.creation", "deal.deletion", "ticket.creation", "ticket.deletion", "product.creation", "product.deletion", "line_item.creation", "line_item.deletion", "conversation.creation", "conversation.deletion", "conversation.newMessage", "conversation.privacyDeletion", "conversation.propertyChange", "contact.merge", "company.merge", "deal.merge", "ticket.merge", "product.merge", "line_item.merge", "contact.restore", "company.restore", "deal.restore", "ticket.restore", "product.restore", "line_item.restore", "contact.associationChange", "company.associationChange", "deal.associationChange", "ticket.associationChange", "line_item.associationChange" ] - }, - "propertyName" : { - "type" : "string", - "description" : "The internal name of the property to monitor for changes. Only applies when `eventType` is `propertyChange`." - }, - "active" : { - "type" : "boolean", - "description" : "Determines if the subscription is active or paused. Defaults to false." - } - }, - "description" : "New webhook settings for an app.", - "example" : { - "active" : true, - "eventType" : "contact.propertyChange", - "propertyName" : "email" - } - } - }, - "responses" : { - "Error" : { - "description" : "An error occurred.", - "content" : { - "*/*" : { - "schema" : { - "$ref" : "#/components/schemas/Error" - } - } - } - } - }, - "securitySchemes" : { - "developer_hapikey" : { - "type" : "apiKey", - "name" : "hapikey", - "in" : "query" - } - } - }, - "x-hubspot-available-client-libraries" : [ "PHP", "Node", "Python", "Ruby" ] -} \ No newline at end of file diff --git a/test/smoke-tests.ps1 b/test/smoke-tests.ps1 index c96004a23..1c5a6554e 100644 --- a/test/smoke-tests.ps1 +++ b/test/smoke-tests.ps1 @@ -141,7 +141,6 @@ function RunTests $filenames = @( "weather", - "bot.paths", "petstore", "petstore-expanded", "petstore-minimal", @@ -152,8 +151,7 @@ function RunTests "link-example", "uber", "uspto", - "hubspot-events", - "hubspot-webhooks" + "hubspot-events" ) $v31Filenames = @(