Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix broken interface generated from HubSpot API's #64

Merged
merged 11 commits into from
Jun 8, 2023
Merged
15 changes: 10 additions & 5 deletions .github/workflows/smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ jobs:
format: [json, yaml]
version: [v2.0, v3.0]
os: [ubuntu-latest]
openapi: ["callback-example", "link-example", "uber", "uspto", "petstore", "ingram-micro"]
openapi_url: [
"https://petstore3.swagger.io/api/v3/openapi.json",
"https://petstore3.swagger.io/api/v3/openapi.yaml"
openapi: [
"callback-example",
"link-example",
"uber",
"uspto",
"petstore",
"ingram-micro"
]

uses: ./.github/workflows/template.yml
Expand All @@ -48,7 +51,9 @@ jobs:
os: [ubuntu-latest]
openapi_url: [
"https://petstore3.swagger.io/api/v3/openapi.json",
"https://petstore3.swagger.io/api/v3/openapi.yaml"
"https://petstore3.swagger.io/api/v3/openapi.yaml",
"https://api.hubspot.com/api-catalog-public/v1/apis/events/v3/send",
"https://api.hubspot.com/api-catalog-public/v1/apis/webhooks/v3"
]

uses: ./.github/workflows/template-url.yml
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,4 @@ test/**/*Petstore*.cs
test/**/*Uber.cs
test/**/*Uspto.cs
test/**/*Ingram-micro.cs
*.lutconfig
61 changes: 61 additions & 0 deletions src/Refitter.Core/OperationNameGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using NSwag;
using NSwag.CodeGeneration.OperationNameGenerators;
using System.Collections.Generic;
using System.Linq;

namespace Refitter.Core;

public class OperationNameGenerator : IOperationNameGenerator
{
private readonly IOperationNameGenerator defaultGenerator =
new MultipleClientsFromOperationIdOperationNameGenerator();

public OperationNameGenerator(OpenApiDocument document)
{
if (this.CheckForDuplicateOperationIds(document))
defaultGenerator = new MultipleClientsFromFirstTagAndPathSegmentsOperationNameGenerator();
}

public bool SupportsMultipleClients => throw new System.NotImplementedException();

public string GetClientName(OpenApiDocument document, string path, string httpMethod, OpenApiOperation operation)
{
return defaultGenerator.GetClientName(document, path, httpMethod, operation);
}

public string GetOperationName(
OpenApiDocument document,
string path,
string httpMethod,
OpenApiOperation operation) =>
defaultGenerator
.GetOperationName(document, path, httpMethod, operation)
.CapitalizeFirstCharacter()
.ConvertKebabCaseToPascalCase()
.ConvertRouteToCamelCase();
}

public static class IOperationNameGeneratorExtensions
{
public static bool CheckForDuplicateOperationIds(
this IOperationNameGenerator generator,
OpenApiDocument document)
{
List<string> operationNames = new();
foreach (var kv in document.Paths)
{
foreach (var operations in kv.Value)
{
var operation = operations.Value;
operationNames.Add(
generator.GetOperationName(
document,
kv.Key,
operations.Key,
operation));
}
}

return operationNames.Distinct().Count() != operationNames.Count;
}
}
5 changes: 2 additions & 3 deletions src/Refitter.Core/RefitInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public RefitInterfaceGenerator(
this.settings = settings;
this.document = document;
this.generator = generator;
generator.BaseSettings.OperationNameGenerator = new OperationNameGenerator(document);
}

public string GenerateRefitInterface()
Expand Down Expand Up @@ -48,9 +49,7 @@ private string GenerateInterfaceBody()
var verb = operations.Key.CapitalizeFirstCharacter();

var name = generator.BaseSettings.OperationNameGenerator
.GetOperationName(document, kv.Key, verb, operation)
.CapitalizeFirstCharacter()
.ConvertKebabCaseToPascalCase();
.GetOperationName(document, kv.Key, verb, operation);

var parameters = ParameterExtractor.GetParameters(generator, operation, settings);
var parametersString = string.Join(", ", parameters);
Expand Down
13 changes: 12 additions & 1 deletion src/Refitter.Core/StringCasingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static string ConvertKebabCaseToPascalCase(this string str)

return string.Join(string.Empty, parts);
}

public static string ConvertKebabCaseToCamelCase(this string str)
{
var parts = str.Split('-');
Expand All @@ -25,6 +25,17 @@ public static string ConvertKebabCaseToCamelCase(this string str)
return string.Join(string.Empty, parts);
}

public static string ConvertRouteToCamelCase(this string str)
{
var parts = str.Split('/');
for (var i = 1; i < parts.Length; i++)
{
parts[i] = parts[i].CapitalizeFirstCharacter();
}

return string.Join(string.Empty, parts);
}

public static string CapitalizeFirstCharacter(this string str)
{
return str.Substring(0, 1).ToUpperInvariant() +
Expand Down
220 changes: 220 additions & 0 deletions test/OpenAPI/v3.0/hubspot-events.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
{
"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"
}
}
}
}
Loading