From ca1ff945fa0e9e74d05e24ea6781254b54f18ed7 Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 2 Oct 2023 15:29:18 -0700 Subject: [PATCH 01/12] chore: Move tests to root. --- .github/workflows/sonarcloud.yml | 2 +- apimanifest.sln | 26 ++++++++++--------- src/tests/Usings.cs | 2 -- .../ApiManifest.Tests.csproj | 7 ++--- {src/tests => tests}/BasicTests.cs | 2 +- {src/tests => tests}/CreateTests.cs | 8 +++--- {src/tests => tests}/PluginTests.cs | 2 +- tests/Usings.cs | 1 + 8 files changed, 27 insertions(+), 23 deletions(-) delete mode 100644 src/tests/Usings.cs rename src/tests/tests.csproj => tests/ApiManifest.Tests.csproj (79%) rename {src/tests => tests}/BasicTests.cs (99%) rename {src/tests => tests}/CreateTests.cs (94%) rename {src/tests => tests}/PluginTests.cs (99%) create mode 100644 tests/Usings.cs diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index dd84375..d47a3a4 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -71,7 +71,7 @@ jobs: CoverletOutputFormat: "opencover" # https://github.com/microsoft/vstest/issues/4014#issuecomment-1307913682 shell: pwsh run: | - ./.sonar/scanner/dotnet-sonarscanner begin /k:"microsoft_OpenApi.ApiManifest" /o:"microsoft" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="src/tests/**/coverage.opencover.xml" + ./.sonar/scanner/dotnet-sonarscanner begin /k:"microsoft_OpenApi.ApiManifest" /o:"microsoft" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="tests/**/coverage.opencover.xml" dotnet build dotnet test apimanifest.sln --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" diff --git a/apimanifest.sln b/apimanifest.sln index cdce9f7..0629722 100644 --- a/apimanifest.sln +++ b/apimanifest.sln @@ -5,27 +5,22 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{39495B7F-9E1F-4DBE-AAA1-C9C9620675AA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tests", "src\tests\tests.csproj", "{02EFB22C-FF50-4D4C-8F83-A394597E11E6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "apimanifest", "src\lib\apimanifest.csproj", "{3B4ACF87-6364-48A2-94B8-0EB3201D922E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "apimanifest", "src\lib\apimanifest.csproj", "{3B4ACF87-6364-48A2-94B8-0EB3201D922E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tool", "src\tool\tool.csproj", "{DCFFC5B9-253A-4BFE-9CBE-0DAAE822E3EB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tool", "src\tool\tool.csproj", "{DCFFC5B9-253A-4BFE-9CBE-0DAAE822E3EB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "benchmark", "src\benchmark\benchmark.csproj", "{24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "benchmark", "src\benchmark\benchmark.csproj", "{24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{13E6B8EB-7EA6-4CAD-A9A2-3473307EB30F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiManifest.Tests", "tests\ApiManifest.Tests.csproj", "{1D899CA0-12DB-4166-9BD7-875CFF204738}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {02EFB22C-FF50-4D4C-8F83-A394597E11E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {02EFB22C-FF50-4D4C-8F83-A394597E11E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {02EFB22C-FF50-4D4C-8F83-A394597E11E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {02EFB22C-FF50-4D4C-8F83-A394597E11E6}.Release|Any CPU.Build.0 = Release|Any CPU {3B4ACF87-6364-48A2-94B8-0EB3201D922E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B4ACF87-6364-48A2-94B8-0EB3201D922E}.Debug|Any CPU.Build.0 = Debug|Any CPU {3B4ACF87-6364-48A2-94B8-0EB3201D922E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -38,11 +33,18 @@ Global {24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU {24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU {24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA}.Release|Any CPU.Build.0 = Release|Any CPU + {1D899CA0-12DB-4166-9BD7-875CFF204738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D899CA0-12DB-4166-9BD7-875CFF204738}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D899CA0-12DB-4166-9BD7-875CFF204738}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D899CA0-12DB-4166-9BD7-875CFF204738}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {02EFB22C-FF50-4D4C-8F83-A394597E11E6} = {39495B7F-9E1F-4DBE-AAA1-C9C9620675AA} {3B4ACF87-6364-48A2-94B8-0EB3201D922E} = {39495B7F-9E1F-4DBE-AAA1-C9C9620675AA} {DCFFC5B9-253A-4BFE-9CBE-0DAAE822E3EB} = {39495B7F-9E1F-4DBE-AAA1-C9C9620675AA} {24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA} = {39495B7F-9E1F-4DBE-AAA1-C9C9620675AA} + {1D899CA0-12DB-4166-9BD7-875CFF204738} = {13E6B8EB-7EA6-4CAD-A9A2-3473307EB30F} EndGlobalSection EndGlobal diff --git a/src/tests/Usings.cs b/src/tests/Usings.cs deleted file mode 100644 index f685d78..0000000 --- a/src/tests/Usings.cs +++ /dev/null @@ -1,2 +0,0 @@ -global using Microsoft.OpenApi.ApiManifest; -global using Xunit; diff --git a/src/tests/tests.csproj b/tests/ApiManifest.Tests.csproj similarity index 79% rename from src/tests/tests.csproj rename to tests/ApiManifest.Tests.csproj index 9bab270..373a52a 100644 --- a/src/tests/tests.csproj +++ b/tests/ApiManifest.Tests.csproj @@ -1,10 +1,11 @@ - + + Microsoft.OpenApi.ApiManifest.Tests + Microsoft.OpenApi.ApiManifest.Tests net7.0 enable enable - false true @@ -23,7 +24,7 @@ - + diff --git a/src/tests/BasicTests.cs b/tests/BasicTests.cs similarity index 99% rename from src/tests/BasicTests.cs rename to tests/BasicTests.cs index faf5f5c..eda0a03 100644 --- a/src/tests/BasicTests.cs +++ b/tests/BasicTests.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; -namespace Tests.ApiManifest; +namespace Microsoft.OpenApi.ApiManifest.Tests; public class BasicTests { diff --git a/src/tests/CreateTests.cs b/tests/CreateTests.cs similarity index 94% rename from src/tests/CreateTests.cs rename to tests/CreateTests.cs index 8bf5f96..bd17cc8 100644 --- a/src/tests/CreateTests.cs +++ b/tests/CreateTests.cs @@ -1,6 +1,6 @@ using System.Text.Json.Nodes; -namespace Tests.ApiManifest; +namespace Microsoft.OpenApi.ApiManifest.Tests; public class CreateTests { @@ -52,8 +52,10 @@ public void CreateApiDependencyWithInvalidApiDeploymentBaseUrl(string apiDeploym { _ = Assert.Throws(() => { - var apiDependency = new ApiDependency(); - apiDependency.ApiDeploymentBaseUrl = apiDeploymentBaseUrl; + var apiDependency = new ApiDependency + { + ApiDeploymentBaseUrl = apiDeploymentBaseUrl + }; } ); } diff --git a/src/tests/PluginTests.cs b/tests/PluginTests.cs similarity index 99% rename from src/tests/PluginTests.cs rename to tests/PluginTests.cs index f8b272b..71113fe 100644 --- a/src/tests/PluginTests.cs +++ b/tests/PluginTests.cs @@ -3,7 +3,7 @@ using Microsoft.OpenApi.ApiManifest.OpenAI; using System.Text.Json; -namespace Tests.OpenAI +namespace Microsoft.OpenApi.ApiManifest.Tests.OpenAI { public class OpenAIPluginManifestTests { diff --git a/tests/Usings.cs b/tests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; From 24aeef5d450edaf432335dd5da1f68cd806777e4 Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 2 Oct 2023 15:47:59 -0700 Subject: [PATCH 02/12] chore: Update solution. --- apimanifest.sln | 15 +- .../ApiManifest.Tests.csproj | 31 +++ tests/ApiManifest.Tests/BasicTests.cs | 201 ++++++++++++++++++ tests/ApiManifest.Tests/CreateTests.cs | 92 ++++++++ tests/ApiManifest.Tests/GlobalUsings.cs | 1 + .../OpenAIPluginManifestTests.cs | 146 +++++++++++++ 6 files changed, 480 insertions(+), 6 deletions(-) create mode 100644 tests/ApiManifest.Tests/ApiManifest.Tests.csproj create mode 100644 tests/ApiManifest.Tests/BasicTests.cs create mode 100644 tests/ApiManifest.Tests/CreateTests.cs create mode 100644 tests/ApiManifest.Tests/GlobalUsings.cs create mode 100644 tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs diff --git a/apimanifest.sln b/apimanifest.sln index 0629722..d1e07fe 100644 --- a/apimanifest.sln +++ b/apimanifest.sln @@ -13,7 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "benchmark", "src\benchmark\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{13E6B8EB-7EA6-4CAD-A9A2-3473307EB30F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiManifest.Tests", "tests\ApiManifest.Tests.csproj", "{1D899CA0-12DB-4166-9BD7-875CFF204738}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiManifest.Tests", "tests\ApiManifest.Tests\ApiManifest.Tests.csproj", "{10411C2B-C1AC-44FC-AF46-E0264438E797}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,10 +33,10 @@ Global {24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU {24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU {24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA}.Release|Any CPU.Build.0 = Release|Any CPU - {1D899CA0-12DB-4166-9BD7-875CFF204738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1D899CA0-12DB-4166-9BD7-875CFF204738}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1D899CA0-12DB-4166-9BD7-875CFF204738}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1D899CA0-12DB-4166-9BD7-875CFF204738}.Release|Any CPU.Build.0 = Release|Any CPU + {10411C2B-C1AC-44FC-AF46-E0264438E797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10411C2B-C1AC-44FC-AF46-E0264438E797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10411C2B-C1AC-44FC-AF46-E0264438E797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10411C2B-C1AC-44FC-AF46-E0264438E797}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -45,6 +45,9 @@ Global {3B4ACF87-6364-48A2-94B8-0EB3201D922E} = {39495B7F-9E1F-4DBE-AAA1-C9C9620675AA} {DCFFC5B9-253A-4BFE-9CBE-0DAAE822E3EB} = {39495B7F-9E1F-4DBE-AAA1-C9C9620675AA} {24B7722C-4FE9-4B52-9AA3-5D2FBCDA2DFA} = {39495B7F-9E1F-4DBE-AAA1-C9C9620675AA} - {1D899CA0-12DB-4166-9BD7-875CFF204738} = {13E6B8EB-7EA6-4CAD-A9A2-3473307EB30F} + {10411C2B-C1AC-44FC-AF46-E0264438E797} = {13E6B8EB-7EA6-4CAD-A9A2-3473307EB30F} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6A91121B-DC65-413C-8635-B32B30C30A6F} EndGlobalSection EndGlobal diff --git a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj new file mode 100644 index 0000000..67a866d --- /dev/null +++ b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + + false + true + Microsoft.OpenApi.ApiManifest.Tests + Microsoft.OpenApi.ApiManifest.Tests + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/ApiManifest.Tests/BasicTests.cs b/tests/ApiManifest.Tests/BasicTests.cs new file mode 100644 index 0000000..238e5ec --- /dev/null +++ b/tests/ApiManifest.Tests/BasicTests.cs @@ -0,0 +1,201 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.OpenApi.ApiManifest.Tests; +public class BasicTests +{ + private readonly ApiManifestDocument exampleApiManifest; + public BasicTests() + { + exampleApiManifest = CreateDocument(); + } + + // Create test to instantiate a simple ApiManifestDocument + [Fact] + public void InitializeDocument() + { + Assert.NotNull(exampleApiManifest); + } + + // Serialize the ApiManifestDocument to a string + [Fact] + public void SerializeDocument() + { + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream); + exampleApiManifest.Write(writer); + writer.Flush(); + // Read string from stream + stream.Position = 0; + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + Debug.WriteLine(json); + var doc = JsonDocument.Parse(json); + Assert.NotNull(doc); + Assert.Equal("application-name", doc.RootElement.GetProperty("applicationName").GetString()); + Assert.Equal("Microsoft", doc.RootElement.GetProperty("publisher").GetProperty("name").GetString()); + } + + // Deserialize the ApiManifestDocument from a string + [Fact] + public void DeserializeDocument() + { + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream); + exampleApiManifest.Write(writer); + writer.Flush(); + // Read string from stream + stream.Position = 0; + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + var doc = JsonDocument.Parse(json); + var apiManifest = ApiManifestDocument.Load(doc.RootElement); + Assert.Equivalent(exampleApiManifest.Publisher, apiManifest.Publisher); + Assert.Equivalent(exampleApiManifest.ApiDependencies["example"].Requests, apiManifest.ApiDependencies["example"].Requests); + Assert.Equivalent(exampleApiManifest.ApiDependencies["example"].ApiDescriptionUrl, apiManifest.ApiDependencies["example"].ApiDescriptionUrl); + Assert.Equivalent(exampleApiManifest.ApiDependencies["example"].ApiDeploymentBaseUrl, apiManifest.ApiDependencies["example"].ApiDeploymentBaseUrl); + var expectedAuth = exampleApiManifest.ApiDependencies["example"].AuthorizationRequirements; + var actualAuth = apiManifest.ApiDependencies["example"].AuthorizationRequirements; + Assert.Equivalent(expectedAuth?.ClientIdentifier, actualAuth?.ClientIdentifier); + Assert.Equivalent(expectedAuth?.Access?[0]?.Content?.ToJsonString(), actualAuth?.Access?[0]?.Content?.ToJsonString()); + } + + + // Create an empty document + [Fact] + public void CreateDocumentWithRequiredFields() + { + var doc = new ApiManifestDocument("application-name"); + Assert.NotNull(doc); + Assert.Equal("application-name", doc.ApplicationName); + Assert.NotNull(doc.ApiDependencies); + Assert.Empty(doc.ApiDependencies); + } + + // Create a document with a publisher that is missing required fields (name and contactEmail). + [Fact] + public void CreateDocumentWithMissingRequiredPublisherFields() + { + _ = Assert.Throws(() => + { + var doc = new ApiManifestDocument("application-name") + { + Publisher = new("", "") + }; + } + ); + } + + [Fact] + public void FailToParseDocumentWithoutApplicationName() + { + _ = Assert.Throws(() => + { + var serializedValue = "{\"apiDependencies\": { \"graph\": {\"apiDescriptionUrl\":\"https://example.org\"}}}"; + var doc = JsonDocument.Parse(serializedValue); + _ = ApiManifestDocument.Load(doc.RootElement); + } + ); + } + + [Fact] + public void ParsesApiDescriptionUrlField() + { + // Given + var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"apiDescriptionUrl\":\"https://example.org\"}}}"; + var doc = JsonDocument.Parse(serializedValue); + + // When + var apiManifest = ApiManifestDocument.Load(doc.RootElement); + + // Then + Assert.Equal("https://example.org", apiManifest.ApiDependencies["graph"].ApiDescriptionUrl); + } + [Fact] + public void ParseApiDescriptionVersionField() + { + // Given + var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"apiDescriptionVersion\":\"v1.0\"}}}"; + var doc = JsonDocument.Parse(serializedValue); + + // When + var apiManifest = ApiManifestDocument.Load(doc.RootElement); + + // Then + Assert.Equal("v1.0", apiManifest.ApiDependencies["graph"].ApiDescriptionVersion); + } + [Fact] + public void ParsesApiDeploymentBaseUrl() + { + // Given + var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"apiDeploymentBaseUrl\":\"https://example.org/\"}}}"; + var doc = JsonDocument.Parse(serializedValue); + + // When + var apiManifest = ApiManifestDocument.Load(doc.RootElement); + + // Then + Assert.Equal("https://example.org/", apiManifest.ApiDependencies["graph"].ApiDeploymentBaseUrl); + } + + [Fact] + public void ParsesApiDeploymentBaseUrlWithDifferentCasing() + { + // Given + var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"APIDeploymentBaseUrl\":\"https://example.org/\"}}}"; + var doc = JsonDocument.Parse(serializedValue); + + // When + var apiManifest = ApiManifestDocument.Load(doc.RootElement); + + // Then + Assert.Equal("https://example.org/", apiManifest.ApiDependencies["graph"].ApiDeploymentBaseUrl); + } + + [Fact] + public void DoesNotFailOnExtraneousProperty() + { + // Given + var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"APIDeploymentBaseUrl\":\"https://example.org/\", \"APISensitivity\":\"low\"}}}"; + var doc = JsonDocument.Parse(serializedValue); + + // When + var apiManifest = ApiManifestDocument.Load(doc.RootElement); + + // Then + Assert.Equal("https://example.org/", apiManifest.ApiDependencies["graph"].ApiDeploymentBaseUrl); + } + + private static ApiManifestDocument CreateDocument() + { + return new ApiManifestDocument("application-name") + { + Publisher = new("Microsoft", "example@example.org"), + ApiDependencies = new() { + { "example", new() + { + ApiDescriptionUrl = "https://example.org", + ApiDeploymentBaseUrl = "https://example.org/v1.0/", + AuthorizationRequirements = new() + { + ClientIdentifier = "1234", + Access = new() { + new () { Type= "application", Content = new JsonObject() { + { "scopes", new JsonArray() {"User.Read.All"} }} + } , + new () { Type= "delegated", Content = new JsonObject() { + { "scopes", new JsonArray() {"User.Read", "Mail.Read"} }} + } + } + }, + Requests = new() { + new () { Method = "GET", UriTemplate = "/api/v1/endpoint" }, + new () { Method = "POST", UriTemplate = "/api/v1/endpoint"} + } + } + } + } + }; + } +} diff --git a/tests/ApiManifest.Tests/CreateTests.cs b/tests/ApiManifest.Tests/CreateTests.cs new file mode 100644 index 0000000..36311ff --- /dev/null +++ b/tests/ApiManifest.Tests/CreateTests.cs @@ -0,0 +1,92 @@ +using System.Text.Json.Nodes; + +namespace Microsoft.OpenApi.ApiManifest.Tests; +public class CreateTests +{ + + [Fact] + public void CreateApiManifestDocumentWithRequiredFields() + { + var apiManifest = new ApiManifestDocument("application-name"); + Assert.NotNull(apiManifest); + Assert.Equal("application-name", apiManifest.ApplicationName); + Assert.Null(apiManifest.Publisher); + Assert.Empty(apiManifest.ApiDependencies); + Assert.Empty(apiManifest.Extensions); + } + + [Theory] + [InlineData("foo@bar")] + [InlineData("foo@bar.com")] + public void CreatePublisher(string contactEmail) + { + var publisher = new Publisher(name: "Contoso", contactEmail: contactEmail); + Assert.Equal("Contoso", publisher.Name); + Assert.Equal(contactEmail, publisher.ContactEmail); + + } + + [Theory] + [InlineData("foo")] + [InlineData("foo@")] + [InlineData("foo@@bar.com")] + [InlineData("foo @bar.com")] + public void CreatePublisherWithInvalidEmail(string contactEmail) + { + _ = Assert.Throws(() => + { + var publisher = new Publisher(name: "Contoso", contactEmail: contactEmail); + } + ); + } + + [Theory] + [InlineData("foo")] + [InlineData("https://foo.com")] + [InlineData("http://128.0.0.0")] + [InlineData("https://foo@@bar.com")] + [InlineData("https://foo bar.com/")] + [InlineData("https://graph.microsoft.com/v1.0")] + public void CreateApiDependencyWithInvalidApiDeploymentBaseUrl(string apiDeploymentBaseUrl) + { + _ = Assert.Throws(() => + { + var apiDependency = new ApiDependency + { + ApiDeploymentBaseUrl = apiDeploymentBaseUrl + }; + } + ); + } + + // Create test to instantiate ApiManifest with auth + [Fact] + public void CreateApiManifestWithAuthorizationRequirements() + { + var apiManifest = new ApiManifestDocument("application-name") + { + Publisher = new(name: "Contoso", contactEmail: "foo@bar.com"), + ApiDependencies = new() { + { "Contoso.Api", new() { + ApiDeploymentBaseUrl = "https://api.contoso.com/", + AuthorizationRequirements = new() { + ClientIdentifier = "2143234-234324-234234234-234", + Access = new() { + new() { Type = "oauth2", + Content = new JsonObject() { + { "scopes", new JsonArray() { "user.read", "user.write" } } + } + } + } + } + } + } + } + }; + Assert.NotNull(apiManifest.ApiDependencies["Contoso.Api"].AuthorizationRequirements); + Assert.Equal("https://api.contoso.com/", apiManifest.ApiDependencies["Contoso.Api"].ApiDeploymentBaseUrl); + Assert.Equal("2143234-234324-234234234-234", apiManifest?.ApiDependencies["Contoso.Api"]?.AuthorizationRequirements?.ClientIdentifier); + Assert.Equal("oauth2", apiManifest?.ApiDependencies["Contoso.Api"]?.AuthorizationRequirements?.Access?[0].Type); + } + +} diff --git a/tests/ApiManifest.Tests/GlobalUsings.cs b/tests/ApiManifest.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/ApiManifest.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs new file mode 100644 index 0000000..be32217 --- /dev/null +++ b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs @@ -0,0 +1,146 @@ +using Microsoft.OpenApi.ApiManifest.OpenAI; +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.Tests; + +public class OpenAIPluginManifestTests +{ + [Fact] + public void LoadOpenAIPluginManifest() + { + var json = @"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""OpenAI GPT-3"", + ""name_for_model"": ""openai-gpt3"", + ""description_for_human"": ""OpenAI GPT-3 is a language model that generates text based on prompts."" , + ""description_for_model"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", + ""auth"": { + ""type"": ""none"" + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"" + }, + ""logo_url"": ""https://avatars.githubusercontent.com/foo"", + ""contact_email"": ""joe@demo.com"" + }"; + + var doc = JsonDocument.Parse(json); + var manifest = OpenAIPluginManifest.Load(doc.RootElement); + + Assert.Equal("1.0.0", manifest.SchemaVersion); + Assert.Equal("OpenAI GPT-3", manifest.NameForHuman); + Assert.Equal("openai-gpt3", manifest.NameForModel); + Assert.Equal("OpenAI GPT-3 is a language model that generates text based on prompts.", manifest.DescriptionForHuman); + Assert.Equal("OpenAI GPT-3 is a language model that generates text based on prompts.", manifest.DescriptionForModel); + Assert.Equal("none", manifest.Auth?.Type); + Assert.Equal("openapi", manifest.Api?.Type); + Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); + Assert.Equal("https://avatars.githubusercontent.com/foo", manifest.LogoUrl); + Assert.Equal("joe@demo.com", manifest.ContactEmail); + } + + // Create minimal OpenAIPluginManifest + [Fact] + public void WriteOpenAIPluginManifest() + { + var manifest = new OpenAIPluginManifest + { + SchemaVersion = "1.0.0", + NameForHuman = "OpenAI GPT-3", + NameForModel = "openai-gpt3", + DescriptionForHuman = "OpenAI GPT-3 is a language model that generates text based on prompts.", + DescriptionForModel = "OpenAI GPT-3 is a language model that generates text based on prompts.", + Auth = new ManifestNoAuth(), + Api = new Api + { + Type = "openapi", + Url = "https://api.openai.com/v1", + IsUserAuthenticated = false + }, + LogoUrl = "https://avatars.githubusercontent.com/bar", + ContactEmail = "joe@test.com" + }; + + // serialize using the Write method + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + manifest.Write(writer); + writer.Flush(); + stream.Position = 0; + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + Assert.Equal(@"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""OpenAI GPT-3"", + ""name_for_model"": ""openai-gpt3"", + ""description_for_human"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", + ""description_for_model"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", + ""auth"": { + ""type"": ""none"" + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + } + + [Fact] + public void WriteOAuthTest() + { + var manifest = new OpenAIPluginManifest + { + SchemaVersion = "1.0.0", + NameForHuman = "TestOAuth", + NameForModel = "TestOAuthModel", + DescriptionForHuman = "SomeHumanDescription", + DescriptionForModel = "SomeModelDescription", + Auth = new ManifestOAuthAuth + { + AuthorizationUrl = "https://api.openai.com/oauth/authorize", + }, + Api = new Api + { + Type = "openapi", + Url = "https://api.openai.com/v1", + IsUserAuthenticated = false + }, + LogoUrl = "https://avatars.githubusercontent.com/bar", + ContactEmail = "joe@test.com" + }; + + // serialize using the Write method + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + manifest.Write(writer); + writer.Flush(); + stream.Position = 0; + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + Assert.Equal(@"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""TestOAuth"", + ""name_for_model"": ""TestOAuthModel"", + ""description_for_human"": ""SomeHumanDescription"", + ""description_for_model"": ""SomeModelDescription"", + ""auth"": { + ""type"": ""oauth"", + ""authorization_url"": ""https://api.openai.com/oauth/authorize"" + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + } + +} From 55e564bf1f7a6fad0a41878aaaf8c2ebf863affa Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Tue, 3 Oct 2023 15:43:42 -0700 Subject: [PATCH 03/12] - Adds verification tokens to OpenAI manifest auth --- src/lib/Extensions.cs | 5 +- src/lib/OpenAI/Api.cs | 3 + src/lib/OpenAI/Auth/BaseManifestAuth.cs | 47 ++++ src/lib/OpenAI/Auth/ManifestNoAuth.cs | 28 ++ src/lib/OpenAI/Auth/ManifestOAuthAuth.cs | 49 ++++ .../OpenAI/Auth/ManifestServiceHttpAuth.cs | 43 +++ src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs | 36 +++ src/lib/OpenAI/Auth/VerificationTokens.cs | 28 ++ src/lib/OpenAI/BaseManifestAuth.cs | 145 ---------- src/lib/OpenAI/OpenAIPluginManifest.cs | 3 + src/lib/OpenAI/OpenApiPluginFactory.cs | 3 + src/lib/ParsingHelpers.cs | 14 + src/lib/Properties/AssemblyInfo.cs | 5 + src/lib/apimanifest.csproj | 2 +- src/{lib => }/sgKey.snk | Bin tests/ApiManifest.Tests.csproj | 30 --- .../ApiManifest.Tests.csproj | 5 +- tests/ApiManifest.Tests/BasicTests.cs | 49 ++-- tests/ApiManifest.Tests/CreateTests.cs | 5 +- .../OpenAIPluginManifestTests.cs | 255 +++++++++++++++++- tests/BasicTests.cs | 203 -------------- tests/CreateTests.cs | 93 ------- tests/PluginTests.cs | 151 ----------- tests/Usings.cs | 1 - 24 files changed, 555 insertions(+), 648 deletions(-) create mode 100644 src/lib/OpenAI/Auth/BaseManifestAuth.cs create mode 100644 src/lib/OpenAI/Auth/ManifestNoAuth.cs create mode 100644 src/lib/OpenAI/Auth/ManifestOAuthAuth.cs create mode 100644 src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs create mode 100644 src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs create mode 100644 src/lib/OpenAI/Auth/VerificationTokens.cs delete mode 100644 src/lib/OpenAI/BaseManifestAuth.cs create mode 100644 src/lib/Properties/AssemblyInfo.cs rename src/{lib => }/sgKey.snk (100%) delete mode 100644 tests/ApiManifest.Tests.csproj delete mode 100644 tests/BasicTests.cs delete mode 100644 tests/CreateTests.cs delete mode 100644 tests/PluginTests.cs delete mode 100644 tests/Usings.cs diff --git a/src/lib/Extensions.cs b/src/lib/Extensions.cs index a5916c9..b6402bf 100644 --- a/src/lib/Extensions.cs +++ b/src/lib/Extensions.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + using System.Text.Json; using System.Text.Json.Nodes; @@ -14,7 +17,7 @@ public static Extensions Load(JsonElement value) { if (property.Value.ValueKind != JsonValueKind.Null) { - var extensionValue = JsonSerializer.Deserialize(property.Value.GetRawText()); + var extensionValue = JsonSerializer.Deserialize(property.Value.GetRawText()); extensions.Add(property.Name, extensionValue); } } diff --git a/src/lib/OpenAI/Api.cs b/src/lib/OpenAI/Api.cs index 0560189..e01408b 100644 --- a/src/lib/OpenAI/Api.cs +++ b/src/lib/OpenAI/Api.cs @@ -1,4 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.OpenAI; diff --git a/src/lib/OpenAI/Auth/BaseManifestAuth.cs b/src/lib/OpenAI/Auth/BaseManifestAuth.cs new file mode 100644 index 0000000..c80f8a0 --- /dev/null +++ b/src/lib/OpenAI/Auth/BaseManifestAuth.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.ApiManifest.OpenAI.Auth; +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI; + +public abstract class BaseManifestAuth +{ + public string? Type { get; set; } + public string? Instructions { get; set; } + + public static BaseManifestAuth? Load(JsonElement value) + { + BaseManifestAuth? auth = null; + + switch (value.GetProperty("type").GetString()) + { + case "none": + auth = new ManifestNoAuth(); + ParsingHelpers.ParseMap(value, (ManifestNoAuth)auth, ManifestNoAuth.handlers); + break; + case "user_http": + var authorizationType = value.GetProperty("authorization_type").GetString(); + auth = new ManifestUserHttpAuth(authorizationType); + ParsingHelpers.ParseMap(value, (ManifestUserHttpAuth)auth, ManifestUserHttpAuth.handlers); + break; + case "service_http": + var verificationTokens = value.GetProperty("verification_tokens"); + auth = new ManifestServiceHttpAuth(VerificationTokens.Load(verificationTokens)); + ParsingHelpers.ParseMap(value, (ManifestServiceHttpAuth)auth, ManifestServiceHttpAuth.handlers); + break; + case "oauth": + auth = new ManifestOAuthAuth(); + ParsingHelpers.ParseMap(value, (ManifestOAuthAuth)auth, ManifestOAuthAuth.handlers); + break; + } + + return auth; + } + + // Create handlers FixedFieldMap for ManifestAuth + + public virtual void Write(Utf8JsonWriter writer) { } + +} diff --git a/src/lib/OpenAI/Auth/ManifestNoAuth.cs b/src/lib/OpenAI/Auth/ManifestNoAuth.cs new file mode 100644 index 0000000..cada494 --- /dev/null +++ b/src/lib/OpenAI/Auth/ManifestNoAuth.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI; + +public class ManifestNoAuth : BaseManifestAuth +{ + public ManifestNoAuth() + { + Type = "none"; + } + + internal static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, + }; + + public override void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteString("type", Type); + if (Instructions != null) writer.WriteString("instructions", Instructions); + writer.WriteEndObject(); + } +} diff --git a/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs b/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs new file mode 100644 index 0000000..ccb908e --- /dev/null +++ b/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.ApiManifest.OpenAI.Auth; +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI; + +public class ManifestOAuthAuth : BaseManifestAuth +{ + public string? ClientUrl { get; set; } + public string? Scope { get; set; } + public string? AuthorizationUrl { get; set; } + public string? AuthorizationContentType { get; set; } + public VerificationTokens VerificationTokens { get; set; } = new VerificationTokens(); + + public ManifestOAuthAuth() + { + Type = "oauth"; + } + internal static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, + { "client_url", (o,v) => {o.ClientUrl = v.GetString(); } }, + { "scope", (o,v) => {o.Scope = v.GetString(); } }, + { "authorization_url", (o,v) => {o.AuthorizationUrl = v.GetString(); } }, + { "authorization_content_type", (o,v) => {o.AuthorizationContentType = v.GetString(); } }, + { "verification_tokens", (o,v) => { o.VerificationTokens = VerificationTokens.Load(v); } }, + }; + + public override void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteString("type", Type); + + if (Instructions != null) writer.WriteString("instructions", Instructions); + if (ClientUrl != null) writer.WriteString("client_url", ClientUrl); + if (Scope != null) writer.WriteString("scope", Scope); + if (AuthorizationUrl != null) writer.WriteString("authorization_url", AuthorizationUrl); + if (AuthorizationContentType != null) writer.WriteString("authorization_content_type", AuthorizationContentType); + if (VerificationTokens.Any()) + { + writer.WritePropertyName("verification_tokens"); + VerificationTokens.Write(writer); + } + writer.WriteEndObject(); + } +} diff --git a/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs b/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs new file mode 100644 index 0000000..2494e8b --- /dev/null +++ b/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.ApiManifest.OpenAI.Auth; +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI; + +public class ManifestServiceHttpAuth : BaseManifestAuth +{ + public string? AuthorizationType { get; set; } + public VerificationTokens VerificationTokens { get; set; } + public ManifestServiceHttpAuth(VerificationTokens verificationTokens) + { + if (verificationTokens == null || !verificationTokens.Any()) + { + // Reference: https://platform.openai.com/docs/plugins/authentication/service-level + throw new ArgumentException($"{nameof(verificationTokens)} must be have at least one verification token."); + } + Type = "service_http"; + AuthorizationType = "bearer"; + VerificationTokens = verificationTokens; + } + + internal static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "authorization_type", (o,v) => {o.AuthorizationType = v.GetString(); } }, + { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, + { "verification_tokens", (o, v) => { o.VerificationTokens = VerificationTokens.Load(v); } } + }; + + public override void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteString("type", Type); + writer.WriteString("authorization_type", AuthorizationType); + if (Instructions != null) writer.WriteString("instructions", Instructions); + writer.WritePropertyName("verification_tokens"); + VerificationTokens.Write(writer); + writer.WriteEndObject(); + } +} diff --git a/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs b/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs new file mode 100644 index 0000000..ea232ad --- /dev/null +++ b/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI; + +public class ManifestUserHttpAuth : BaseManifestAuth +{ + public string? AuthorizationType { get; set; } + public ManifestUserHttpAuth(string? authorizationType) + { + if (string.IsNullOrWhiteSpace(authorizationType) || (authorizationType != "basic" && authorizationType != "bearer")) + { + // Reference: https://platform.openai.com/docs/plugins/authentication/user-level + throw new ArgumentException($"{nameof(authorizationType)} must be either 'basic' or 'bearer'."); + } + Type = "user_http"; + AuthorizationType = authorizationType; + } + + internal static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "authorization_type", (o,v) => {o.AuthorizationType = v.GetString(); } }, + { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, + }; + public override void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteString("type", Type); + writer.WriteString("authorization_type", AuthorizationType); + if (Instructions != null) writer.WriteString("instructions", Instructions); + writer.WriteEndObject(); + } +} diff --git a/src/lib/OpenAI/Auth/VerificationTokens.cs b/src/lib/OpenAI/Auth/VerificationTokens.cs new file mode 100644 index 0000000..b8cd438 --- /dev/null +++ b/src/lib/OpenAI/Auth/VerificationTokens.cs @@ -0,0 +1,28 @@ + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI.Auth; + +public class VerificationTokens : Dictionary +{ + public VerificationTokens(IDictionary dictionary) : base(dictionary, StringComparer.OrdinalIgnoreCase) { } + public VerificationTokens() : base(StringComparer.OrdinalIgnoreCase) { } + + internal static VerificationTokens Load(JsonElement value) + { + return new VerificationTokens(ParsingHelpers.GetMapOfString(value)); + } + + public void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + foreach (var verificationToken in this) + { + writer.WriteString(verificationToken.Key, verificationToken.Value); + } + writer.WriteEndObject(); + } +} diff --git a/src/lib/OpenAI/BaseManifestAuth.cs b/src/lib/OpenAI/BaseManifestAuth.cs deleted file mode 100644 index f73d9cf..0000000 --- a/src/lib/OpenAI/BaseManifestAuth.cs +++ /dev/null @@ -1,145 +0,0 @@ - -using System.Text.Json; - -namespace Microsoft.OpenApi.ApiManifest.OpenAI; - -public abstract class BaseManifestAuth -{ - public string? Type { get; set; } - public string? Instructions { get; set; } - - public static BaseManifestAuth? Load(JsonElement value) - { - BaseManifestAuth? auth = null; - - switch (value.GetProperty("type").GetString()) - { - case "none": - auth = new ManifestNoAuth(); - ParsingHelpers.ParseMap(value, (ManifestNoAuth)auth, ManifestNoAuth.handlers); - break; - case "user_http": - auth = new ManifestUserHttpAuth(); - ParsingHelpers.ParseMap(value, (ManifestUserHttpAuth)auth, ManifestUserHttpAuth.handlers); - break; - case "service_http": - auth = new ManifestServiceHttpAuth(); - ParsingHelpers.ParseMap(value, (ManifestServiceHttpAuth)auth, ManifestServiceHttpAuth.handlers); - break; - case "oauth": - auth = new ManifestOAuthAuth(); - ParsingHelpers.ParseMap(value, (ManifestOAuthAuth)auth, ManifestOAuthAuth.handlers); - break; - } - - return auth; - } - - // Create handlers FixedFieldMap for ManifestAuth - - public virtual void Write(Utf8JsonWriter writer) { } - -} - -public class ManifestNoAuth : BaseManifestAuth -{ - public ManifestNoAuth() - { - Type = "none"; - } - - internal static FixedFieldMap handlers = new() - { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, - }; - - public override void Write(Utf8JsonWriter writer) - { - writer.WriteStartObject(); - writer.WriteString("type", Type); - if (Instructions != null) writer.WriteString("instructions", Instructions); - writer.WriteEndObject(); - } -} - -public class ManifestOAuthAuth : BaseManifestAuth -{ - public string? ClientUrl { get; set; } - public string? Scope { get; set; } - public string? AuthorizationUrl { get; set; } - public string? AuthorizationContentType { get; set; } - public Dictionary? VerificationTokens { get; set; } - - public ManifestOAuthAuth() - { - Type = "oauth"; - } - internal static FixedFieldMap handlers = new() - { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, - { "client_url", (o,v) => {o.ClientUrl = v.GetString(); } }, - { "scope", (o,v) => {o.Scope = v.GetString(); } }, - { "authorization_url", (o,v) => {o.AuthorizationUrl = v.GetString(); } }, - { "authorization_content_type", (o,v) => {o.AuthorizationContentType = v.GetString(); } }, - { "verification_tokens", (o,v) => { o.VerificationTokens = ParsingHelpers.GetMap(v,(e) => e.GetString() is string val && !string.IsNullOrEmpty(val) ? val : string.Empty ); } }, - }; - - public override void Write(Utf8JsonWriter writer) - { - writer.WriteStartObject(); - writer.WriteString("type", Type); - - if (Instructions != null) writer.WriteString("instructions", Instructions); - if (ClientUrl != null) writer.WriteString("client_url", ClientUrl); - if (Scope != null) writer.WriteString("scope", Scope); - if (AuthorizationUrl != null) writer.WriteString("authorization_url", AuthorizationUrl); - if (AuthorizationContentType != null) writer.WriteString("authorization_content_type", AuthorizationContentType); - writer.WriteEndObject(); - } -} - -public class ManifestUserHttpAuth : BaseManifestAuth -{ - public ManifestUserHttpAuth() - { - Type = "user_http"; - } - internal static FixedFieldMap handlers = new() - { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, - }; - public override void Write(Utf8JsonWriter writer) - { - writer.WriteStartObject(); - writer.WriteString("type", Type); - writer.WriteString("instructions", Instructions); - writer.WriteEndObject(); - } -} - -public class ManifestServiceHttpAuth : BaseManifestAuth -{ - public ManifestServiceHttpAuth() - { - Type = "service_http"; - } - internal static FixedFieldMap handlers = new() - { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, - }; - public override void Write(Utf8JsonWriter writer) - { - writer.WriteStartObject(); - writer.WriteString("type", Type); - writer.WriteString("instructions", Instructions); - writer.WriteEndObject(); - } -} - - - - diff --git a/src/lib/OpenAI/OpenAIPluginManifest.cs b/src/lib/OpenAI/OpenAIPluginManifest.cs index cac4465..65fe74c 100644 --- a/src/lib/OpenAI/OpenAIPluginManifest.cs +++ b/src/lib/OpenAI/OpenAIPluginManifest.cs @@ -1,4 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.OpenAI; diff --git a/src/lib/OpenAI/OpenApiPluginFactory.cs b/src/lib/OpenAI/OpenApiPluginFactory.cs index 9e66ff5..6e5fa63 100644 --- a/src/lib/OpenAI/OpenApiPluginFactory.cs +++ b/src/lib/OpenAI/OpenApiPluginFactory.cs @@ -1,4 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + namespace Microsoft.OpenApi.ApiManifest.OpenAI; public class OpenApiPluginFactory diff --git a/src/lib/ParsingHelpers.cs b/src/lib/ParsingHelpers.cs index c523ca5..88cc86e 100644 --- a/src/lib/ParsingHelpers.cs +++ b/src/lib/ParsingHelpers.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest; @@ -36,6 +39,17 @@ internal static Dictionary GetMap(JsonElement v, Func GetMapOfString(JsonElement v) + { + var map = new Dictionary(); + foreach (var item in v.EnumerateObject()) + { + var value = item.Value.GetString(); + map.Add(item.Name, value ?? string.Empty); + } + return map; + } + internal static SortedDictionary GetOrderedMap(JsonElement v, Func load) { var map = new SortedDictionary(); diff --git a/src/lib/Properties/AssemblyInfo.cs b/src/lib/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..75d6ba2 --- /dev/null +++ b/src/lib/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ + + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.OpenApi.ApiManifest.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010079141b19153e38ff281aa3cfb29e887c0c27af8da48ff54bdb50364ea31cda51b165adc776f54768d5b18b1a15ee7167e8befaafd0d89eac38788820a0cfb3f5867fbc46c7faef5cf1d1f6000490f4a0781311170da4f51b5b16e4fa7c4f27964996548dfe565dc67b5829d8dc0229ef83aebfe8a3b4a67a24e6b836bc7d12d2")] diff --git a/src/lib/apimanifest.csproj b/src/lib/apimanifest.csproj index af3b2b4..d86e684 100644 --- a/src/lib/apimanifest.csproj +++ b/src/lib/apimanifest.csproj @@ -19,7 +19,7 @@ API Manifest Microsoft.OpenApi.ApiManifest Microsoft.OpenApi.ApiManifest - sgKey.snk + ..\sgKey.snk \ No newline at end of file diff --git a/src/lib/sgKey.snk b/src/sgKey.snk similarity index 100% rename from src/lib/sgKey.snk rename to src/sgKey.snk diff --git a/tests/ApiManifest.Tests.csproj b/tests/ApiManifest.Tests.csproj deleted file mode 100644 index 373a52a..0000000 --- a/tests/ApiManifest.Tests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - Microsoft.OpenApi.ApiManifest.Tests - Microsoft.OpenApi.ApiManifest.Tests - net7.0 - enable - enable - false - true - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj index 67a866d..b7424c5 100644 --- a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj +++ b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj @@ -1,14 +1,15 @@ - + net7.0 enable enable - + True false true Microsoft.OpenApi.ApiManifest.Tests Microsoft.OpenApi.ApiManifest.Tests + ..\..\src\sgKey.snk diff --git a/tests/ApiManifest.Tests/BasicTests.cs b/tests/ApiManifest.Tests/BasicTests.cs index 238e5ec..18cfb72 100644 --- a/tests/ApiManifest.Tests/BasicTests.cs +++ b/tests/ApiManifest.Tests/BasicTests.cs @@ -1,4 +1,7 @@ -using System.Diagnostics; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Nodes; @@ -59,6 +62,10 @@ public void DeserializeDocument() var actualAuth = apiManifest.ApiDependencies["example"].AuthorizationRequirements; Assert.Equivalent(expectedAuth?.ClientIdentifier, actualAuth?.ClientIdentifier); Assert.Equivalent(expectedAuth?.Access?[0]?.Content?.ToJsonString(), actualAuth?.Access?[0]?.Content?.ToJsonString()); + Assert.NotNull(exampleApiManifest.Extensions["api-manifest-extension"]); + Assert.Equal(exampleApiManifest.Extensions["api-manifest-extension"]?.ToString(), apiManifest.Extensions?["api-manifest-extension"]?.ToString()); + Assert.NotNull(exampleApiManifest.ApiDependencies["example"]?.Extensions?["EXAMPLE-API-DEPENDENCY-EXTENSION"]); + Assert.Equal(exampleApiManifest.ApiDependencies["example"]?.Extensions?["example-API-dependency-extension"]?.ToString(), apiManifest.ApiDependencies["example"]?.Extensions?["example-api-dependency-extension"]?.ToString()); } @@ -174,27 +181,35 @@ private static ApiManifestDocument CreateDocument() Publisher = new("Microsoft", "example@example.org"), ApiDependencies = new() { { "example", new() + { + ApiDescriptionUrl = "https://example.org", + ApiDeploymentBaseUrl = "https://example.org/v1.0/", + AuthorizationRequirements = new() { - ApiDescriptionUrl = "https://example.org", - ApiDeploymentBaseUrl = "https://example.org/v1.0/", - AuthorizationRequirements = new() - { - ClientIdentifier = "1234", - Access = new() { - new () { Type= "application", Content = new JsonObject() { - { "scopes", new JsonArray() {"User.Read.All"} }} - } , - new () { Type= "delegated", Content = new JsonObject() { - { "scopes", new JsonArray() {"User.Read", "Mail.Read"} }} - } + ClientIdentifier = "1234", + Access = new() { + new() { Type = "application", Content = new JsonObject() { + { "scopes", new JsonArray() { "User.Read.All" } } } + }, + new() { Type = "delegated", Content = new JsonObject() { + { "scopes", new JsonArray() { "User.Read", "Mail.Read" } } } } - }, - Requests = new() { - new () { Method = "GET", UriTemplate = "/api/v1/endpoint" }, - new () { Method = "POST", UriTemplate = "/api/v1/endpoint"} } + }, + Requests = new() { + new() { Method = "GET", UriTemplate = "/api/v1/endpoint" }, + new() { Method = "POST", UriTemplate = "/api/v1/endpoint" } + }, + Extensions = new() + { + { "example-api-dependency-extension", "dependency-extension-value" } } } + } + }, + Extensions = new() + { + { "api-manifest-extension", "manifest-extension-value" } } }; } diff --git a/tests/ApiManifest.Tests/CreateTests.cs b/tests/ApiManifest.Tests/CreateTests.cs index 36311ff..2db3749 100644 --- a/tests/ApiManifest.Tests/CreateTests.cs +++ b/tests/ApiManifest.Tests/CreateTests.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Nodes; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json.Nodes; namespace Microsoft.OpenApi.ApiManifest.Tests; public class CreateTests diff --git a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs index be32217..28833ca 100644 --- a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs +++ b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs @@ -1,4 +1,7 @@ -using Microsoft.OpenApi.ApiManifest.OpenAI; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.ApiManifest.OpenAI; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.Tests; @@ -91,7 +94,55 @@ public void WriteOpenAIPluginManifest() } [Fact] - public void WriteOAuthTest() + public void LoadOpenAIPluginManifestWithOAuth() + { + var json = @"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""TestOAuth"", + ""name_for_model"": ""TestOAuthModel"", + ""description_for_human"": ""SomeHumanDescription"", + ""description_for_model"": ""SomeModelDescription"", + ""auth"": { + ""type"": ""oauth"", + ""authorization_url"": ""https://api.openai.com/oauth/authorize"", + ""authorization_content_type"": ""application/json"", + ""client_url"": ""https://api.openai.com/oauth/token"", + ""scope"": ""all:all"", + ""verification_tokens"": { + ""openai"": ""dummy_verification_token"" + } + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}"; + + var doc = JsonDocument.Parse(json); + var manifest = OpenAIPluginManifest.Load(doc.RootElement); + + Assert.Equal("1.0.0", manifest.SchemaVersion); + Assert.Equal("TestOAuth", manifest.NameForHuman); + Assert.Equal("TestOAuthModel", manifest.NameForModel); + Assert.Equal("SomeHumanDescription", manifest.DescriptionForHuman); + Assert.Equal("SomeModelDescription", manifest.DescriptionForModel); + Assert.Equal("oauth", manifest.Auth?.Type); + Assert.Equal("https://api.openai.com/oauth/authorize", ((ManifestOAuthAuth)manifest.Auth)?.AuthorizationUrl); + Assert.Equal("application/json", ((ManifestOAuthAuth)manifest.Auth)?.AuthorizationContentType); + Assert.Equal("https://api.openai.com/oauth/token", ((ManifestOAuthAuth)manifest.Auth)?.ClientUrl); + Assert.Equal("all:all", ((ManifestOAuthAuth)manifest.Auth)?.Scope); + Assert.Equal("dummy_verification_token", ((ManifestOAuthAuth)manifest.Auth)?.VerificationTokens["OPENAI"]); + Assert.Equal("openapi", manifest.Api?.Type); + Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); + Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); + Assert.Equal("joe@test.com", manifest.ContactEmail); + } + + [Fact] + public void WriteOpenAIPluginManifestWithOAuth() { var manifest = new OpenAIPluginManifest { @@ -103,6 +154,13 @@ public void WriteOAuthTest() Auth = new ManifestOAuthAuth { AuthorizationUrl = "https://api.openai.com/oauth/authorize", + AuthorizationContentType = "application/json", + ClientUrl = "https://api.openai.com/oauth/token", + Scope = "all:all", + VerificationTokens = new OpenAI.Auth.VerificationTokens + { + { "openai", "dummy_verification_token" } + } }, Api = new Api { @@ -131,7 +189,101 @@ public void WriteOAuthTest() ""description_for_model"": ""SomeModelDescription"", ""auth"": { ""type"": ""oauth"", - ""authorization_url"": ""https://api.openai.com/oauth/authorize"" + ""client_url"": ""https://api.openai.com/oauth/token"", + ""scope"": ""all:all"", + ""authorization_url"": ""https://api.openai.com/oauth/authorize"", + ""authorization_content_type"": ""application/json"", + ""verification_tokens"": { + ""openai"": ""dummy_verification_token"" + } + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + } + + [Fact] + public void LoadOpenAIPluginManifestWithUserHttp() + { + var json = @"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""TestOAuth"", + ""name_for_model"": ""TestOAuthModel"", + ""description_for_human"": ""SomeHumanDescription"", + ""description_for_model"": ""SomeModelDescription"", + ""auth"": { + ""type"": ""user_http"", + ""authorization_type"": ""bearer"" + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}"; + + var doc = JsonDocument.Parse(json); + var manifest = OpenAIPluginManifest.Load(doc.RootElement); + + Assert.Equal("1.0.0", manifest.SchemaVersion); + Assert.Equal("TestOAuth", manifest.NameForHuman); + Assert.Equal("TestOAuthModel", manifest.NameForModel); + Assert.Equal("SomeHumanDescription", manifest.DescriptionForHuman); + Assert.Equal("SomeModelDescription", manifest.DescriptionForModel); + Assert.Equal("user_http", manifest.Auth?.Type); + Assert.Equal("bearer", ((ManifestUserHttpAuth)manifest.Auth)?.AuthorizationType); + Assert.Equal("openapi", manifest.Api?.Type); + Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); + Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); + Assert.Equal("joe@test.com", manifest.ContactEmail); + } + + [Fact] + public void WriteOpenAIPluginManifestWithUserHttp() + { + var manifest = new OpenAIPluginManifest + { + SchemaVersion = "1.0.0", + NameForHuman = "TestOAuth", + NameForModel = "TestOAuthModel", + DescriptionForHuman = "SomeHumanDescription", + DescriptionForModel = "SomeModelDescription", + Auth = new ManifestUserHttpAuth("bearer"), + Api = new Api + { + Type = "openapi", + Url = "https://api.openai.com/v1", + IsUserAuthenticated = false + }, + LogoUrl = "https://avatars.githubusercontent.com/bar", + ContactEmail = "joe@test.com" + }; + + // serialize using the Write method + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + manifest.Write(writer); + writer.Flush(); + stream.Position = 0; + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + Assert.Equal(@"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""TestOAuth"", + ""name_for_model"": ""TestOAuthModel"", + ""description_for_human"": ""SomeHumanDescription"", + ""description_for_model"": ""SomeModelDescription"", + ""auth"": { + ""type"": ""user_http"", + ""authorization_type"": ""bearer"" }, ""api"": { ""type"": ""openapi"", @@ -143,4 +295,101 @@ public void WriteOAuthTest() }", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } + [Fact] + public void LoadOpenAIPluginManifestWithServiceHttp() + { + var json = @"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""TestOAuth"", + ""name_for_model"": ""TestOAuthModel"", + ""description_for_human"": ""SomeHumanDescription"", + ""description_for_model"": ""SomeModelDescription"", + ""auth"": { + ""type"": ""service_http"", + ""authorization_type"": ""bearer"", + ""verification_tokens"": { + ""openai"": ""dummy_verification_token"" + } + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}"; + + var doc = JsonDocument.Parse(json); + var manifest = OpenAIPluginManifest.Load(doc.RootElement); + + Assert.Equal("1.0.0", manifest.SchemaVersion); + Assert.Equal("TestOAuth", manifest.NameForHuman); + Assert.Equal("TestOAuthModel", manifest.NameForModel); + Assert.Equal("SomeHumanDescription", manifest.DescriptionForHuman); + Assert.Equal("SomeModelDescription", manifest.DescriptionForModel); + Assert.Equal("service_http", manifest.Auth?.Type); + Assert.Equal("bearer", ((ManifestServiceHttpAuth)manifest.Auth)?.AuthorizationType); + Assert.Equal("dummy_verification_token", ((ManifestServiceHttpAuth)manifest.Auth)?.VerificationTokens["OPENAI"]); + Assert.Equal("openapi", manifest.Api?.Type); + Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); + Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); + Assert.Equal("joe@test.com", manifest.ContactEmail); + } + + [Fact] + public void WriteOpenAIPluginManifestWithServiceHttp() + { + var manifest = new OpenAIPluginManifest + { + SchemaVersion = "1.0.0", + NameForHuman = "TestOAuth", + NameForModel = "TestOAuthModel", + DescriptionForHuman = "SomeHumanDescription", + DescriptionForModel = "SomeModelDescription", + Auth = new ManifestServiceHttpAuth(new OpenAI.Auth.VerificationTokens + { + { "openai", "dummy_verification_token" } + }), + Api = new Api + { + Type = "openapi", + Url = "https://api.openai.com/v1", + IsUserAuthenticated = false + }, + LogoUrl = "https://avatars.githubusercontent.com/bar", + ContactEmail = "joe@test.com" + }; + + // serialize using the Write method + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + manifest.Write(writer); + writer.Flush(); + stream.Position = 0; + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + Assert.Equal(@"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""TestOAuth"", + ""name_for_model"": ""TestOAuthModel"", + ""description_for_human"": ""SomeHumanDescription"", + ""description_for_model"": ""SomeModelDescription"", + ""auth"": { + ""type"": ""service_http"", + ""authorization_type"": ""bearer"", + ""verification_tokens"": { + ""openai"": ""dummy_verification_token"" + } + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + } } diff --git a/tests/BasicTests.cs b/tests/BasicTests.cs deleted file mode 100644 index eda0a03..0000000 --- a/tests/BasicTests.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System.Diagnostics; -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace Microsoft.OpenApi.ApiManifest.Tests; - -public class BasicTests -{ - private readonly ApiManifestDocument exampleApiManifest; - public BasicTests() - { - exampleApiManifest = CreateDocument(); - } - - // Create test to instantiate a simple ApiManifestDocument - [Fact] - public void InitializeDocument() - { - Assert.NotNull(exampleApiManifest); - } - - // Serialize the ApiManifestDocument to a string - [Fact] - public void SerializeDocument() - { - var stream = new MemoryStream(); - var writer = new Utf8JsonWriter(stream); - exampleApiManifest.Write(writer); - writer.Flush(); - // Read string from stream - stream.Position = 0; - var reader = new StreamReader(stream); - var json = reader.ReadToEnd(); - Debug.WriteLine(json); - var doc = JsonDocument.Parse(json); - Assert.NotNull(doc); - Assert.Equal("application-name", doc.RootElement.GetProperty("applicationName").GetString()); - Assert.Equal("Microsoft", doc.RootElement.GetProperty("publisher").GetProperty("name").GetString()); - } - - // Deserialize the ApiManifestDocument from a string - [Fact] - public void DeserializeDocument() - { - var stream = new MemoryStream(); - var writer = new Utf8JsonWriter(stream); - exampleApiManifest.Write(writer); - writer.Flush(); - // Read string from stream - stream.Position = 0; - var reader = new StreamReader(stream); - var json = reader.ReadToEnd(); - var doc = JsonDocument.Parse(json); - var apiManifest = ApiManifestDocument.Load(doc.RootElement); - Assert.Equivalent(exampleApiManifest.Publisher, apiManifest.Publisher); - Assert.Equivalent(exampleApiManifest.ApiDependencies["example"].Requests, apiManifest.ApiDependencies["example"].Requests); - Assert.Equivalent(exampleApiManifest.ApiDependencies["example"].ApiDescriptionUrl, apiManifest.ApiDependencies["example"].ApiDescriptionUrl); - Assert.Equivalent(exampleApiManifest.ApiDependencies["example"].ApiDeploymentBaseUrl, apiManifest.ApiDependencies["example"].ApiDeploymentBaseUrl); - var expectedAuth = exampleApiManifest.ApiDependencies["example"].AuthorizationRequirements; - var actualAuth = apiManifest.ApiDependencies["example"].AuthorizationRequirements; - Assert.Equivalent(expectedAuth?.ClientIdentifier, actualAuth?.ClientIdentifier); - Assert.Equivalent(expectedAuth?.Access?[0]?.Content?.ToJsonString(), actualAuth?.Access?[0]?.Content?.ToJsonString()); - } - - - // Create an empty document - [Fact] - public void CreateDocumentWithRequiredFields() - { - var doc = new ApiManifestDocument("application-name"); - Assert.NotNull(doc); - Assert.Equal("application-name", doc.ApplicationName); - Assert.NotNull(doc.ApiDependencies); - Assert.Empty(doc.ApiDependencies); - } - - // Create a document with a publisher that is missing required fields (name and contactEmail). - [Fact] - public void CreateDocumentWithMissingRequiredPublisherFields() - { - _ = Assert.Throws(() => - { - var doc = new ApiManifestDocument("application-name") - { - Publisher = new("", "") - }; - } - ); - } - - [Fact] - public void FailToParseDocumentWithoutApplicationName() - { - _ = Assert.Throws(() => - { - var serializedValue = "{\"apiDependencies\": { \"graph\": {\"apiDescriptionUrl\":\"https://example.org\"}}}"; - var doc = JsonDocument.Parse(serializedValue); - _ = ApiManifestDocument.Load(doc.RootElement); - } - ); - } - - [Fact] - public void ParsesApiDescriptionUrlField() - { - // Given - var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"apiDescriptionUrl\":\"https://example.org\"}}}"; - var doc = JsonDocument.Parse(serializedValue); - - // When - var apiManifest = ApiManifestDocument.Load(doc.RootElement); - - // Then - Assert.Equal("https://example.org", apiManifest.ApiDependencies["graph"].ApiDescriptionUrl); - } - [Fact] - public void ParseApiDescriptionVersionField() - { - // Given - var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"apiDescriptionVersion\":\"v1.0\"}}}"; - var doc = JsonDocument.Parse(serializedValue); - - // When - var apiManifest = ApiManifestDocument.Load(doc.RootElement); - - // Then - Assert.Equal("v1.0", apiManifest.ApiDependencies["graph"].ApiDescriptionVersion); - } - [Fact] - public void ParsesApiDeploymentBaseUrl() - { - // Given - var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"apiDeploymentBaseUrl\":\"https://example.org/\"}}}"; - var doc = JsonDocument.Parse(serializedValue); - - // When - var apiManifest = ApiManifestDocument.Load(doc.RootElement); - - // Then - Assert.Equal("https://example.org/", apiManifest.ApiDependencies["graph"].ApiDeploymentBaseUrl); - } - - [Fact] - public void ParsesApiDeploymentBaseUrlWithDifferentCasing() - { - // Given - var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"APIDeploymentBaseUrl\":\"https://example.org/\"}}}"; - var doc = JsonDocument.Parse(serializedValue); - - // When - var apiManifest = ApiManifestDocument.Load(doc.RootElement); - - // Then - Assert.Equal("https://example.org/", apiManifest.ApiDependencies["graph"].ApiDeploymentBaseUrl); - } - - [Fact] - public void DoesNotFailOnExtraneousProperty() - { - // Given - var serializedValue = "{\"applicationName\": \"application-name\", \"apiDependencies\": { \"graph\": {\"APIDeploymentBaseUrl\":\"https://example.org/\", \"APISensitivity\":\"low\"}}}"; - var doc = JsonDocument.Parse(serializedValue); - - // When - var apiManifest = ApiManifestDocument.Load(doc.RootElement); - - // Then - Assert.Equal("https://example.org/", apiManifest.ApiDependencies["graph"].ApiDeploymentBaseUrl); - } - - private static ApiManifestDocument CreateDocument() - { - return new ApiManifestDocument("application-name") - { - Publisher = new("Microsoft", "example@example.org"), - ApiDependencies = new() { - { "example", new() - { - ApiDescriptionUrl = "https://example.org", - ApiDeploymentBaseUrl = "https://example.org/v1.0/", - AuthorizationRequirements = new() - { - ClientIdentifier = "1234", - Access = new() { - new () { Type= "application", Content = new JsonObject() { - { "scopes", new JsonArray() {"User.Read.All"} }} - } , - new () { Type= "delegated", Content = new JsonObject() { - { "scopes", new JsonArray() {"User.Read", "Mail.Read"} }} - } - } - }, - Requests = new() { - new () { Method = "GET", UriTemplate = "/api/v1/endpoint" }, - new () { Method = "POST", UriTemplate = "/api/v1/endpoint"} - } - } - } - } - }; - } - -} \ No newline at end of file diff --git a/tests/CreateTests.cs b/tests/CreateTests.cs deleted file mode 100644 index bd17cc8..0000000 --- a/tests/CreateTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Text.Json.Nodes; - -namespace Microsoft.OpenApi.ApiManifest.Tests; - -public class CreateTests -{ - - [Fact] - public void CreateApiManifestDocumentWithRequiredFields() - { - var apiManifest = new ApiManifestDocument("application-name"); - Assert.NotNull(apiManifest); - Assert.Equal("application-name", apiManifest.ApplicationName); - Assert.Null(apiManifest.Publisher); - Assert.Empty(apiManifest.ApiDependencies); - Assert.Empty(apiManifest.Extensions); - } - - [Theory] - [InlineData("foo@bar")] - [InlineData("foo@bar.com")] - public void CreatePublisher(string contactEmail) - { - var publisher = new Publisher(name: "Contoso", contactEmail: contactEmail); - Assert.Equal("Contoso", publisher.Name); - Assert.Equal(contactEmail, publisher.ContactEmail); - - } - - [Theory] - [InlineData("foo")] - [InlineData("foo@")] - [InlineData("foo@@bar.com")] - [InlineData("foo @bar.com")] - public void CreatePublisherWithInvalidEmail(string contactEmail) - { - _ = Assert.Throws(() => - { - var publisher = new Publisher(name: "Contoso", contactEmail: contactEmail); - } - ); - } - - [Theory] - [InlineData("foo")] - [InlineData("https://foo.com")] - [InlineData("http://128.0.0.0")] - [InlineData("https://foo@@bar.com")] - [InlineData("https://foo bar.com/")] - [InlineData("https://graph.microsoft.com/v1.0")] - public void CreateApiDependencyWithInvalidApiDeploymentBaseUrl(string apiDeploymentBaseUrl) - { - _ = Assert.Throws(() => - { - var apiDependency = new ApiDependency - { - ApiDeploymentBaseUrl = apiDeploymentBaseUrl - }; - } - ); - } - - // Create test to instantiate ApiManifest with auth - [Fact] - public void CreateApiManifestWithAuthorizationRequirements() - { - var apiManifest = new ApiManifestDocument("application-name") - { - Publisher = new(name: "Contoso", contactEmail: "foo@bar.com"), - ApiDependencies = new() { - { "Contoso.Api", new() { - ApiDeploymentBaseUrl = "https://api.contoso.com/", - AuthorizationRequirements = new() { - ClientIdentifier = "2143234-234324-234234234-234", - Access = new() { - new() { Type = "oauth2", - Content = new JsonObject() { - { "scopes", new JsonArray() { "user.read", "user.write" } } - } - } - } - } - } - } - } - }; - Assert.NotNull(apiManifest.ApiDependencies["Contoso.Api"].AuthorizationRequirements); - Assert.Equal("https://api.contoso.com/", apiManifest.ApiDependencies["Contoso.Api"].ApiDeploymentBaseUrl); - Assert.Equal("2143234-234324-234234234-234", apiManifest?.ApiDependencies["Contoso.Api"]?.AuthorizationRequirements?.ClientIdentifier); - Assert.Equal("oauth2", apiManifest?.ApiDependencies["Contoso.Api"]?.AuthorizationRequirements?.Access?[0].Type); - } - -} \ No newline at end of file diff --git a/tests/PluginTests.cs b/tests/PluginTests.cs deleted file mode 100644 index 71113fe..0000000 --- a/tests/PluginTests.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Write tests for OpenAIPluginManifest - -using Microsoft.OpenApi.ApiManifest.OpenAI; -using System.Text.Json; - -namespace Microsoft.OpenApi.ApiManifest.Tests.OpenAI -{ - public class OpenAIPluginManifestTests - { - [Fact] - public void LoadOpenAIPluginManifest() - { - var json = @"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""OpenAI GPT-3"", - ""name_for_model"": ""openai-gpt3"", - ""description_for_human"": ""OpenAI GPT-3 is a language model that generates text based on prompts."" , - ""description_for_model"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", - ""auth"": { - ""type"": ""none"" - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"" - }, - ""logo_url"": ""https://avatars.githubusercontent.com/foo"", - ""contact_email"": ""joe@demo.com"" - }"; - - var doc = JsonDocument.Parse(json); - var manifest = OpenAIPluginManifest.Load(doc.RootElement); - - Assert.Equal("1.0.0", manifest.SchemaVersion); - Assert.Equal("OpenAI GPT-3", manifest.NameForHuman); - Assert.Equal("openai-gpt3", manifest.NameForModel); - Assert.Equal("OpenAI GPT-3 is a language model that generates text based on prompts.", manifest.DescriptionForHuman); - Assert.Equal("OpenAI GPT-3 is a language model that generates text based on prompts.", manifest.DescriptionForModel); - Assert.Equal("none", manifest.Auth?.Type); - Assert.Equal("openapi", manifest.Api?.Type); - Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); - Assert.Equal("https://avatars.githubusercontent.com/foo", manifest.LogoUrl); - Assert.Equal("joe@demo.com", manifest.ContactEmail); - } - - // Create minimal OpenAIPluginManifest - [Fact] - public void WriteOpenAIPluginManifest() - { - var manifest = new OpenAIPluginManifest - { - SchemaVersion = "1.0.0", - NameForHuman = "OpenAI GPT-3", - NameForModel = "openai-gpt3", - DescriptionForHuman = "OpenAI GPT-3 is a language model that generates text based on prompts.", - DescriptionForModel = "OpenAI GPT-3 is a language model that generates text based on prompts.", - Auth = new ManifestNoAuth(), - Api = new Api - { - Type = "openapi", - Url = "https://api.openai.com/v1", - IsUserAuthenticated = false - }, - LogoUrl = "https://avatars.githubusercontent.com/bar", - ContactEmail = "joe@test.com" - }; - - // serialize using the Write method - var stream = new MemoryStream(); - var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); - manifest.Write(writer); - writer.Flush(); - stream.Position = 0; - var reader = new StreamReader(stream); - var json = reader.ReadToEnd(); - - Assert.Equal(@"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""OpenAI GPT-3"", - ""name_for_model"": ""openai-gpt3"", - ""description_for_human"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", - ""description_for_model"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", - ""auth"": { - ""type"": ""none"" - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); - } - - [Fact] - public void WriteOAuthTest() - { - var manifest = new OpenAIPluginManifest - { - SchemaVersion = "1.0.0", - NameForHuman = "TestOAuth", - NameForModel = "TestOAuthModel", - DescriptionForHuman = "SomeHumanDescription", - DescriptionForModel = "SomeModelDescription", - Auth = new ManifestOAuthAuth - { - AuthorizationUrl = "https://api.openai.com/oauth/authorize", - }, - Api = new Api - { - Type = "openapi", - Url = "https://api.openai.com/v1", - IsUserAuthenticated = false - }, - LogoUrl = "https://avatars.githubusercontent.com/bar", - ContactEmail = "joe@test.com" - }; - - // serialize using the Write method - var stream = new MemoryStream(); - var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); - manifest.Write(writer); - writer.Flush(); - stream.Position = 0; - var reader = new StreamReader(stream); - var json = reader.ReadToEnd(); - - Assert.Equal(@"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""TestOAuth"", - ""name_for_model"": ""TestOAuthModel"", - ""description_for_human"": ""SomeHumanDescription"", - ""description_for_model"": ""SomeModelDescription"", - ""auth"": { - ""type"": ""oauth"", - ""authorization_url"": ""https://api.openai.com/oauth/authorize"" - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); - } - - } - - -} \ No newline at end of file diff --git a/tests/Usings.cs b/tests/Usings.cs deleted file mode 100644 index c802f44..0000000 --- a/tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; From 29a33901477910ef97f51169fdbf101979ddd80e Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Tue, 3 Oct 2023 16:12:19 -0700 Subject: [PATCH 04/12] chore: Fix code smells. --- src/lib/OpenAI/Auth/ManifestNoAuth.cs | 2 +- src/lib/OpenAI/Auth/ManifestOAuthAuth.cs | 2 +- src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs | 2 +- src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs | 2 +- src/lib/OpenAI/OpenApiPluginFactory.cs | 2 +- src/lib/ParsingHelpers.cs | 9 +++++++-- .../OpenAIPluginManifestTests.cs | 16 ++++++++-------- 7 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/lib/OpenAI/Auth/ManifestNoAuth.cs b/src/lib/OpenAI/Auth/ManifestNoAuth.cs index cada494..ef41b73 100644 --- a/src/lib/OpenAI/Auth/ManifestNoAuth.cs +++ b/src/lib/OpenAI/Auth/ManifestNoAuth.cs @@ -12,7 +12,7 @@ public ManifestNoAuth() Type = "none"; } - internal static FixedFieldMap handlers = new() + internal static readonly FixedFieldMap handlers = new() { { "type", (o,v) => {o.Type = v.GetString(); } }, { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, diff --git a/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs b/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs index ccb908e..5cdbef5 100644 --- a/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs +++ b/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs @@ -18,7 +18,7 @@ public ManifestOAuthAuth() { Type = "oauth"; } - internal static FixedFieldMap handlers = new() + internal static readonly FixedFieldMap handlers = new() { { "type", (o,v) => {o.Type = v.GetString(); } }, { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, diff --git a/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs b/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs index 2494e8b..8a189e1 100644 --- a/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs +++ b/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs @@ -22,7 +22,7 @@ public ManifestServiceHttpAuth(VerificationTokens verificationTokens) VerificationTokens = verificationTokens; } - internal static FixedFieldMap handlers = new() + internal static readonly FixedFieldMap handlers = new() { { "type", (o,v) => {o.Type = v.GetString(); } }, { "authorization_type", (o,v) => {o.AuthorizationType = v.GetString(); } }, diff --git a/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs b/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs index ea232ad..d02e522 100644 --- a/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs +++ b/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs @@ -19,7 +19,7 @@ public ManifestUserHttpAuth(string? authorizationType) AuthorizationType = authorizationType; } - internal static FixedFieldMap handlers = new() + internal static readonly FixedFieldMap handlers = new() { { "type", (o,v) => {o.Type = v.GetString(); } }, { "authorization_type", (o,v) => {o.AuthorizationType = v.GetString(); } }, diff --git a/src/lib/OpenAI/OpenApiPluginFactory.cs b/src/lib/OpenAI/OpenApiPluginFactory.cs index 6e5fa63..8b1f8f9 100644 --- a/src/lib/OpenAI/OpenApiPluginFactory.cs +++ b/src/lib/OpenAI/OpenApiPluginFactory.cs @@ -4,7 +4,7 @@ namespace Microsoft.OpenApi.ApiManifest.OpenAI; -public class OpenApiPluginFactory +public static class OpenApiPluginFactory { public static OpenAIPluginManifest CreateOpenAIPluginManifest() diff --git a/src/lib/ParsingHelpers.cs b/src/lib/ParsingHelpers.cs index 88cc86e..43796d3 100644 --- a/src/lib/ParsingHelpers.cs +++ b/src/lib/ParsingHelpers.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Diagnostics; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest; -internal class ParsingHelpers +internal static class ParsingHelpers { public static void ParseMap(JsonElement node, T permissionsDocument, FixedFieldMap handlers) { @@ -15,7 +16,11 @@ public static void ParseMap(JsonElement node, T permissionsDocument, FixedFie { handler(permissionsDocument, element.Value); } - //TODO we should log the unknown property or use an additional properties model + else + { + // Logs the unknown property. We can switch to additional properties model in the future if need be. + Debug.WriteLine($"Skipped {element.Name}. The property is unknown."); + } }; } diff --git a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs index 28833ca..bdb9352 100644 --- a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs +++ b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs @@ -130,11 +130,11 @@ public void LoadOpenAIPluginManifestWithOAuth() Assert.Equal("SomeHumanDescription", manifest.DescriptionForHuman); Assert.Equal("SomeModelDescription", manifest.DescriptionForModel); Assert.Equal("oauth", manifest.Auth?.Type); - Assert.Equal("https://api.openai.com/oauth/authorize", ((ManifestOAuthAuth)manifest.Auth)?.AuthorizationUrl); - Assert.Equal("application/json", ((ManifestOAuthAuth)manifest.Auth)?.AuthorizationContentType); - Assert.Equal("https://api.openai.com/oauth/token", ((ManifestOAuthAuth)manifest.Auth)?.ClientUrl); - Assert.Equal("all:all", ((ManifestOAuthAuth)manifest.Auth)?.Scope); - Assert.Equal("dummy_verification_token", ((ManifestOAuthAuth)manifest.Auth)?.VerificationTokens["OPENAI"]); + Assert.Equal("https://api.openai.com/oauth/authorize", ((ManifestOAuthAuth?)manifest.Auth)?.AuthorizationUrl); + Assert.Equal("application/json", ((ManifestOAuthAuth?)manifest.Auth)?.AuthorizationContentType); + Assert.Equal("https://api.openai.com/oauth/token", ((ManifestOAuthAuth?)manifest.Auth)?.ClientUrl); + Assert.Equal("all:all", ((ManifestOAuthAuth?)manifest.Auth)?.Scope); + Assert.Equal("dummy_verification_token", ((ManifestOAuthAuth?)manifest.Auth)?.VerificationTokens["OPENAI"]); Assert.Equal("openapi", manifest.Api?.Type); Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); @@ -238,7 +238,7 @@ public void LoadOpenAIPluginManifestWithUserHttp() Assert.Equal("SomeHumanDescription", manifest.DescriptionForHuman); Assert.Equal("SomeModelDescription", manifest.DescriptionForModel); Assert.Equal("user_http", manifest.Auth?.Type); - Assert.Equal("bearer", ((ManifestUserHttpAuth)manifest.Auth)?.AuthorizationType); + Assert.Equal("bearer", ((ManifestUserHttpAuth?)manifest.Auth)?.AuthorizationType); Assert.Equal("openapi", manifest.Api?.Type); Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); @@ -329,8 +329,8 @@ public void LoadOpenAIPluginManifestWithServiceHttp() Assert.Equal("SomeHumanDescription", manifest.DescriptionForHuman); Assert.Equal("SomeModelDescription", manifest.DescriptionForModel); Assert.Equal("service_http", manifest.Auth?.Type); - Assert.Equal("bearer", ((ManifestServiceHttpAuth)manifest.Auth)?.AuthorizationType); - Assert.Equal("dummy_verification_token", ((ManifestServiceHttpAuth)manifest.Auth)?.VerificationTokens["OPENAI"]); + Assert.Equal("bearer", ((ManifestServiceHttpAuth?)manifest.Auth)?.AuthorizationType); + Assert.Equal("dummy_verification_token", ((ManifestServiceHttpAuth?)manifest.Auth)?.VerificationTokens["OPENAI"]); Assert.Equal("openapi", manifest.Api?.Type); Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); From a9ab5adbb36b8f16729b0bd8403c722300a32f96 Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Tue, 3 Oct 2023 16:46:22 -0700 Subject: [PATCH 05/12] chore: Remove empty statement. --- src/lib/ParsingHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/ParsingHelpers.cs b/src/lib/ParsingHelpers.cs index 43796d3..cac7a50 100644 --- a/src/lib/ParsingHelpers.cs +++ b/src/lib/ParsingHelpers.cs @@ -21,7 +21,7 @@ public static void ParseMap(JsonElement node, T permissionsDocument, FixedFie // Logs the unknown property. We can switch to additional properties model in the future if need be. Debug.WriteLine($"Skipped {element.Name}. The property is unknown."); } - }; + } } internal static List GetList(JsonElement v, Func load) From 8d373a02ec0b4e95cd37c85d22197eb1483b3ffd Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Fri, 6 Oct 2023 14:27:08 -0700 Subject: [PATCH 06/12] - refactor BaseManifestAuth class. --- src/lib/OpenAI/Api.cs | 15 +- src/lib/OpenAI/Auth/BaseManifestAuth.cs | 47 --- src/lib/OpenAI/Auth/ManifestNoAuth.cs | 28 -- src/lib/OpenAI/Auth/ManifestOAuthAuth.cs | 49 --- .../OpenAI/Authentication/BaseManifestAuth.cs | 43 +++ .../Authentication/ManifestAuthFactory.cs | 23 ++ .../OpenAI/Authentication/ManifestNoAuth.cs | 28 ++ .../Authentication/ManifestOAuthAuth.cs | 59 ++++ .../ManifestServiceHttpAuth.cs | 27 +- .../ManifestUserHttpAuth.cs | 19 +- .../VerificationTokens.cs | 2 +- src/lib/OpenAI/OpenAIPluginManifest.cs | 52 +-- src/lib/ParsingHelpers.cs | 2 +- .../ApiManifest.Tests.csproj | 8 +- .../OpenAIPluginManifestTests.cs | 331 +++++++++--------- 15 files changed, 400 insertions(+), 333 deletions(-) delete mode 100644 src/lib/OpenAI/Auth/BaseManifestAuth.cs delete mode 100644 src/lib/OpenAI/Auth/ManifestNoAuth.cs delete mode 100644 src/lib/OpenAI/Auth/ManifestOAuthAuth.cs create mode 100644 src/lib/OpenAI/Authentication/BaseManifestAuth.cs create mode 100644 src/lib/OpenAI/Authentication/ManifestAuthFactory.cs create mode 100644 src/lib/OpenAI/Authentication/ManifestNoAuth.cs create mode 100644 src/lib/OpenAI/Authentication/ManifestOAuthAuth.cs rename src/lib/OpenAI/{Auth => Authentication}/ManifestServiceHttpAuth.cs (53%) rename src/lib/OpenAI/{Auth => Authentication}/ManifestUserHttpAuth.cs (61%) rename src/lib/OpenAI/{Auth => Authentication}/VerificationTokens.cs (92%) diff --git a/src/lib/OpenAI/Api.cs b/src/lib/OpenAI/Api.cs index e01408b..15010c1 100644 --- a/src/lib/OpenAI/Api.cs +++ b/src/lib/OpenAI/Api.cs @@ -8,6 +8,9 @@ namespace Microsoft.OpenApi.ApiManifest.OpenAI; public class Api { + private const string TypeProperty = "type"; + private const string UrlProperty = "url"; + private const string IsUserAuthenticatedProperty = "is_user_authenticated"; public string? Type { get; set; } public string? Url { get; set; } public bool? IsUserAuthenticated { get; set; } @@ -22,17 +25,17 @@ public static Api Load(JsonElement value) // Create handlers FixedFieldMap for Api private static readonly FixedFieldMap handlers = new() { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "url", (o,v) => {o.Url = v.GetString(); } }, - { "is_user_authenticated", (o,v) => {o.IsUserAuthenticated = v.GetBoolean(); }}, + { TypeProperty, (o,v) => {o.Type = v.GetString(); } }, + { UrlProperty, (o,v) => {o.Url = v.GetString(); } }, + { IsUserAuthenticatedProperty, (o,v) => {o.IsUserAuthenticated = v.GetBoolean(); }}, }; public void Write(Utf8JsonWriter writer) { writer.WriteStartObject(); - writer.WriteString("type", Type); - writer.WriteString("url", Url); - writer.WriteBoolean("is_user_authenticated", IsUserAuthenticated ?? false); + writer.WriteString(TypeProperty, Type); + writer.WriteString(UrlProperty, Url); + writer.WriteBoolean(IsUserAuthenticatedProperty, IsUserAuthenticated ?? false); writer.WriteEndObject(); } } diff --git a/src/lib/OpenAI/Auth/BaseManifestAuth.cs b/src/lib/OpenAI/Auth/BaseManifestAuth.cs deleted file mode 100644 index c80f8a0..0000000 --- a/src/lib/OpenAI/Auth/BaseManifestAuth.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.OpenApi.ApiManifest.OpenAI.Auth; -using System.Text.Json; - -namespace Microsoft.OpenApi.ApiManifest.OpenAI; - -public abstract class BaseManifestAuth -{ - public string? Type { get; set; } - public string? Instructions { get; set; } - - public static BaseManifestAuth? Load(JsonElement value) - { - BaseManifestAuth? auth = null; - - switch (value.GetProperty("type").GetString()) - { - case "none": - auth = new ManifestNoAuth(); - ParsingHelpers.ParseMap(value, (ManifestNoAuth)auth, ManifestNoAuth.handlers); - break; - case "user_http": - var authorizationType = value.GetProperty("authorization_type").GetString(); - auth = new ManifestUserHttpAuth(authorizationType); - ParsingHelpers.ParseMap(value, (ManifestUserHttpAuth)auth, ManifestUserHttpAuth.handlers); - break; - case "service_http": - var verificationTokens = value.GetProperty("verification_tokens"); - auth = new ManifestServiceHttpAuth(VerificationTokens.Load(verificationTokens)); - ParsingHelpers.ParseMap(value, (ManifestServiceHttpAuth)auth, ManifestServiceHttpAuth.handlers); - break; - case "oauth": - auth = new ManifestOAuthAuth(); - ParsingHelpers.ParseMap(value, (ManifestOAuthAuth)auth, ManifestOAuthAuth.handlers); - break; - } - - return auth; - } - - // Create handlers FixedFieldMap for ManifestAuth - - public virtual void Write(Utf8JsonWriter writer) { } - -} diff --git a/src/lib/OpenAI/Auth/ManifestNoAuth.cs b/src/lib/OpenAI/Auth/ManifestNoAuth.cs deleted file mode 100644 index ef41b73..0000000 --- a/src/lib/OpenAI/Auth/ManifestNoAuth.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -using System.Text.Json; - -namespace Microsoft.OpenApi.ApiManifest.OpenAI; - -public class ManifestNoAuth : BaseManifestAuth -{ - public ManifestNoAuth() - { - Type = "none"; - } - - internal static readonly FixedFieldMap handlers = new() - { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, - }; - - public override void Write(Utf8JsonWriter writer) - { - writer.WriteStartObject(); - writer.WriteString("type", Type); - if (Instructions != null) writer.WriteString("instructions", Instructions); - writer.WriteEndObject(); - } -} diff --git a/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs b/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs deleted file mode 100644 index 5cdbef5..0000000 --- a/src/lib/OpenAI/Auth/ManifestOAuthAuth.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.OpenApi.ApiManifest.OpenAI.Auth; -using System.Text.Json; - -namespace Microsoft.OpenApi.ApiManifest.OpenAI; - -public class ManifestOAuthAuth : BaseManifestAuth -{ - public string? ClientUrl { get; set; } - public string? Scope { get; set; } - public string? AuthorizationUrl { get; set; } - public string? AuthorizationContentType { get; set; } - public VerificationTokens VerificationTokens { get; set; } = new VerificationTokens(); - - public ManifestOAuthAuth() - { - Type = "oauth"; - } - internal static readonly FixedFieldMap handlers = new() - { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, - { "client_url", (o,v) => {o.ClientUrl = v.GetString(); } }, - { "scope", (o,v) => {o.Scope = v.GetString(); } }, - { "authorization_url", (o,v) => {o.AuthorizationUrl = v.GetString(); } }, - { "authorization_content_type", (o,v) => {o.AuthorizationContentType = v.GetString(); } }, - { "verification_tokens", (o,v) => { o.VerificationTokens = VerificationTokens.Load(v); } }, - }; - - public override void Write(Utf8JsonWriter writer) - { - writer.WriteStartObject(); - writer.WriteString("type", Type); - - if (Instructions != null) writer.WriteString("instructions", Instructions); - if (ClientUrl != null) writer.WriteString("client_url", ClientUrl); - if (Scope != null) writer.WriteString("scope", Scope); - if (AuthorizationUrl != null) writer.WriteString("authorization_url", AuthorizationUrl); - if (AuthorizationContentType != null) writer.WriteString("authorization_content_type", AuthorizationContentType); - if (VerificationTokens.Any()) - { - writer.WritePropertyName("verification_tokens"); - VerificationTokens.Write(writer); - } - writer.WriteEndObject(); - } -} diff --git a/src/lib/OpenAI/Authentication/BaseManifestAuth.cs b/src/lib/OpenAI/Authentication/BaseManifestAuth.cs new file mode 100644 index 0000000..261b2f3 --- /dev/null +++ b/src/lib/OpenAI/Authentication/BaseManifestAuth.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; + +public abstract class BaseManifestAuth +{ + private const string TypeProperty = "type"; + private const string InstructionsProperty = "instructions"; + + public string? Type { get; internal set; } + public string? Instructions { get; set; } + + // Create handlers FixedFieldMap for BaseManifestAuth properties. + private static readonly FixedFieldMap handlers = new() + { + { TypeProperty, (o,v) => {o.Type = v.GetString(); } }, + { InstructionsProperty, (o,v) => {o.Instructions = v.GetString(); } } + }; + + /// + /// Loads the common properties for all authentication types. + /// + /// The to parse. + protected void LoadProperties(JsonElement value) + { + ParsingHelpers.ParseMap(value, this, handlers); + } + + /// + /// Write the common properties for all authentication types. This method does not write the opening and closing object tags. + /// + /// The to use. + protected void WriteProperties(Utf8JsonWriter writer) + { + writer.WriteString(TypeProperty, Type); + if (!string.IsNullOrWhiteSpace(Instructions)) writer.WriteString(InstructionsProperty, Instructions); + } + + public virtual void Write(Utf8JsonWriter writer) { } +} diff --git a/src/lib/OpenAI/Authentication/ManifestAuthFactory.cs b/src/lib/OpenAI/Authentication/ManifestAuthFactory.cs new file mode 100644 index 0000000..a2823ae --- /dev/null +++ b/src/lib/OpenAI/Authentication/ManifestAuthFactory.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication +{ + internal static class ManifestAuthFactory + { + public static BaseManifestAuth CreateManifestAuth(JsonElement value) + { + var authType = value.GetProperty("type").GetString()?.ToLowerInvariant(); + return authType switch + { + "none" => ManifestNoAuth.Load(value), + "user_http" => ManifestUserHttpAuth.Load(value), + "service_http" => ManifestServiceHttpAuth.Load(value), + "oauth" => ManifestOAuthAuth.Load(value), + _ => throw new ArgumentOutOfRangeException(nameof(value), $"Unknown auth type: {authType}") + }; + } + } +} diff --git a/src/lib/OpenAI/Authentication/ManifestNoAuth.cs b/src/lib/OpenAI/Authentication/ManifestNoAuth.cs new file mode 100644 index 0000000..ede769c --- /dev/null +++ b/src/lib/OpenAI/Authentication/ManifestNoAuth.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; + +public class ManifestNoAuth : BaseManifestAuth +{ + public ManifestNoAuth() + { + Type = "none"; + } + + public static ManifestNoAuth Load(JsonElement value) + { + var auth = new ManifestNoAuth(); + auth.LoadProperties(value); + return auth; + } + + public override void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + WriteProperties(writer); + writer.WriteEndObject(); + } +} diff --git a/src/lib/OpenAI/Authentication/ManifestOAuthAuth.cs b/src/lib/OpenAI/Authentication/ManifestOAuthAuth.cs new file mode 100644 index 0000000..fc2cca5 --- /dev/null +++ b/src/lib/OpenAI/Authentication/ManifestOAuthAuth.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; + +public class ManifestOAuthAuth : BaseManifestAuth +{ + private const string ClientUrlPropertyName = "client_url"; + private const string ScopePropertyName = "scope"; + private const string AuthorizationUrlPropertyName = "authorization_url"; + private const string AuthorizationContentTypePropertyName = "authorization_content_type"; + private const string VerificationTokensPropertyName = "verification_tokens"; + + public string? ClientUrl { get; set; } + public string? Scope { get; set; } + public string? AuthorizationUrl { get; set; } + public string? AuthorizationContentType { get; set; } + public VerificationTokens VerificationTokens { get; set; } = new VerificationTokens(); + + public ManifestOAuthAuth() + { + Type = "oauth"; + } + + private static readonly FixedFieldMap handlers = new() + { + { ClientUrlPropertyName, (o,v) => {o.ClientUrl = v.GetString(); } }, + { ScopePropertyName, (o,v) => {o.Scope = v.GetString(); } }, + { AuthorizationUrlPropertyName, (o,v) => {o.AuthorizationUrl = v.GetString(); } }, + { AuthorizationContentTypePropertyName, (o,v) => {o.AuthorizationContentType = v.GetString(); } }, + { VerificationTokensPropertyName, (o,v) => { o.VerificationTokens = VerificationTokens.Load(v); } }, + }; + + public static ManifestOAuthAuth Load(JsonElement value) + { + var auth = new ManifestOAuthAuth(); + auth.LoadProperties(value); + ParsingHelpers.ParseMap(value, auth, handlers); + return auth; + } + + public override void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + WriteProperties(writer); + if (!string.IsNullOrWhiteSpace(ClientUrl)) writer.WriteString(ClientUrlPropertyName, ClientUrl); + if (!string.IsNullOrWhiteSpace(Scope)) writer.WriteString(ScopePropertyName, Scope); + if (!string.IsNullOrWhiteSpace(AuthorizationUrl)) writer.WriteString(AuthorizationUrlPropertyName, AuthorizationUrl); + if (!string.IsNullOrWhiteSpace(AuthorizationContentType)) writer.WriteString(AuthorizationContentTypePropertyName, AuthorizationContentType); + if (VerificationTokens.Any()) + { + writer.WritePropertyName(VerificationTokensPropertyName); + VerificationTokens.Write(writer); + } + writer.WriteEndObject(); + } +} diff --git a/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs b/src/lib/OpenAI/Authentication/ManifestServiceHttpAuth.cs similarity index 53% rename from src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs rename to src/lib/OpenAI/Authentication/ManifestServiceHttpAuth.cs index 8a189e1..19d94f9 100644 --- a/src/lib/OpenAI/Auth/ManifestServiceHttpAuth.cs +++ b/src/lib/OpenAI/Authentication/ManifestServiceHttpAuth.cs @@ -1,13 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using Microsoft.OpenApi.ApiManifest.OpenAI.Auth; using System.Text.Json; -namespace Microsoft.OpenApi.ApiManifest.OpenAI; +namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; public class ManifestServiceHttpAuth : BaseManifestAuth { + private const string AuthorizationTypeProperty = "authorization_type"; + private const string VerificationTokensProperty = "verification_tokens"; public string? AuthorizationType { get; set; } public VerificationTokens VerificationTokens { get; set; } public ManifestServiceHttpAuth(VerificationTokens verificationTokens) @@ -22,21 +23,25 @@ public ManifestServiceHttpAuth(VerificationTokens verificationTokens) VerificationTokens = verificationTokens; } - internal static readonly FixedFieldMap handlers = new() + private static readonly FixedFieldMap handlers = new() { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "authorization_type", (o,v) => {o.AuthorizationType = v.GetString(); } }, - { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, - { "verification_tokens", (o, v) => { o.VerificationTokens = VerificationTokens.Load(v); } } + { AuthorizationTypeProperty, (o,v) => {o.AuthorizationType = v.GetString(); } } }; + public static ManifestServiceHttpAuth Load(JsonElement value) + { + var auth = new ManifestServiceHttpAuth(VerificationTokens.Load(value.GetProperty(VerificationTokensProperty))); + auth.LoadProperties(value); + ParsingHelpers.ParseMap(value, auth, handlers); + return auth; + } + public override void Write(Utf8JsonWriter writer) { writer.WriteStartObject(); - writer.WriteString("type", Type); - writer.WriteString("authorization_type", AuthorizationType); - if (Instructions != null) writer.WriteString("instructions", Instructions); - writer.WritePropertyName("verification_tokens"); + WriteProperties(writer); + writer.WriteString(AuthorizationTypeProperty, AuthorizationType); + writer.WritePropertyName(VerificationTokensProperty); VerificationTokens.Write(writer); writer.WriteEndObject(); } diff --git a/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs b/src/lib/OpenAI/Authentication/ManifestUserHttpAuth.cs similarity index 61% rename from src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs rename to src/lib/OpenAI/Authentication/ManifestUserHttpAuth.cs index d02e522..45692c8 100644 --- a/src/lib/OpenAI/Auth/ManifestUserHttpAuth.cs +++ b/src/lib/OpenAI/Authentication/ManifestUserHttpAuth.cs @@ -3,10 +3,11 @@ using System.Text.Json; -namespace Microsoft.OpenApi.ApiManifest.OpenAI; +namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; public class ManifestUserHttpAuth : BaseManifestAuth { + private const string AuthorizationTypeProperty = "authorization_type"; public string? AuthorizationType { get; set; } public ManifestUserHttpAuth(string? authorizationType) { @@ -19,18 +20,18 @@ public ManifestUserHttpAuth(string? authorizationType) AuthorizationType = authorizationType; } - internal static readonly FixedFieldMap handlers = new() + public static ManifestUserHttpAuth Load(JsonElement value) { - { "type", (o,v) => {o.Type = v.GetString(); } }, - { "authorization_type", (o,v) => {o.AuthorizationType = v.GetString(); } }, - { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, - }; + var auth = new ManifestUserHttpAuth(value.GetProperty(AuthorizationTypeProperty).GetString()); + auth.LoadProperties(value); + return auth; + } + public override void Write(Utf8JsonWriter writer) { writer.WriteStartObject(); - writer.WriteString("type", Type); - writer.WriteString("authorization_type", AuthorizationType); - if (Instructions != null) writer.WriteString("instructions", Instructions); + WriteProperties(writer); + writer.WriteString(AuthorizationTypeProperty, AuthorizationType); writer.WriteEndObject(); } } diff --git a/src/lib/OpenAI/Auth/VerificationTokens.cs b/src/lib/OpenAI/Authentication/VerificationTokens.cs similarity index 92% rename from src/lib/OpenAI/Auth/VerificationTokens.cs rename to src/lib/OpenAI/Authentication/VerificationTokens.cs index b8cd438..8cee586 100644 --- a/src/lib/OpenAI/Auth/VerificationTokens.cs +++ b/src/lib/OpenAI/Authentication/VerificationTokens.cs @@ -4,7 +4,7 @@ using System.Text.Json; -namespace Microsoft.OpenApi.ApiManifest.OpenAI.Auth; +namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; public class VerificationTokens : Dictionary { diff --git a/src/lib/OpenAI/OpenAIPluginManifest.cs b/src/lib/OpenAI/OpenAIPluginManifest.cs index 65fe74c..19239f1 100644 --- a/src/lib/OpenAI/OpenAIPluginManifest.cs +++ b/src/lib/OpenAI/OpenAIPluginManifest.cs @@ -2,12 +2,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.OpenAI; public class OpenAIPluginManifest { + private const string SchemaVersionProperty = "schema_version"; + private const string NameForHumanProperty = "name_for_human"; + private const string NameForModelProperty = "name_for_model"; + private const string DescriptionForHumanProperty = "description_for_human"; + private const string DescriptionForModelProperty = "description_for_model"; + private const string AuthProperty = "auth"; + private const string ApiProperty = "api"; + private const string LogoUrlProperty = "logo_url"; + private const string ContactEmailProperty = "contact_email"; + private const string LegalInfoUrlProperty = "legal_info_url"; + public string? SchemaVersion { get; set; } public string? NameForHuman { get; set; } public string? NameForModel { get; set; } @@ -34,40 +46,40 @@ public static OpenAIPluginManifest Load(JsonElement value) // Create handlers FixedFieldMap for OpenAIPluginManifest private static readonly FixedFieldMap handlers = new() { - { "schema_version", (o,v) => {o.SchemaVersion = v.GetString(); } }, - { "name_for_human", (o,v) => {o.NameForHuman = v.GetString(); } }, - { "name_for_model", (o,v) => {o.NameForModel = v.GetString(); } }, - { "description_for_human", (o,v) => {o.DescriptionForHuman = v.GetString(); } }, - { "description_for_model", (o,v) => {o.DescriptionForModel = v.GetString(); } }, - { "auth", (o,v) => {o.Auth = BaseManifestAuth.Load(v); } }, - { "api", (o,v) => {o.Api = Api.Load(v); } }, - { "logo_url", (o,v) => {o.LogoUrl = v.GetString(); } }, - { "contact_email", (o,v) => {o.ContactEmail = v.GetString(); } }, - { "legal_info_url", (o,v) => {o.LegalInfoUrl = v.GetString(); } }, + { SchemaVersionProperty, (o,v) => {o.SchemaVersion = v.GetString(); } }, + { NameForHumanProperty, (o,v) => {o.NameForHuman = v.GetString(); } }, + { NameForModelProperty, (o,v) => {o.NameForModel = v.GetString(); } }, + { DescriptionForHumanProperty, (o,v) => {o.DescriptionForHuman = v.GetString(); } }, + { DescriptionForModelProperty, (o,v) => {o.DescriptionForModel = v.GetString(); } }, + { AuthProperty, (o,v) => {o.Auth = ManifestAuthFactory.CreateManifestAuth(v); } }, + { ApiProperty, (o,v) => {o.Api = Api.Load(v); } }, + { LogoUrlProperty, (o,v) => {o.LogoUrl = v.GetString(); } }, + { ContactEmailProperty, (o,v) => {o.ContactEmail = v.GetString(); } }, + { LegalInfoUrlProperty, (o,v) => {o.LegalInfoUrl = v.GetString(); } }, }; //Write method public void Write(Utf8JsonWriter writer) { writer.WriteStartObject(); - writer.WriteString("schema_version", SchemaVersion); - writer.WriteString("name_for_human", NameForHuman); - writer.WriteString("name_for_model", NameForModel); - writer.WriteString("description_for_human", DescriptionForHuman); - writer.WriteString("description_for_model", DescriptionForModel); + writer.WriteString(SchemaVersionProperty, SchemaVersion); + writer.WriteString(NameForHumanProperty, NameForHuman); + writer.WriteString(NameForModelProperty, NameForModel); + writer.WriteString(DescriptionForHumanProperty, DescriptionForHuman); + writer.WriteString(DescriptionForModelProperty, DescriptionForModel); if (Auth != null) { - writer.WritePropertyName("auth"); + writer.WritePropertyName(AuthProperty); Auth.Write(writer); } if (Api != null) { - writer.WritePropertyName("api"); + writer.WritePropertyName(ApiProperty); Api?.Write(writer); } - if (LogoUrl != null) writer.WriteString("logo_url", LogoUrl); - if (ContactEmail != null) writer.WriteString("contact_email", ContactEmail); - if (LegalInfoUrl != null) writer.WriteString("legal_info_url", LegalInfoUrl); + if (LogoUrl != null) writer.WriteString(LogoUrlProperty, LogoUrl); + if (ContactEmail != null) writer.WriteString(ContactEmailProperty, ContactEmail); + if (LegalInfoUrl != null) writer.WriteString(LegalInfoUrlProperty, LegalInfoUrl); writer.WriteEndObject(); } } diff --git a/src/lib/ParsingHelpers.cs b/src/lib/ParsingHelpers.cs index cac7a50..d5abbaa 100644 --- a/src/lib/ParsingHelpers.cs +++ b/src/lib/ParsingHelpers.cs @@ -50,7 +50,7 @@ internal static Dictionary GetMapOfString(JsonElement v) foreach (var item in v.EnumerateObject()) { var value = item.Value.GetString(); - map.Add(item.Name, value ?? string.Empty); + map.Add(item.Name, !string.IsNullOrWhiteSpace(value) ? value : string.Empty); } return map; } diff --git a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj index b7424c5..bc65642 100644 --- a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj +++ b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj @@ -13,13 +13,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs index bdb9352..ce54a55 100644 --- a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs +++ b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.OpenApi.ApiManifest.OpenAI; +using Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.Tests; @@ -11,22 +12,24 @@ public class OpenAIPluginManifestTests [Fact] public void LoadOpenAIPluginManifest() { - var json = @"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""OpenAI GPT-3"", - ""name_for_model"": ""openai-gpt3"", - ""description_for_human"": ""OpenAI GPT-3 is a language model that generates text based on prompts."" , - ""description_for_model"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", - ""auth"": { - ""type"": ""none"" - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"" - }, - ""logo_url"": ""https://avatars.githubusercontent.com/foo"", - ""contact_email"": ""joe@demo.com"" - }"; + var json = """ + { + "schema_version": "1.0.0", + "name_for_human": "OpenAI GPT-3", + "name_for_model": "openai-gpt3", + "description_for_human": "OpenAI GPT-3 is a language model that generates text based on prompts." , + "description_for_model": "OpenAI GPT-3 is a language model that generates text based on prompts.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1" + }, + "logo_url": "https://avatars.githubusercontent.com/foo", + "contact_email": "joe@demo.com" + } + """; var doc = JsonDocument.Parse(json); var manifest = OpenAIPluginManifest.Load(doc.RootElement); @@ -74,52 +77,56 @@ public void WriteOpenAIPluginManifest() var reader = new StreamReader(stream); var json = reader.ReadToEnd(); - Assert.Equal(@"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""OpenAI GPT-3"", - ""name_for_model"": ""openai-gpt3"", - ""description_for_human"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", - ""description_for_model"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", - ""auth"": { - ""type"": ""none"" - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + Assert.Equal(""" + { + "schema_version": "1.0.0", + "name_for_human": "OpenAI GPT-3", + "name_for_model": "openai-gpt3", + "description_for_human": "OpenAI GPT-3 is a language model that generates text based on prompts.", + "description_for_model": "OpenAI GPT-3 is a language model that generates text based on prompts.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com" + } + """, json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } [Fact] public void LoadOpenAIPluginManifestWithOAuth() { - var json = @"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""TestOAuth"", - ""name_for_model"": ""TestOAuthModel"", - ""description_for_human"": ""SomeHumanDescription"", - ""description_for_model"": ""SomeModelDescription"", - ""auth"": { - ""type"": ""oauth"", - ""authorization_url"": ""https://api.openai.com/oauth/authorize"", - ""authorization_content_type"": ""application/json"", - ""client_url"": ""https://api.openai.com/oauth/token"", - ""scope"": ""all:all"", - ""verification_tokens"": { - ""openai"": ""dummy_verification_token"" + var json = """ + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "oauth", + "authorization_url": "https://api.openai.com/oauth/authorize", + "authorization_content_type": "application/json", + "client_url": "https://api.openai.com/oauth/token", + "scope": "all:all", + "verification_tokens": { + "openai": "dummy_verification_token" + } + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com" } - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}"; + """; var doc = JsonDocument.Parse(json); var manifest = OpenAIPluginManifest.Load(doc.RootElement); @@ -157,7 +164,7 @@ public void WriteOpenAIPluginManifestWithOAuth() AuthorizationContentType = "application/json", ClientUrl = "https://api.openai.com/oauth/token", Scope = "all:all", - VerificationTokens = new OpenAI.Auth.VerificationTokens + VerificationTokens = new VerificationTokens { { "openai", "dummy_verification_token" } } @@ -181,53 +188,57 @@ public void WriteOpenAIPluginManifestWithOAuth() var reader = new StreamReader(stream); var json = reader.ReadToEnd(); - Assert.Equal(@"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""TestOAuth"", - ""name_for_model"": ""TestOAuthModel"", - ""description_for_human"": ""SomeHumanDescription"", - ""description_for_model"": ""SomeModelDescription"", - ""auth"": { - ""type"": ""oauth"", - ""client_url"": ""https://api.openai.com/oauth/token"", - ""scope"": ""all:all"", - ""authorization_url"": ""https://api.openai.com/oauth/authorize"", - ""authorization_content_type"": ""application/json"", - ""verification_tokens"": { - ""openai"": ""dummy_verification_token"" + Assert.Equal(""" + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "oauth", + "client_url": "https://api.openai.com/oauth/token", + "scope": "all:all", + "authorization_url": "https://api.openai.com/oauth/authorize", + "authorization_content_type": "application/json", + "verification_tokens": { + "openai": "dummy_verification_token" + } + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com" } - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + """, json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } [Fact] public void LoadOpenAIPluginManifestWithUserHttp() { - var json = @"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""TestOAuth"", - ""name_for_model"": ""TestOAuthModel"", - ""description_for_human"": ""SomeHumanDescription"", - ""description_for_model"": ""SomeModelDescription"", - ""auth"": { - ""type"": ""user_http"", - ""authorization_type"": ""bearer"" - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}"; + var json = """ + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "user_http", + "authorization_type": "bearer" + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com" + } + """; var doc = JsonDocument.Parse(json); var manifest = OpenAIPluginManifest.Load(doc.RootElement); @@ -275,50 +286,54 @@ public void WriteOpenAIPluginManifestWithUserHttp() var reader = new StreamReader(stream); var json = reader.ReadToEnd(); - Assert.Equal(@"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""TestOAuth"", - ""name_for_model"": ""TestOAuthModel"", - ""description_for_human"": ""SomeHumanDescription"", - ""description_for_model"": ""SomeModelDescription"", - ""auth"": { - ""type"": ""user_http"", - ""authorization_type"": ""bearer"" - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + Assert.Equal(""" + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "user_http", + "authorization_type": "bearer" + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com" + } + """, json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } [Fact] public void LoadOpenAIPluginManifestWithServiceHttp() { - var json = @"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""TestOAuth"", - ""name_for_model"": ""TestOAuthModel"", - ""description_for_human"": ""SomeHumanDescription"", - ""description_for_model"": ""SomeModelDescription"", - ""auth"": { - ""type"": ""service_http"", - ""authorization_type"": ""bearer"", - ""verification_tokens"": { - ""openai"": ""dummy_verification_token"" + var json = """ + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "service_http", + "authorization_type": "bearer", + "verification_tokens": { + "openai": "dummy_verification_token" + } + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com" } - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}"; + """; var doc = JsonDocument.Parse(json); var manifest = OpenAIPluginManifest.Load(doc.RootElement); @@ -347,7 +362,7 @@ public void WriteOpenAIPluginManifestWithServiceHttp() NameForModel = "TestOAuthModel", DescriptionForHuman = "SomeHumanDescription", DescriptionForModel = "SomeModelDescription", - Auth = new ManifestServiceHttpAuth(new OpenAI.Auth.VerificationTokens + Auth = new ManifestServiceHttpAuth(new VerificationTokens { { "openai", "dummy_verification_token" } }), @@ -370,26 +385,28 @@ public void WriteOpenAIPluginManifestWithServiceHttp() var reader = new StreamReader(stream); var json = reader.ReadToEnd(); - Assert.Equal(@"{ - ""schema_version"": ""1.0.0"", - ""name_for_human"": ""TestOAuth"", - ""name_for_model"": ""TestOAuthModel"", - ""description_for_human"": ""SomeHumanDescription"", - ""description_for_model"": ""SomeModelDescription"", - ""auth"": { - ""type"": ""service_http"", - ""authorization_type"": ""bearer"", - ""verification_tokens"": { - ""openai"": ""dummy_verification_token"" + Assert.Equal(""" + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "service_http", + "authorization_type": "bearer", + "verification_tokens": { + "openai": "dummy_verification_token" + } + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com" } - }, - ""api"": { - ""type"": ""openapi"", - ""url"": ""https://api.openai.com/v1"", - ""is_user_authenticated"": false - }, - ""logo_url"": ""https://avatars.githubusercontent.com/bar"", - ""contact_email"": ""joe@test.com"" -}", json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + """, json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } } From 52a56f40e091cbe27a0b8dc0243e756923d17bfd Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Fri, 6 Oct 2023 14:59:55 -0700 Subject: [PATCH 07/12] chore: Adds missing coverlet.msbuild --- tests/ApiManifest.Tests/ApiManifest.Tests.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj index bc65642..63efb30 100644 --- a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj +++ b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj @@ -13,6 +13,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 653400abff12a9191ff3994d0b2c68263883075e Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 9 Oct 2023 16:01:29 -0700 Subject: [PATCH 08/12] - Adds OpenAI Plugin manifest validation --- src/lib/ApiDependency.cs | 15 +- src/lib/ApiManifestDocument.cs | 6 +- src/lib/Constants.cs | 7 +- src/lib/Helpers/ParsingHelpers.cs | 147 +++++++++++ src/lib/Helpers/ValidationHelpers.cs | 47 ++++ src/lib/OpenAI/OpenAIPluginManifest.cs | 121 +++++++-- src/lib/OpenAI/OpenApiPluginFactory.cs | 10 +- src/lib/Publisher.cs | 16 +- .../OpenAIPluginManifestTests.cs | 238 +++++++++++------- 9 files changed, 459 insertions(+), 148 deletions(-) create mode 100644 src/lib/Helpers/ParsingHelpers.cs create mode 100644 src/lib/Helpers/ValidationHelpers.cs diff --git a/src/lib/ApiDependency.cs b/src/lib/ApiDependency.cs index cb461ff..c36e85d 100644 --- a/src/lib/ApiDependency.cs +++ b/src/lib/ApiDependency.cs @@ -1,3 +1,4 @@ +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest; @@ -6,11 +7,12 @@ public class ApiDependency public string? ApiDescriptionUrl { get; set; } public string? ApiDescriptionVersion { get; set; } private string? _apiDeploymentBaseUrl; - public string? ApiDeploymentBaseUrl { + public string? ApiDeploymentBaseUrl + { get { return _apiDeploymentBaseUrl; } set { - ValidateApiDeploymentBaseUrl(value); + ValidationHelpers.ValidateBaseUrl(nameof(ApiDeploymentBaseUrl), value); _apiDeploymentBaseUrl = value; } } @@ -69,15 +71,6 @@ internal static ApiDependency Load(JsonElement value) return apiDependency; } - private static void ValidateApiDeploymentBaseUrl(string? apiDeploymentBaseUrl) - { - // Check if the apiDeploymentBaseUrl is a valid URL and ends in a slash. - if (apiDeploymentBaseUrl == null || !apiDeploymentBaseUrl.EndsWith("/", StringComparison.Ordinal) || !Uri.TryCreate(apiDeploymentBaseUrl, UriKind.Absolute, out _)) - { - throw new ArgumentException($"The {nameof(apiDeploymentBaseUrl)} must be a valid URL and end in a slash."); - } - } - // Fixed fieldmap for ApiDependency private static readonly FixedFieldMap handlers = new() { diff --git a/src/lib/ApiManifestDocument.cs b/src/lib/ApiManifestDocument.cs index 0e6d59f..7f2fd5e 100644 --- a/src/lib/ApiManifestDocument.cs +++ b/src/lib/ApiManifestDocument.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using Microsoft.OpenApi.ApiManifest.Helpers; +using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest; @@ -72,8 +73,7 @@ public static ApiManifestDocument Load(JsonElement value) private static void Validate(string? applicationName) { - if (string.IsNullOrWhiteSpace(applicationName)) - throw new ArgumentNullException(applicationName, String.Format(ErrorMessage.FieldIsRequired, "applicationName", "ApiManifest")); + ValidationHelpers.ValidateNullOrWhitespace(nameof(applicationName), applicationName, nameof(ApiManifestDocument)); } // Create fixed field map for ApiManifest diff --git a/src/lib/Constants.cs b/src/lib/Constants.cs index 4cdd4dc..2d4fe73 100644 --- a/src/lib/Constants.cs +++ b/src/lib/Constants.cs @@ -1,4 +1,7 @@ -namespace Microsoft.OpenApi.ApiManifest +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.OpenApi.ApiManifest { internal static class Constants { @@ -9,5 +12,7 @@ internal static class ErrorMessage { public static string FieldIsRequired = "'{0}' is a required property of '{1}'."; public static string FieldIsNotValid = "'{0}' is not valid."; + public static string FieldLengthExceeded = "'{0}' length exceeded. Maximum length allowed is '{1}'."; + public static string BaseUrlIsNotValid = "The {0} must be a valid URL and end in a slash."; } } \ No newline at end of file diff --git a/src/lib/Helpers/ParsingHelpers.cs b/src/lib/Helpers/ParsingHelpers.cs new file mode 100644 index 0000000..492d6f2 --- /dev/null +++ b/src/lib/Helpers/ParsingHelpers.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Diagnostics; +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.Helpers; + +internal static class ParsingHelpers +{ + internal static void ParseMap(JsonElement node, T permissionsDocument, FixedFieldMap handlers) + { + foreach (var element in node.EnumerateObject()) + { + if (handlers.TryGetValue(element.Name, out var handler)) + { + handler(permissionsDocument, element.Value); + } + else + { + // Logs the unknown property. We can switch to additional properties model in the future if need be. + Debug.WriteLine($"Skipped {element.Name}. The property is unknown."); + } + } + } + + internal static List GetList(JsonElement v, Func load) + { + var list = new List(); + foreach (var item in v.EnumerateArray()) + { + list.Add(load(item)); + } + return list; + } + + internal static Dictionary GetMap(JsonElement v, Func load) + { + var map = new Dictionary(); + foreach (var item in v.EnumerateObject()) + { + map.Add(item.Name, load(item.Value)); + } + return map; + } + + internal static Dictionary GetMapOfString(JsonElement v) + { + var map = new Dictionary(); + foreach (var item in v.EnumerateObject()) + { + var value = item.Value.GetString(); + map.Add(item.Name, string.IsNullOrWhiteSpace(value) ? string.Empty : value); + } + return map; + } + + internal static SortedDictionary GetOrderedMap(JsonElement v, Func load) + { + var map = new SortedDictionary(); + foreach (var item in v.EnumerateObject()) + { + map.Add(item.Name, load(item.Value)); + } + return map; + } + + internal static List GetListOfString(JsonElement v) + { + var list = new List(); + foreach (var item in v.EnumerateArray()) + { + var value = item.GetString(); + if (value != null) + list.Add(value); + } + return list; + } + + internal static HashSet GetHashSetOfString(JsonElement v) + { + var hashSet = new HashSet(); + foreach (var item in v.EnumerateArray()) + { + var value = item.GetString(); + if (value != null) + _ = hashSet.Add(value); + } + return hashSet; + } + + internal static SortedSet GetOrderedHashSetOfString(JsonElement v) + { + var sortedSet = new SortedSet(); + foreach (var item in v.EnumerateArray()) + { + var value = item.GetString(); + if (value != null) + _ = sortedSet.Add(value); + } + return sortedSet; + } + + /// + /// Parse properties. + /// + /// Name-value pair separated by ';'. + internal static Dictionary ParseProperties(string context) + { + var properties = new Dictionary(); + foreach (var pair in ParseKey(context)) + { + properties.Add(pair.Key, pair.Value); + } + + return properties; + } + + /// + /// Enumerate the key value pairs for the configuration key. + /// + /// Configuration key supplied in the setting. + /// *Key value pairs. + internal static IEnumerable> ParseKey(string key) + { + foreach (var pair in key.Split(';')) + { + if (string.IsNullOrEmpty(pair)) + continue; + + var index = pair.IndexOf('='); + if (index == -1) + throw new InvalidOperationException($"Unable to parse: {key}. Format is name1=value1;name2=value2;..."); + + var keyValue = new KeyValuePair(pair[..index], pair[(index + 1)..]); + yield return keyValue; + } + } +} + +internal class FixedFieldMap : Dictionary> +{ + public FixedFieldMap() : base(StringComparer.OrdinalIgnoreCase) + { + + } +} diff --git a/src/lib/Helpers/ValidationHelpers.cs b/src/lib/Helpers/ValidationHelpers.cs new file mode 100644 index 0000000..3f7ff17 --- /dev/null +++ b/src/lib/Helpers/ValidationHelpers.cs @@ -0,0 +1,47 @@ +using System.Text.RegularExpressions; + +namespace Microsoft.OpenApi.ApiManifest.Helpers +{ + internal static class ValidationHelpers + { + internal static void ValidateNullOrWhitespace(string parameterName, string? value, string parentName) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(parameterName, string.Format(ErrorMessage.FieldIsRequired, parameterName, parentName)); + } + + internal static void ValidateNullObject(string parameterName, object? value, string parentName) + { + if (value == null) + throw new ArgumentNullException(parameterName, string.Format(ErrorMessage.FieldIsRequired, parameterName, parentName)); + } + + internal static void ValidateLength(string parameterName, string? value, int maxLength) + { + if (value?.Length > maxLength) + throw new ArgumentOutOfRangeException(parameterName, string.Format(ErrorMessage.FieldLengthExceeded, parameterName, maxLength)); + } + + internal static void ValidateEmail(string parameterName, string? value, string parentName) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(parameterName, string.Format(ErrorMessage.FieldIsRequired, parameterName, parentName)); + else + ValidateEmail(parameterName, value); + } + + internal static void ValidateBaseUrl(string parameterName, string? baseUrl) + { + // Check if the baseUrl is a valid URL and ends in a slash. + if (string.IsNullOrWhiteSpace(baseUrl) || !baseUrl.EndsWith("/", StringComparison.Ordinal) || !Uri.TryCreate(baseUrl, UriKind.Absolute, out _)) + throw new ArgumentException(string.Format(ErrorMessage.BaseUrlIsNotValid, nameof(baseUrl)), parameterName); + } + + private static readonly Regex s_emailRegex = new(@"^[^@\s]+@[^@\s]+$", RegexOptions.Compiled, Constants.DefaultRegexTimeout); + private static void ValidateEmail(string parameterName, string value) + { + if (!s_emailRegex.IsMatch(value)) + throw new ArgumentException(string.Format(ErrorMessage.FieldIsNotValid, parameterName), parameterName); + } + } +} diff --git a/src/lib/OpenAI/OpenAIPluginManifest.cs b/src/lib/OpenAI/OpenAIPluginManifest.cs index 19239f1..d3c37d2 100644 --- a/src/lib/OpenAI/OpenAIPluginManifest.cs +++ b/src/lib/OpenAI/OpenAIPluginManifest.cs @@ -2,6 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.OpenApi.ApiManifest.Helpers; using Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; using System.Text.Json; @@ -20,27 +21,92 @@ public class OpenAIPluginManifest private const string ContactEmailProperty = "contact_email"; private const string LegalInfoUrlProperty = "legal_info_url"; + /// + /// REQUIRED. The version of the manifest schema. + /// public string? SchemaVersion { get; set; } + /// + /// REQUIRED. The name of the plugin that will be shown to users. + /// public string? NameForHuman { get; set; } + /// + /// REQUIRED. The name the model will use to target the plugin. + /// public string? NameForModel { get; set; } + /// + /// REQUIRED. A description of the plugin that will be shown to users. + /// public string? DescriptionForHuman { get; set; } + /// + /// REQUIRED. Description better tailored to the model, such as token context length considerations or keyword usage for improved plugin prompting. + /// public string? DescriptionForModel { get; set; } + /// + /// REQUIRED. The authentication schema type for the plugin. This can be one of the following types: , , , and . + /// public BaseManifestAuth? Auth { get; set; } + /// + /// REQUIRED. The API specification for the plugin. + /// public Api? Api { get; set; } + /// + /// REQUIRED. A URL to a logo for the plugin. This logo will be shown to users. Suggested size: 512 x 512. Transparent backgrounds are supported. Must be an image, no GIFs are allowed. + /// public string? LogoUrl { get; set; } + /// + /// REQUIRED. An email address for safety/moderation, support, and deactivation. + /// public string? ContactEmail { get; set; } + /// + /// REQUIRED. A URL to a page with legal information about the plugin. + /// public string? LegalInfoUrl { get; set; } - public OpenAIPluginManifest() + public OpenAIPluginManifest(string nameForModel, string nameForHuman, string descriptionForHuman, string descriptionForModel, BaseManifestAuth auth, Api api, string logoUrl, string contactEmail, string legalInfoUrl, string schemaVersion = "v1") { - SchemaVersion = "v1"; + SchemaVersion = schemaVersion; + NameForHuman = nameForHuman; + NameForModel = nameForModel; + DescriptionForHuman = descriptionForHuman; + DescriptionForModel = descriptionForModel; + Auth = auth; + Api = api; + LogoUrl = logoUrl; + ContactEmail = contactEmail; + LegalInfoUrl = legalInfoUrl; + + Validate(this); + } + + internal OpenAIPluginManifest(JsonElement value) + { + ParsingHelpers.ParseMap(value, this, handlers); + Validate(this); } public static OpenAIPluginManifest Load(JsonElement value) { - var manifest = new OpenAIPluginManifest(); - ParsingHelpers.ParseMap(value, manifest, handlers); - return manifest; + return new OpenAIPluginManifest(value); + } + + //Write method + public void Write(Utf8JsonWriter writer) + { + Validate(this); + writer.WriteStartObject(); + writer.WriteString(SchemaVersionProperty, SchemaVersion); + writer.WriteString(NameForHumanProperty, NameForHuman); + writer.WriteString(NameForModelProperty, NameForModel); + writer.WriteString(DescriptionForHumanProperty, DescriptionForHuman); + writer.WriteString(DescriptionForModelProperty, DescriptionForModel); + writer.WritePropertyName(AuthProperty); + Auth?.Write(writer); + writer.WritePropertyName(ApiProperty); + Api?.Write(writer); + writer.WriteString(LogoUrlProperty, LogoUrl); + writer.WriteString(ContactEmailProperty, ContactEmail); + writer.WriteString(LegalInfoUrlProperty, LegalInfoUrl); + writer.WriteEndObject(); } // Create handlers FixedFieldMap for OpenAIPluginManifest @@ -58,29 +124,30 @@ public static OpenAIPluginManifest Load(JsonElement value) { LegalInfoUrlProperty, (o,v) => {o.LegalInfoUrl = v.GetString(); } }, }; - //Write method - public void Write(Utf8JsonWriter writer) + /// + /// Validate the provided based on the Open AI Plugin manifest schema at https://platform.openai.com/docs/plugins/getting-started/plugin-manifest. + /// + /// The to validate. + private void Validate(OpenAIPluginManifest openAIPluginManifest) { - writer.WriteStartObject(); - writer.WriteString(SchemaVersionProperty, SchemaVersion); - writer.WriteString(NameForHumanProperty, NameForHuman); - writer.WriteString(NameForModelProperty, NameForModel); - writer.WriteString(DescriptionForHumanProperty, DescriptionForHuman); - writer.WriteString(DescriptionForModelProperty, DescriptionForModel); - if (Auth != null) - { - writer.WritePropertyName(AuthProperty); - Auth.Write(writer); - } - if (Api != null) - { - writer.WritePropertyName(ApiProperty); - Api?.Write(writer); - } - if (LogoUrl != null) writer.WriteString(LogoUrlProperty, LogoUrl); - if (ContactEmail != null) writer.WriteString(ContactEmailProperty, ContactEmail); - if (LegalInfoUrl != null) writer.WriteString(LegalInfoUrlProperty, LegalInfoUrl); - writer.WriteEndObject(); + ValidationHelpers.ValidateNullOrWhitespace(nameof(NameForHuman), openAIPluginManifest.NameForHuman, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateLength(nameof(NameForHuman), openAIPluginManifest.NameForHuman, 20); + + ValidationHelpers.ValidateNullOrWhitespace(nameof(NameForModel), openAIPluginManifest.NameForModel, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateLength(nameof(NameForModel), openAIPluginManifest.NameForModel, 50); + + ValidationHelpers.ValidateNullOrWhitespace(nameof(DescriptionForHuman), openAIPluginManifest.DescriptionForHuman, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateLength(nameof(DescriptionForHuman), openAIPluginManifest.DescriptionForHuman, 100); + + ValidationHelpers.ValidateNullOrWhitespace(nameof(DescriptionForModel), openAIPluginManifest.DescriptionForModel, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateLength(nameof(DescriptionForModel), openAIPluginManifest.DescriptionForModel, 8000); + + ValidationHelpers.ValidateNullOrWhitespace(nameof(SchemaVersion), openAIPluginManifest.SchemaVersion, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateNullObject(nameof(Auth), openAIPluginManifest.Auth, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateNullObject(nameof(Api), openAIPluginManifest.Api, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateNullOrWhitespace(nameof(LogoUrl), openAIPluginManifest.LogoUrl, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateEmail(nameof(ContactEmail), openAIPluginManifest.ContactEmail, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateNullOrWhitespace(nameof(LegalInfoUrl), openAIPluginManifest.LegalInfoUrl, nameof(OpenAIPluginManifest)); } } diff --git a/src/lib/OpenAI/OpenApiPluginFactory.cs b/src/lib/OpenAI/OpenApiPluginFactory.cs index 8b1f8f9..d0cc3ba 100644 --- a/src/lib/OpenAI/OpenApiPluginFactory.cs +++ b/src/lib/OpenAI/OpenApiPluginFactory.cs @@ -2,17 +2,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; + namespace Microsoft.OpenApi.ApiManifest.OpenAI; public static class OpenApiPluginFactory { - public static OpenAIPluginManifest CreateOpenAIPluginManifest() + public static OpenAIPluginManifest CreateOpenAIPluginManifest(string nameForModel, string nameForHuman, string descriptionForHuman, string descriptionForModel, BaseManifestAuth auth, Api api, string logoUrl, string contactEmail, string legalInfoUrl, string schemaVersion = "v1") { - var manifest = new OpenAIPluginManifest - { - SchemaVersion = "v1" - }; - return manifest; + return new OpenAIPluginManifest(nameForModel, nameForHuman, descriptionForHuman, descriptionForModel, auth, api, logoUrl, contactEmail, legalInfoUrl, schemaVersion); } } \ No newline at end of file diff --git a/src/lib/Publisher.cs b/src/lib/Publisher.cs index 8c192c1..5fa090f 100644 --- a/src/lib/Publisher.cs +++ b/src/lib/Publisher.cs @@ -1,5 +1,5 @@ +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; -using System.Text.RegularExpressions; namespace Microsoft.OpenApi.ApiManifest; @@ -11,8 +11,6 @@ public class Publisher private const string NameProperty = "name"; private const string ContactEmailProperty = "contactEmail"; - private static readonly Regex s_emailRegex = new(@"^[^@\s]+@[^@\s]+$", RegexOptions.Compiled, Constants.DefaultRegexTimeout); - public Publisher(string name, string contactEmail) { Validate(name, contactEmail); @@ -32,10 +30,8 @@ public void Write(Utf8JsonWriter writer) Validate(Name, ContactEmail); writer.WriteStartObject(); - writer.WriteString(NameProperty, Name); writer.WriteString(ContactEmailProperty, ContactEmail); - writer.WriteEndObject(); } @@ -47,14 +43,8 @@ internal static Publisher Load(JsonElement value) private static void Validate(string? name, string? contactEmail) { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullException(name, String.Format(ErrorMessage.FieldIsRequired, "name", "publisher")); - - if (string.IsNullOrWhiteSpace(contactEmail)) - throw new ArgumentNullException(contactEmail, String.Format(ErrorMessage.FieldIsRequired, "contactEmail", "publisher")); - - if (!s_emailRegex.IsMatch(contactEmail)) - throw new ArgumentException(string.Format(ErrorMessage.FieldIsNotValid, "contactEmail"), contactEmail); + ValidationHelpers.ValidateNullOrWhitespace(nameof(name), name, nameof(Publisher)); + ValidationHelpers.ValidateEmail(nameof(contactEmail), contactEmail, nameof(Publisher)); } private static readonly FixedFieldMap handlers = new() diff --git a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs index ce54a55..cada8ab 100644 --- a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs +++ b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs @@ -9,8 +9,9 @@ namespace Microsoft.OpenApi.ApiManifest.Tests; public class OpenAIPluginManifestTests { + // With no auth. [Fact] - public void LoadOpenAIPluginManifest() + public void LoadOpenAIPluginManifestWithNoAuth() { var json = """ { @@ -27,6 +28,7 @@ public void LoadOpenAIPluginManifest() "url": "https://api.openai.com/v1" }, "logo_url": "https://avatars.githubusercontent.com/foo", + "legal_info_url": "https://legalinfo.foobar.com", "contact_email": "joe@demo.com" } """; @@ -44,29 +46,13 @@ public void LoadOpenAIPluginManifest() Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); Assert.Equal("https://avatars.githubusercontent.com/foo", manifest.LogoUrl); Assert.Equal("joe@demo.com", manifest.ContactEmail); + Assert.Equal("https://legalinfo.foobar.com", manifest.LegalInfoUrl); } - // Create minimal OpenAIPluginManifest [Fact] - public void WriteOpenAIPluginManifest() + public void WriteOpenAIPluginManifestWithNoAuth() { - var manifest = new OpenAIPluginManifest - { - SchemaVersion = "1.0.0", - NameForHuman = "OpenAI GPT-3", - NameForModel = "openai-gpt3", - DescriptionForHuman = "OpenAI GPT-3 is a language model that generates text based on prompts.", - DescriptionForModel = "OpenAI GPT-3 is a language model that generates text based on prompts.", - Auth = new ManifestNoAuth(), - Api = new Api - { - Type = "openapi", - Url = "https://api.openai.com/v1", - IsUserAuthenticated = false - }, - LogoUrl = "https://avatars.githubusercontent.com/bar", - ContactEmail = "joe@test.com" - }; + var manifest = CreateManifestPlugIn(); // serialize using the Write method var stream = new MemoryStream(); @@ -80,10 +66,10 @@ public void WriteOpenAIPluginManifest() Assert.Equal(""" { "schema_version": "1.0.0", - "name_for_human": "OpenAI GPT-3", - "name_for_model": "openai-gpt3", - "description_for_human": "OpenAI GPT-3 is a language model that generates text based on prompts.", - "description_for_model": "OpenAI GPT-3 is a language model that generates text based on prompts.", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", "auth": { "type": "none" }, @@ -93,11 +79,13 @@ public void WriteOpenAIPluginManifest() "is_user_authenticated": false }, "logo_url": "https://avatars.githubusercontent.com/bar", - "contact_email": "joe@test.com" + "contact_email": "joe@test.com", + "legal_info_url": "https://legalinfo.foobar.com" } """, json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } + // With no OAuth. [Fact] public void LoadOpenAIPluginManifestWithOAuth() { @@ -124,6 +112,7 @@ public void LoadOpenAIPluginManifestWithOAuth() "is_user_authenticated": false }, "logo_url": "https://avatars.githubusercontent.com/bar", + "legal_info_url": "https://legalinfo.foobar.com", "contact_email": "joe@test.com" } """; @@ -145,38 +134,24 @@ public void LoadOpenAIPluginManifestWithOAuth() Assert.Equal("openapi", manifest.Api?.Type); Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); + Assert.Equal("https://legalinfo.foobar.com", manifest.LegalInfoUrl); Assert.Equal("joe@test.com", manifest.ContactEmail); } [Fact] public void WriteOpenAIPluginManifestWithOAuth() { - var manifest = new OpenAIPluginManifest + var manifest = CreateManifestPlugIn(); + manifest.Auth = new ManifestOAuthAuth { - SchemaVersion = "1.0.0", - NameForHuman = "TestOAuth", - NameForModel = "TestOAuthModel", - DescriptionForHuman = "SomeHumanDescription", - DescriptionForModel = "SomeModelDescription", - Auth = new ManifestOAuthAuth - { - AuthorizationUrl = "https://api.openai.com/oauth/authorize", - AuthorizationContentType = "application/json", - ClientUrl = "https://api.openai.com/oauth/token", - Scope = "all:all", - VerificationTokens = new VerificationTokens + AuthorizationUrl = "https://api.openai.com/oauth/authorize", + AuthorizationContentType = "application/json", + ClientUrl = "https://api.openai.com/oauth/token", + Scope = "all:all", + VerificationTokens = new VerificationTokens { { "openai", "dummy_verification_token" } } - }, - Api = new Api - { - Type = "openapi", - Url = "https://api.openai.com/v1", - IsUserAuthenticated = false - }, - LogoUrl = "https://avatars.githubusercontent.com/bar", - ContactEmail = "joe@test.com" }; // serialize using the Write method @@ -211,11 +186,13 @@ public void WriteOpenAIPluginManifestWithOAuth() "is_user_authenticated": false }, "logo_url": "https://avatars.githubusercontent.com/bar", - "contact_email": "joe@test.com" + "contact_email": "joe@test.com", + "legal_info_url": "https://legalinfo.foobar.com" } """, json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } + // With user HTTP. [Fact] public void LoadOpenAIPluginManifestWithUserHttp() { @@ -236,6 +213,7 @@ public void LoadOpenAIPluginManifestWithUserHttp() "is_user_authenticated": false }, "logo_url": "https://avatars.githubusercontent.com/bar", + "legal_info_url": "https://legalinfo.foobar.com", "contact_email": "joe@test.com" } """; @@ -254,28 +232,14 @@ public void LoadOpenAIPluginManifestWithUserHttp() Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); Assert.Equal("joe@test.com", manifest.ContactEmail); + Assert.Equal("https://legalinfo.foobar.com", manifest.LegalInfoUrl); } [Fact] public void WriteOpenAIPluginManifestWithUserHttp() { - var manifest = new OpenAIPluginManifest - { - SchemaVersion = "1.0.0", - NameForHuman = "TestOAuth", - NameForModel = "TestOAuthModel", - DescriptionForHuman = "SomeHumanDescription", - DescriptionForModel = "SomeModelDescription", - Auth = new ManifestUserHttpAuth("bearer"), - Api = new Api - { - Type = "openapi", - Url = "https://api.openai.com/v1", - IsUserAuthenticated = false - }, - LogoUrl = "https://avatars.githubusercontent.com/bar", - ContactEmail = "joe@test.com" - }; + var manifest = CreateManifestPlugIn(); + manifest.Auth = new ManifestUserHttpAuth("bearer"); // serialize using the Write method var stream = new MemoryStream(); @@ -303,11 +267,13 @@ public void WriteOpenAIPluginManifestWithUserHttp() "is_user_authenticated": false }, "logo_url": "https://avatars.githubusercontent.com/bar", - "contact_email": "joe@test.com" + "contact_email": "joe@test.com", + "legal_info_url": "https://legalinfo.foobar.com" } """, json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } + // With service HTTP. [Fact] public void LoadOpenAIPluginManifestWithServiceHttp() { @@ -331,7 +297,8 @@ public void LoadOpenAIPluginManifestWithServiceHttp() "is_user_authenticated": false }, "logo_url": "https://avatars.githubusercontent.com/bar", - "contact_email": "joe@test.com" + "contact_email": "joe@test.com", + "legal_info_url": "https://legalinfo.foobar.com" } """; @@ -350,31 +317,17 @@ public void LoadOpenAIPluginManifestWithServiceHttp() Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); Assert.Equal("https://avatars.githubusercontent.com/bar", manifest.LogoUrl); Assert.Equal("joe@test.com", manifest.ContactEmail); + Assert.Equal("https://legalinfo.foobar.com", manifest.LegalInfoUrl); } [Fact] public void WriteOpenAIPluginManifestWithServiceHttp() { - var manifest = new OpenAIPluginManifest + var manifest = CreateManifestPlugIn(); + manifest.Auth = new ManifestServiceHttpAuth(new VerificationTokens { - SchemaVersion = "1.0.0", - NameForHuman = "TestOAuth", - NameForModel = "TestOAuthModel", - DescriptionForHuman = "SomeHumanDescription", - DescriptionForModel = "SomeModelDescription", - Auth = new ManifestServiceHttpAuth(new VerificationTokens - { - { "openai", "dummy_verification_token" } - }), - Api = new Api - { - Type = "openapi", - Url = "https://api.openai.com/v1", - IsUserAuthenticated = false - }, - LogoUrl = "https://avatars.githubusercontent.com/bar", - ContactEmail = "joe@test.com" - }; + { "openai", "dummy_verification_token" } + }); // serialize using the Write method var stream = new MemoryStream(); @@ -405,8 +358,119 @@ public void WriteOpenAIPluginManifestWithServiceHttp() "is_user_authenticated": false }, "logo_url": "https://avatars.githubusercontent.com/bar", - "contact_email": "joe@test.com" + "contact_email": "joe@test.com", + "legal_info_url": "https://legalinfo.foobar.com" } """, json, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); } + + [Theory] + [InlineData("foo")] + [InlineData("foo@")] + [InlineData("foo@@bar.com")] + [InlineData("foo @bar.com")] + public void WriteOpenAIPluginManifestWithInvalidContactEmail(string email) + { + var manifest = CreateManifestPlugIn(); + manifest.ContactEmail = email; + + // serialize using the Write method + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + _ = Assert.Throws(() => + { + manifest.Write(writer); + }); + } + + [Fact] + public void LoadOpenAIPluginManifestWithInvalidAuthType() + { + var json = """ + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "NOT_VALID_service_http", + "authorization_type": "bearer", + "verification_tokens": { + "openai": "dummy_verification_token" + } + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com", + "legal_info_url": "https://legalinfo.foobar.com" + } + """; + + var doc = JsonDocument.Parse(json); + var exception = Assert.Throws(() => + { + _ = OpenAIPluginManifest.Load(doc.RootElement); + }); + Assert.Equal("Unknown auth type: not_valid_service_http (Parameter 'value')", exception.Message); + } + + [Fact] + public void LoadOpenAIPluginManifestWithIncompleteManifest() + { + var json = """ + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "service_http", + "authorization_type": "bearer", + "verification_tokens": { + "openai": "dummy_verification_token" + } + }, + "api": { + "type": "openapi", + "url": "https://api.openai.com/v1", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com", + "legal_info_url": "https://legalinfo.foobar.com" + } + """; + + var doc = JsonDocument.Parse(json); + var exception = Assert.Throws(() => + { + _ = OpenAIPluginManifest.Load(doc.RootElement); + }); + Assert.Equal("'NameForModel' is a required property of 'OpenAIPluginManifest'. (Parameter 'NameForModel')", exception.Message); + } + + private static OpenAIPluginManifest CreateManifestPlugIn() + { + return OpenApiPluginFactory.CreateOpenAIPluginManifest( + schemaVersion: "1.0.0", + nameForHuman: "TestOAuth", + nameForModel: "TestOAuthModel", + descriptionForHuman: "SomeHumanDescription", + descriptionForModel: "SomeModelDescription", + auth: new ManifestNoAuth(), + api: new Api + { + Type = "openapi", + Url = "https://api.openai.com/v1", + IsUserAuthenticated = false + }, + logoUrl: "https://avatars.githubusercontent.com/bar", + legalInfoUrl: "https://legalinfo.foobar.com", + contactEmail: "joe@test.com"); + } } From 05a99499110de3b4230b64441d8762ed71c728e6 Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 9 Oct 2023 16:36:16 -0700 Subject: [PATCH 09/12] chore: Simplify OpenAIPluginManifest constructor. --- src/lib/AccessRequest.cs | 1 + src/lib/AuthorizationRequirements.cs | 1 + src/lib/Constants.cs | 8 +- src/lib/OpenAI/Api.cs | 23 ++- .../OpenAI/Authentication/BaseManifestAuth.cs | 1 + .../Authentication/ManifestOAuthAuth.cs | 1 + .../Authentication/ManifestServiceHttpAuth.cs | 1 + .../Authentication/VerificationTokens.cs | 1 + src/lib/OpenAI/OpenAIPluginManifest.cs | 8 +- src/lib/OpenAI/OpenApiPluginFactory.cs | 7 +- src/lib/ParsingHelpers.cs | 151 ------------------ src/lib/RequestInfo.cs | 1 + .../OpenAIPluginManifestTests.cs | 54 +++++-- 13 files changed, 80 insertions(+), 178 deletions(-) delete mode 100644 src/lib/ParsingHelpers.cs diff --git a/src/lib/AccessRequest.cs b/src/lib/AccessRequest.cs index edb3796..08da308 100644 --- a/src/lib/AccessRequest.cs +++ b/src/lib/AccessRequest.cs @@ -1,3 +1,4 @@ +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; using System.Text.Json.Nodes; diff --git a/src/lib/AuthorizationRequirements.cs b/src/lib/AuthorizationRequirements.cs index 1acfff6..1aed1a3 100644 --- a/src/lib/AuthorizationRequirements.cs +++ b/src/lib/AuthorizationRequirements.cs @@ -1,3 +1,4 @@ +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest; diff --git a/src/lib/Constants.cs b/src/lib/Constants.cs index 2d4fe73..b46de2a 100644 --- a/src/lib/Constants.cs +++ b/src/lib/Constants.cs @@ -10,9 +10,9 @@ internal static class Constants internal static class ErrorMessage { - public static string FieldIsRequired = "'{0}' is a required property of '{1}'."; - public static string FieldIsNotValid = "'{0}' is not valid."; - public static string FieldLengthExceeded = "'{0}' length exceeded. Maximum length allowed is '{1}'."; - public static string BaseUrlIsNotValid = "The {0} must be a valid URL and end in a slash."; + public static readonly string FieldIsRequired = "'{0}' is a required property of '{1}'."; + public static readonly string FieldIsNotValid = "'{0}' is not valid."; + public static readonly string FieldLengthExceeded = "'{0}' length exceeded. Maximum length allowed is '{1}'."; + public static readonly string BaseUrlIsNotValid = "The {0} must be a valid URL and end in a slash."; } } \ No newline at end of file diff --git a/src/lib/OpenAI/Api.cs b/src/lib/OpenAI/Api.cs index 15010c1..3f48184 100644 --- a/src/lib/OpenAI/Api.cs +++ b/src/lib/OpenAI/Api.cs @@ -2,6 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.OpenAI; @@ -15,9 +16,22 @@ public class Api public string? Url { get; set; } public bool? IsUserAuthenticated { get; set; } + public Api(string type, string url) + { + Type = type; + Url = url; + Validate(this); + } + + internal Api(JsonElement value) + { + ParsingHelpers.ParseMap(value, this, handlers); + Validate(this); + } + public static Api Load(JsonElement value) { - var api = new Api(); + var api = new Api(value); ParsingHelpers.ParseMap(value, api, handlers); return api; } @@ -32,12 +46,19 @@ public static Api Load(JsonElement value) public void Write(Utf8JsonWriter writer) { + Validate(this); writer.WriteStartObject(); writer.WriteString(TypeProperty, Type); writer.WriteString(UrlProperty, Url); writer.WriteBoolean(IsUserAuthenticatedProperty, IsUserAuthenticated ?? false); writer.WriteEndObject(); } + + private void Validate(Api api) + { + ValidationHelpers.ValidateNullOrWhitespace(nameof(Type), api.Type, nameof(Api)); + ValidationHelpers.ValidateNullOrWhitespace(nameof(Url), api.Url, nameof(Api)); + } } diff --git a/src/lib/OpenAI/Authentication/BaseManifestAuth.cs b/src/lib/OpenAI/Authentication/BaseManifestAuth.cs index 261b2f3..d8dd841 100644 --- a/src/lib/OpenAI/Authentication/BaseManifestAuth.cs +++ b/src/lib/OpenAI/Authentication/BaseManifestAuth.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; diff --git a/src/lib/OpenAI/Authentication/ManifestOAuthAuth.cs b/src/lib/OpenAI/Authentication/ManifestOAuthAuth.cs index fc2cca5..b7bc61f 100644 --- a/src/lib/OpenAI/Authentication/ManifestOAuthAuth.cs +++ b/src/lib/OpenAI/Authentication/ManifestOAuthAuth.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; diff --git a/src/lib/OpenAI/Authentication/ManifestServiceHttpAuth.cs b/src/lib/OpenAI/Authentication/ManifestServiceHttpAuth.cs index 19d94f9..c5e5652 100644 --- a/src/lib/OpenAI/Authentication/ManifestServiceHttpAuth.cs +++ b/src/lib/OpenAI/Authentication/ManifestServiceHttpAuth.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; diff --git a/src/lib/OpenAI/Authentication/VerificationTokens.cs b/src/lib/OpenAI/Authentication/VerificationTokens.cs index 8cee586..4e74de7 100644 --- a/src/lib/OpenAI/Authentication/VerificationTokens.cs +++ b/src/lib/OpenAI/Authentication/VerificationTokens.cs @@ -2,6 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; diff --git a/src/lib/OpenAI/OpenAIPluginManifest.cs b/src/lib/OpenAI/OpenAIPluginManifest.cs index d3c37d2..ed38c77 100644 --- a/src/lib/OpenAI/OpenAIPluginManifest.cs +++ b/src/lib/OpenAI/OpenAIPluginManifest.cs @@ -62,20 +62,14 @@ public class OpenAIPluginManifest /// public string? LegalInfoUrl { get; set; } - public OpenAIPluginManifest(string nameForModel, string nameForHuman, string descriptionForHuman, string descriptionForModel, BaseManifestAuth auth, Api api, string logoUrl, string contactEmail, string legalInfoUrl, string schemaVersion = "v1") + public OpenAIPluginManifest(string nameForModel, string nameForHuman, string logoUrl, string contactEmail, string legalInfoUrl, string schemaVersion = "v1") { SchemaVersion = schemaVersion; NameForHuman = nameForHuman; NameForModel = nameForModel; - DescriptionForHuman = descriptionForHuman; - DescriptionForModel = descriptionForModel; - Auth = auth; - Api = api; LogoUrl = logoUrl; ContactEmail = contactEmail; LegalInfoUrl = legalInfoUrl; - - Validate(this); } internal OpenAIPluginManifest(JsonElement value) diff --git a/src/lib/OpenAI/OpenApiPluginFactory.cs b/src/lib/OpenAI/OpenApiPluginFactory.cs index d0cc3ba..1a06a52 100644 --- a/src/lib/OpenAI/OpenApiPluginFactory.cs +++ b/src/lib/OpenAI/OpenApiPluginFactory.cs @@ -1,16 +1,13 @@ - // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using Microsoft.OpenApi.ApiManifest.OpenAI.Authentication; - namespace Microsoft.OpenApi.ApiManifest.OpenAI; public static class OpenApiPluginFactory { - public static OpenAIPluginManifest CreateOpenAIPluginManifest(string nameForModel, string nameForHuman, string descriptionForHuman, string descriptionForModel, BaseManifestAuth auth, Api api, string logoUrl, string contactEmail, string legalInfoUrl, string schemaVersion = "v1") + public static OpenAIPluginManifest CreateOpenAIPluginManifest(string nameForModel, string nameForHuman, string logoUrl, string contactEmail, string legalInfoUrl, string schemaVersion = "v1") { - return new OpenAIPluginManifest(nameForModel, nameForHuman, descriptionForHuman, descriptionForModel, auth, api, logoUrl, contactEmail, legalInfoUrl, schemaVersion); + return new OpenAIPluginManifest(nameForModel: nameForModel, nameForHuman: nameForHuman, logoUrl: logoUrl, contactEmail: contactEmail, legalInfoUrl: legalInfoUrl, schemaVersion: schemaVersion); } } \ No newline at end of file diff --git a/src/lib/ParsingHelpers.cs b/src/lib/ParsingHelpers.cs deleted file mode 100644 index d5abbaa..0000000 --- a/src/lib/ParsingHelpers.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -using System.Diagnostics; -using System.Text.Json; - -namespace Microsoft.OpenApi.ApiManifest; - -internal static class ParsingHelpers -{ - public static void ParseMap(JsonElement node, T permissionsDocument, FixedFieldMap handlers) - { - foreach (var element in node.EnumerateObject()) - { - if (handlers.TryGetValue(element.Name, out var handler)) - { - handler(permissionsDocument, element.Value); - } - else - { - // Logs the unknown property. We can switch to additional properties model in the future if need be. - Debug.WriteLine($"Skipped {element.Name}. The property is unknown."); - } - } - } - - internal static List GetList(JsonElement v, Func load) - { - var list = new List(); - foreach (var item in v.EnumerateArray()) - { - list.Add(load(item)); - } - return list; - } - - internal static Dictionary GetMap(JsonElement v, Func load) - { - var map = new Dictionary(); - foreach (var item in v.EnumerateObject()) - { - map.Add(item.Name, load(item.Value)); - } - return map; - } - - internal static Dictionary GetMapOfString(JsonElement v) - { - var map = new Dictionary(); - foreach (var item in v.EnumerateObject()) - { - var value = item.Value.GetString(); - map.Add(item.Name, !string.IsNullOrWhiteSpace(value) ? value : string.Empty); - } - return map; - } - - internal static SortedDictionary GetOrderedMap(JsonElement v, Func load) - { - var map = new SortedDictionary(); - foreach (var item in v.EnumerateObject()) - { - map.Add(item.Name, load(item.Value)); - } - return map; - } - - internal static List GetListOfString(JsonElement v) - { - var list = new List(); - foreach (var item in v.EnumerateArray()) - { - var value = item.GetString(); - if (value != null) - list.Add(value); - } - return list; - } - - internal static HashSet GetHashSetOfString(JsonElement v) - { - var hashSet = new HashSet(); - foreach (var item in v.EnumerateArray()) - { - var value = item.GetString(); - if (value != null) - _ = hashSet.Add(value); - } - return hashSet; - } - - internal static SortedSet GetOrderedHashSetOfString(JsonElement v) - { - var sortedSet = new SortedSet(); - foreach (var item in v.EnumerateArray()) - { - var value = item.GetString(); - if (value != null) - _ = sortedSet.Add(value); - } - return sortedSet; - } - - /// - /// Parse properties. - /// - /// Name-value pair separated by ';'. - internal static Dictionary ParseProperties(string context) - { - var properties = new Dictionary(); - foreach (var pair in ParseKey(context)) - { - properties.Add(pair.Key, pair.Value); - } - - return properties; - } - - /// - /// Enumerate the key value pairs for the configuration key. - /// - /// Configuration key supplied in the setting. - /// *Key value pairs. - internal static IEnumerable> ParseKey(string key) - { - foreach (var pair in key.Split(';')) - { - if (string.IsNullOrEmpty(pair)) - { - continue; - } - - var index = pair.IndexOf('='); - if (index == -1) - { - throw new InvalidOperationException($"Unable to parse: {key}. Format is name1=value1;name2=value2;..."); - } - - var keyValue = new KeyValuePair(pair.Substring(0, index), pair.Substring(index + 1)); - yield return keyValue; - } - } -} - -public class FixedFieldMap : Dictionary> -{ - public FixedFieldMap() : base(StringComparer.OrdinalIgnoreCase) - { - - } -} diff --git a/src/lib/RequestInfo.cs b/src/lib/RequestInfo.cs index 4274222..c06d106 100644 --- a/src/lib/RequestInfo.cs +++ b/src/lib/RequestInfo.cs @@ -1,3 +1,4 @@ +using Microsoft.OpenApi.ApiManifest.Helpers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest; diff --git a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs index cada8ab..e446fd1 100644 --- a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs +++ b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs @@ -454,23 +454,57 @@ public void LoadOpenAIPluginManifestWithIncompleteManifest() Assert.Equal("'NameForModel' is a required property of 'OpenAIPluginManifest'. (Parameter 'NameForModel')", exception.Message); } + [Fact] + public void LoadOpenAIPluginManifestWithNoApiUrl() + { + var json = """ + { + "schema_version": "1.0.0", + "name_for_human": "TestOAuth", + "name_for_model": "TestOAuthModel", + "description_for_human": "SomeHumanDescription", + "description_for_model": "SomeModelDescription", + "auth": { + "type": "service_http", + "authorization_type": "bearer", + "verification_tokens": { + "openai": "dummy_verification_token" + } + }, + "api": { + "type": "openapi", + "is_user_authenticated": false + }, + "logo_url": "https://avatars.githubusercontent.com/bar", + "contact_email": "joe@test.com", + "legal_info_url": "https://legalinfo.foobar.com" + } + """; + + var doc = JsonDocument.Parse(json); + var exception = Assert.Throws(() => + { + _ = OpenAIPluginManifest.Load(doc.RootElement); + }); + Assert.Equal("'Url' is a required property of 'Api'. (Parameter 'Url')", exception.Message); + } + private static OpenAIPluginManifest CreateManifestPlugIn() { - return OpenApiPluginFactory.CreateOpenAIPluginManifest( + var manifest = OpenApiPluginFactory.CreateOpenAIPluginManifest( schemaVersion: "1.0.0", nameForHuman: "TestOAuth", nameForModel: "TestOAuthModel", - descriptionForHuman: "SomeHumanDescription", - descriptionForModel: "SomeModelDescription", - auth: new ManifestNoAuth(), - api: new Api - { - Type = "openapi", - Url = "https://api.openai.com/v1", - IsUserAuthenticated = false - }, logoUrl: "https://avatars.githubusercontent.com/bar", legalInfoUrl: "https://legalinfo.foobar.com", contactEmail: "joe@test.com"); + manifest.DescriptionForHuman = "SomeHumanDescription"; + manifest.DescriptionForModel = "SomeModelDescription"; + manifest.Auth = new ManifestNoAuth(); + manifest.Api = new Api("openapi", "https://api.openai.com/v1") + { + IsUserAuthenticated = false + }; + return manifest; } } From 687f1d367c54154dfab23a1964e7b4e1ed5ce14a Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 9 Oct 2023 20:37:38 -0700 Subject: [PATCH 10/12] chore: Adds tests for ParseHelpers. --- src/lib/AccessRequest.cs | 2 - src/lib/AuthorizationRequirements.cs | 1 - src/lib/Helpers/ParsingHelpers.cs | 14 +-- .../Helpers/ParsingHelpersTests.cs | 116 ++++++++++++++++++ 4 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs diff --git a/src/lib/AccessRequest.cs b/src/lib/AccessRequest.cs index 08da308..90d0aec 100644 --- a/src/lib/AccessRequest.cs +++ b/src/lib/AccessRequest.cs @@ -6,9 +6,7 @@ namespace Microsoft.OpenApi.ApiManifest; public class AccessRequest { - // TODO: Add validation. Type is required and is unique for the described API according to RAR - https://www.rfc-editor.org/rfc/rfc9396. private const string TypeProperty = "type"; - // TODO: Rename to 'actions' to match RAR spec. private const string ContentProperty = "content"; public string? Type { get; set; } diff --git a/src/lib/AuthorizationRequirements.cs b/src/lib/AuthorizationRequirements.cs index 1aed1a3..d6c3281 100644 --- a/src/lib/AuthorizationRequirements.cs +++ b/src/lib/AuthorizationRequirements.cs @@ -6,7 +6,6 @@ namespace Microsoft.OpenApi.ApiManifest; public class AuthorizationRequirements { public string? ClientIdentifier { get; set; } - // TODO: Confirm the need for AccessReference property. It is not present in the spec. public List? AccessReference { get; set; } public List? Access { get; set; } diff --git a/src/lib/Helpers/ParsingHelpers.cs b/src/lib/Helpers/ParsingHelpers.cs index 492d6f2..4bddecd 100644 --- a/src/lib/Helpers/ParsingHelpers.cs +++ b/src/lib/Helpers/ParsingHelpers.cs @@ -44,23 +44,23 @@ internal static Dictionary GetMap(JsonElement v, Func GetMapOfString(JsonElement v) + internal static SortedDictionary GetOrderedMap(JsonElement v, Func load) { - var map = new Dictionary(); + var map = new SortedDictionary(); foreach (var item in v.EnumerateObject()) { - var value = item.Value.GetString(); - map.Add(item.Name, string.IsNullOrWhiteSpace(value) ? string.Empty : value); + map.Add(item.Name, load(item.Value)); } return map; } - internal static SortedDictionary GetOrderedMap(JsonElement v, Func load) + internal static Dictionary GetMapOfString(JsonElement v) { - var map = new SortedDictionary(); + var map = new Dictionary(); foreach (var item in v.EnumerateObject()) { - map.Add(item.Name, load(item.Value)); + var value = item.Value.GetString(); + map.Add(item.Name, string.IsNullOrWhiteSpace(value) ? string.Empty : value); } return map; } diff --git a/tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs b/tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs new file mode 100644 index 0000000..09e5e45 --- /dev/null +++ b/tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.ApiManifest.Helpers; +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.Tests.Helpers +{ + public class ParsingHelpersTests + { + private readonly string exampleKeyValuePair; + private readonly JsonDocument exampleJsonDoc; + + public ParsingHelpersTests() + { + exampleKeyValuePair = "foo=bar;foo1=bar1;foo2=bar3"; + var json = """ + { + "foo": ["a", "b", "c"], + "bar": { + "foo1": "bar1" + } + } + """; + exampleJsonDoc = JsonDocument.Parse(json); + } + + [Fact] + public void GetList() + { + var listOfString = ParsingHelpers.GetList(exampleJsonDoc.RootElement.GetProperty("foo"), + (v) => + { + var value = v.GetString(); + return string.IsNullOrWhiteSpace(value) ? string.Empty : value; + }); + Assert.Equal(3, listOfString.Count); + Assert.Equal(listOfString, new List { "a", "b", "c" }); + } + + [Fact] + public void GetMap() + { + var map = ParsingHelpers.GetMap(exampleJsonDoc.RootElement.GetProperty("bar"), + (v) => + { + var value = v.GetString(); + return string.IsNullOrWhiteSpace(value) ? string.Empty : value; + }); + Assert.Equal(1, map?.Count); + Assert.Equal("bar1", map?["foo1"]); + } + + [Fact] + public void GetOrderedMap() + { + var orderedMap = ParsingHelpers.GetOrderedMap(exampleJsonDoc.RootElement.GetProperty("bar"), + (v) => + { + var value = v.GetString(); + return string.IsNullOrWhiteSpace(value) ? string.Empty : value; + }); + Assert.Equal(1, orderedMap?.Count); + Assert.Equal("bar1", orderedMap?["foo1"]); + } + + [Fact] + public void GetMapOfString() + { + var mapOfString = ParsingHelpers.GetMapOfString(exampleJsonDoc.RootElement.GetProperty("bar")); + Assert.Equal(1, mapOfString?.Count); + Assert.Equal("bar1", mapOfString?["foo1"]); + } + + [Fact] + public void GetListOfString() + { + var listOfString = ParsingHelpers.GetListOfString(exampleJsonDoc.RootElement.GetProperty("foo")); + Assert.Equal(3, listOfString.Count); + Assert.Equal(listOfString, new List { "a", "b", "c" }); + } + + [Fact] + public void GetHashSetOfString() + { + var hashSetOfString = ParsingHelpers.GetHashSetOfString(exampleJsonDoc.RootElement.GetProperty("foo")); + Assert.Equal(3, hashSetOfString.Count); + Assert.Equal(hashSetOfString, new HashSet { "a", "b", "c" }); + } + + [Fact] + public void GetOrderedHashSetOfString() + { + var hashSetOfString = ParsingHelpers.GetOrderedHashSetOfString(exampleJsonDoc.RootElement.GetProperty("foo")); + Assert.Equal(3, hashSetOfString.Count); + Assert.Equal(hashSetOfString, new SortedSet { "a", "b", "c" }); + } + + [Fact] + public void ParseProperties() + { + var kvPairs = ParsingHelpers.ParseProperties(exampleKeyValuePair); + Assert.Equal(3, kvPairs.Count); + Assert.Equal("bar", kvPairs["foo"]); + Assert.Equal("bar1", kvPairs["foo1"]); + Assert.Equal("bar3", kvPairs["foo2"]); + } + + [Fact] + public void ParseKeyValuePair() + { + var kvPairs = ParsingHelpers.ParseKey(exampleKeyValuePair); + Assert.Equal(3, kvPairs.Count()); + } + } +} From 36189d1950252955136550becc367b1ed6f00aaa Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Tue, 10 Oct 2023 10:12:10 -0700 Subject: [PATCH 11/12] chore: Use ThrowIfNull for validating null objects. --- src/lib/Helpers/ValidationHelpers.cs | 5 ++--- src/lib/OpenAI/Authentication/ManifestUserHttpAuth.cs | 3 ++- src/lib/OpenAI/OpenAIPluginManifest.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/Helpers/ValidationHelpers.cs b/src/lib/Helpers/ValidationHelpers.cs index 3f7ff17..0299c9a 100644 --- a/src/lib/Helpers/ValidationHelpers.cs +++ b/src/lib/Helpers/ValidationHelpers.cs @@ -10,10 +10,9 @@ internal static void ValidateNullOrWhitespace(string parameterName, string? valu throw new ArgumentNullException(parameterName, string.Format(ErrorMessage.FieldIsRequired, parameterName, parentName)); } - internal static void ValidateNullObject(string parameterName, object? value, string parentName) + internal static void ValidateNullObject(string parameterName, object? value) { - if (value == null) - throw new ArgumentNullException(parameterName, string.Format(ErrorMessage.FieldIsRequired, parameterName, parentName)); + ArgumentNullException.ThrowIfNull(value, parameterName); } internal static void ValidateLength(string parameterName, string? value, int maxLength) diff --git a/src/lib/OpenAI/Authentication/ManifestUserHttpAuth.cs b/src/lib/OpenAI/Authentication/ManifestUserHttpAuth.cs index 45692c8..5435dab 100644 --- a/src/lib/OpenAI/Authentication/ManifestUserHttpAuth.cs +++ b/src/lib/OpenAI/Authentication/ManifestUserHttpAuth.cs @@ -11,7 +11,8 @@ public class ManifestUserHttpAuth : BaseManifestAuth public string? AuthorizationType { get; set; } public ManifestUserHttpAuth(string? authorizationType) { - if (string.IsNullOrWhiteSpace(authorizationType) || (authorizationType != "basic" && authorizationType != "bearer")) + if (string.IsNullOrWhiteSpace(authorizationType) || + (!string.Equals(authorizationType, "basic", StringComparison.OrdinalIgnoreCase) && !string.Equals(authorizationType, "bearer", StringComparison.OrdinalIgnoreCase))) { // Reference: https://platform.openai.com/docs/plugins/authentication/user-level throw new ArgumentException($"{nameof(authorizationType)} must be either 'basic' or 'bearer'."); diff --git a/src/lib/OpenAI/OpenAIPluginManifest.cs b/src/lib/OpenAI/OpenAIPluginManifest.cs index ed38c77..ac586e5 100644 --- a/src/lib/OpenAI/OpenAIPluginManifest.cs +++ b/src/lib/OpenAI/OpenAIPluginManifest.cs @@ -137,8 +137,8 @@ private void Validate(OpenAIPluginManifest openAIPluginManifest) ValidationHelpers.ValidateLength(nameof(DescriptionForModel), openAIPluginManifest.DescriptionForModel, 8000); ValidationHelpers.ValidateNullOrWhitespace(nameof(SchemaVersion), openAIPluginManifest.SchemaVersion, nameof(OpenAIPluginManifest)); - ValidationHelpers.ValidateNullObject(nameof(Auth), openAIPluginManifest.Auth, nameof(OpenAIPluginManifest)); - ValidationHelpers.ValidateNullObject(nameof(Api), openAIPluginManifest.Api, nameof(OpenAIPluginManifest)); + ValidationHelpers.ValidateNullObject(nameof(Auth), openAIPluginManifest.Auth); + ValidationHelpers.ValidateNullObject(nameof(Api), openAIPluginManifest.Api); ValidationHelpers.ValidateNullOrWhitespace(nameof(LogoUrl), openAIPluginManifest.LogoUrl, nameof(OpenAIPluginManifest)); ValidationHelpers.ValidateEmail(nameof(ContactEmail), openAIPluginManifest.ContactEmail, nameof(OpenAIPluginManifest)); ValidationHelpers.ValidateNullOrWhitespace(nameof(LegalInfoUrl), openAIPluginManifest.LegalInfoUrl, nameof(OpenAIPluginManifest)); From ff2841e5b8f569b2bdf63e4a7d95bc366f9e955f Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Tue, 10 Oct 2023 10:21:45 -0700 Subject: [PATCH 12/12] chore: Remove explicit caller argument info on ThrowIfNull. --- src/lib/Helpers/ValidationHelpers.cs | 5 ----- src/lib/OpenAI/OpenAIPluginManifest.cs | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/lib/Helpers/ValidationHelpers.cs b/src/lib/Helpers/ValidationHelpers.cs index 0299c9a..0e193dc 100644 --- a/src/lib/Helpers/ValidationHelpers.cs +++ b/src/lib/Helpers/ValidationHelpers.cs @@ -10,11 +10,6 @@ internal static void ValidateNullOrWhitespace(string parameterName, string? valu throw new ArgumentNullException(parameterName, string.Format(ErrorMessage.FieldIsRequired, parameterName, parentName)); } - internal static void ValidateNullObject(string parameterName, object? value) - { - ArgumentNullException.ThrowIfNull(value, parameterName); - } - internal static void ValidateLength(string parameterName, string? value, int maxLength) { if (value?.Length > maxLength) diff --git a/src/lib/OpenAI/OpenAIPluginManifest.cs b/src/lib/OpenAI/OpenAIPluginManifest.cs index ac586e5..7ab27c7 100644 --- a/src/lib/OpenAI/OpenAIPluginManifest.cs +++ b/src/lib/OpenAI/OpenAIPluginManifest.cs @@ -137,8 +137,8 @@ private void Validate(OpenAIPluginManifest openAIPluginManifest) ValidationHelpers.ValidateLength(nameof(DescriptionForModel), openAIPluginManifest.DescriptionForModel, 8000); ValidationHelpers.ValidateNullOrWhitespace(nameof(SchemaVersion), openAIPluginManifest.SchemaVersion, nameof(OpenAIPluginManifest)); - ValidationHelpers.ValidateNullObject(nameof(Auth), openAIPluginManifest.Auth); - ValidationHelpers.ValidateNullObject(nameof(Api), openAIPluginManifest.Api); + ArgumentNullException.ThrowIfNull(openAIPluginManifest.Auth); + ArgumentNullException.ThrowIfNull(openAIPluginManifest.Api); ValidationHelpers.ValidateNullOrWhitespace(nameof(LogoUrl), openAIPluginManifest.LogoUrl, nameof(OpenAIPluginManifest)); ValidationHelpers.ValidateEmail(nameof(ContactEmail), openAIPluginManifest.ContactEmail, nameof(OpenAIPluginManifest)); ValidationHelpers.ValidateNullOrWhitespace(nameof(LegalInfoUrl), openAIPluginManifest.LegalInfoUrl, nameof(OpenAIPluginManifest));