From b61754f706320930b981b7459ba5c944c54a5599 Mon Sep 17 00:00:00 2001 From: Azad Abbasi Date: Thu, 11 Feb 2021 12:14:25 -0800 Subject: [PATCH 1/5] Initial Commit - Bring ModelRepository Project to the mono repo --- .../Azure.Iot.ModelsRepository/.gitignore | 1 + .../Azure.Iot.ModelsRepository.sln | 37 +++ .../src/Azure.Iot.ModelsRepository.csproj | 23 ++ .../src/DtmiConventions.cs | 32 ++ .../src/Fetchers/FetchResult.cs | 15 + .../src/Fetchers/IModelFetcher.cs | 16 + .../src/Fetchers/LocalModelFetcher.cs | 70 ++++ .../src/Fetchers/RemoteModelFetcher.cs | 99 ++++++ .../src/ModelMetadata.cs | 23 ++ .../src/ModelQuery.cs | 176 +++++++++++ .../src/Properties/AssemblyInfo.cs | 6 + .../src/RepositoryHandler.cs | 118 +++++++ .../src/ResolverClient.cs | 129 ++++++++ .../src/ResolverClientOptions.cs | 72 +++++ .../src/ResolverEventSource.cs | 103 ++++++ .../src/ResolverException.cs | 53 ++++ .../src/StandardStrings.cs | 19 ++ .../Azure.Iot.ModelsRepository.Tests.csproj | 35 ++ .../tests/ClientTests.cs | 88 ++++++ .../tests/DtmiConversionTests.cs | 48 +++ .../tests/ModelQueryTests.cs | 188 +++++++++++ .../tests/ResolveIntegrationTests.cs | 298 ++++++++++++++++++ .../tests/TestHelpers.cs | 52 +++ .../devicemanagement/deviceinformation-1.json | 64 ++++ .../devicemanagement/deviceinformation-2.json | 16 + .../dtmi/com/example/base-1.json | 56 ++++ .../dtmi/com/example/base-2.json | 57 ++++ .../dtmi/com/example/building-1.json | 19 ++ .../dtmi/com/example/camera-3.json | 13 + .../dtmi/com/example/coldstorage-1.json | 13 + .../dtmi/com/example/conferenceroom-1.json | 13 + .../dtmi/com/example/freezer-1.json | 12 + .../incompleteexpanded-1.expanded.json | 151 +++++++++ .../dtmi/com/example/invalidmodel-1.json | 13 + .../dtmi/com/example/invalidmodel-2.json | 23 ++ .../dtmi/com/example/phone-2.json | 23 ++ .../dtmi/com/example/room-1.json | 12 + .../temperaturecontroller-1.expanded.json | 215 +++++++++++++ .../com/example/temperaturecontroller-1.json | 60 ++++ .../dtmi/com/example/thermostat-1.json | 19 ++ .../dtmi/company/demodevice-1.json | 31 ++ .../dtmi/company/demodevice-2.json | 31 ++ .../dtmi/strict/badfilepath-1.json | 12 + .../dtmi/strict/emptyarray-1.json | 1 + .../dtmi/strict/namespaceconflict-1.json | 37 +++ .../TestModelRepo/dtmi/strict/nondtdl-1.json | 1 + .../dtmi/strict/unsupportedrootarray-1.json | 91 ++++++ 47 files changed, 2684 insertions(+) create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/.gitignore create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/Azure.Iot.ModelsRepository.sln create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/IModelFetcher.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelQuery.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Properties/AssemblyInfo.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverException.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/Azure.Iot.ModelsRepository.Tests.csproj create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/DtmiConversionTests.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ResolveIntegrationTests.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-2.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-2.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/building-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/camera-3.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/coldstorage-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/conferenceroom-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/freezer-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/incompleteexpanded-1.expanded.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-2.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/phone-2.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/room-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.expanded.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/thermostat-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-2.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/badfilepath-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/emptyarray-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/namespaceconflict-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/nondtdl-1.json create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/unsupportedrootarray-1.json diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/.gitignore b/sdk/modelsrepository/Azure.Iot.ModelsRepository/.gitignore new file mode 100644 index 000000000000..031950aa8414 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/.gitignore @@ -0,0 +1 @@ +launchSettings.json diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/Azure.Iot.ModelsRepository.sln b/sdk/modelsrepository/Azure.Iot.ModelsRepository/Azure.Iot.ModelsRepository.sln new file mode 100644 index 000000000000..68ee35711dec --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/Azure.Iot.ModelsRepository.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30717.126 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Iot.ModelsRepository", "src\Azure.Iot.ModelsRepository.csproj", "{5E11A377-0D20-49F8-952B-50390196EF4B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Iot.ModelsRepository.Tests", "tests\Azure.Iot.ModelsRepository.Tests.csproj", "{092E6CE2-9998-428C-A801-2BAB4E14A577}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Core.TestFramework", "..\..\core\Azure.Core.TestFramework\src\Azure.Core.TestFramework.csproj", "{1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5E11A377-0D20-49F8-952B-50390196EF4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E11A377-0D20-49F8-952B-50390196EF4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E11A377-0D20-49F8-952B-50390196EF4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E11A377-0D20-49F8-952B-50390196EF4B}.Release|Any CPU.Build.0 = Release|Any CPU + {092E6CE2-9998-428C-A801-2BAB4E14A577}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {092E6CE2-9998-428C-A801-2BAB4E14A577}.Debug|Any CPU.Build.0 = Debug|Any CPU + {092E6CE2-9998-428C-A801-2BAB4E14A577}.Release|Any CPU.ActiveCfg = Release|Any CPU + {092E6CE2-9998-428C-A801-2BAB4E14A577}.Release|Any CPU.Build.0 = Release|Any CPU + {1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {671D1EFB-2BB9-4846-91EF-A3FB1FF9DDA6} + EndGlobalSection +EndGlobal diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj new file mode 100644 index 000000000000..467935155015 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj @@ -0,0 +1,23 @@ + + + + Azure IoT Models Repository SDK + $(RequiredTargetFrameworks) + + true + + + + + IoT;ModelsRepository;$(PackageCommonTags) + SDK for the Azure IoT Models Repository Tools + 1.0.0-beta.1 + + + + + + + + + diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs new file mode 100644 index 000000000000..9709a6359234 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Azure.Iot.ModelsRepository +{ + internal class DtmiConventions + { + public static bool IsDtmi(string dtmi) => !string.IsNullOrEmpty(dtmi) && new Regex(@"^dtmi:[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?(?::[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)*;[1-9][0-9]{0,8}$").IsMatch(dtmi); + public static string DtmiToPath(string dtmi) => IsDtmi(dtmi) ? $"{dtmi.ToLowerInvariant().Replace(":", "/").Replace(";", "-")}.json" : null; + + public static string DtmiToQualifiedPath(string dtmi, string basePath, bool fromExpanded = false) + { + string dtmiPath = DtmiToPath(dtmi); + if (dtmiPath == null) + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, StandardStrings.InvalidDtmiFormat, dtmi)); + + if (!basePath.EndsWith("/", StringComparison.InvariantCultureIgnoreCase)) + basePath += "/"; + + string fullyQualifiedPath = $"{basePath}{dtmiPath}"; + + if (fromExpanded) + fullyQualifiedPath = fullyQualifiedPath.Replace(".json", ".expanded.json"); + + return fullyQualifiedPath; + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs new file mode 100644 index 000000000000..6fb78ac8d4d3 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Iot.ModelsRepository.Fetchers +{ + internal class FetchResult + { + public string Definition { get; set; } + public string Path { get; set; } + public bool FromExpanded + { + get { return Path.EndsWith("expanded.json", System.StringComparison.InvariantCultureIgnoreCase); } + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/IModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/IModelFetcher.cs new file mode 100644 index 000000000000..696ad7417d6f --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/IModelFetcher.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Iot.ModelsRepository.Fetchers +{ + internal interface IModelFetcher + { + Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default); + + FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default); + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs new file mode 100644 index 000000000000..cb38de270c98 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading.Tasks; +using System.Text; +using System.Collections.Generic; +using System.Threading; +using System.Globalization; + +namespace Azure.Iot.ModelsRepository.Fetchers +{ + internal class LocalModelFetcher : IModelFetcher + { + private readonly bool _tryExpanded; + + public LocalModelFetcher(ResolverClientOptions clientOptions) + { + _tryExpanded = clientOptions.DependencyResolution == DependencyResolutionOption.TryFromExpanded; + } + + public async Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) + { + return await Task.Run(() => Fetch(dtmi, repositoryUri, cancellationToken)).ConfigureAwait(false); + } + + public FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) + { + Queue work = new Queue(); + + if (_tryExpanded) + work.Enqueue(GetPath(dtmi, repositoryUri, true)); + + work.Enqueue(GetPath(dtmi, repositoryUri, false)); + + string fnfError = string.Empty; + while (work.Count != 0 && !cancellationToken.IsCancellationRequested) + { + string tryContentPath = work.Dequeue(); + ResolverEventSource.Shared.FetchingModelContent(tryContentPath); + + if (EvaluatePath(tryContentPath)) + { + return new FetchResult() + { + Definition = File.ReadAllText(tryContentPath, Encoding.UTF8), + Path = tryContentPath + }; + } + + ResolverEventSource.Shared.ErrorFetchingModelContent(tryContentPath); + fnfError = string.Format(CultureInfo.InvariantCulture, StandardStrings.ErrorFetchingModelContent, tryContentPath); + } + + throw new FileNotFoundException(fnfError); + } + + private static string GetPath(string dtmi, Uri repositoryUri, bool expanded = false) + { + string registryPath = repositoryUri.AbsolutePath; + return DtmiConventions.DtmiToQualifiedPath(dtmi, registryPath, expanded); + } + + private static bool EvaluatePath(string path) + { + return File.Exists(path); + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs new file mode 100644 index 000000000000..b1379e5cbbe1 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Core.Pipeline; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Iot.ModelsRepository.Fetchers +{ + internal class RemoteModelFetcher : IModelFetcher + { + private readonly HttpPipeline _pipeline; + private readonly bool _tryExpanded; + + public RemoteModelFetcher(ResolverClientOptions clientOptions) + { + _pipeline = CreatePipeline(clientOptions); + _tryExpanded = clientOptions.DependencyResolution == DependencyResolutionOption.TryFromExpanded; + } + + public FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public async Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) + { + Queue work = new Queue(); + + if (_tryExpanded) + work.Enqueue(GetPath(dtmi, repositoryUri, true)); + + work.Enqueue(GetPath(dtmi, repositoryUri, false)); + + string remoteFetchError = string.Empty; + while (work.Count != 0 && !cancellationToken.IsCancellationRequested) + { + string tryContentPath = work.Dequeue(); + ResolverEventSource.Shared.FetchingModelContent(tryContentPath); + + string content = await EvaluatePathAsync(tryContentPath, cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(content)) + { + return new FetchResult() + { + Definition = content, + Path = tryContentPath + }; + } + + ResolverEventSource.Shared.ErrorFetchingModelContent(tryContentPath); + remoteFetchError = string.Format(CultureInfo.InvariantCulture, StandardStrings.ErrorFetchingModelContent, tryContentPath); + } + + throw new RequestFailedException(remoteFetchError); + } + + private static string GetPath(string dtmi, Uri repositoryUri, bool expanded = false) + { + string absoluteUri = repositoryUri.AbsoluteUri; + return DtmiConventions.DtmiToQualifiedPath(dtmi, absoluteUri, expanded); + } + + private async Task EvaluatePathAsync(string path, CancellationToken cancellationToken) + { + Request request = _pipeline.CreateRequest(); + request.Method = RequestMethod.Get; + request.Uri = new RequestUriBuilder(); + request.Uri.Reset(new Uri(path)); + + Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); + if (response.Status >= 200 && response.Status <= 299) + { + return await GetContentAsync(response.ContentStream, cancellationToken).ConfigureAwait(false); + } + + return null; + } + private static async Task GetContentAsync(Stream content, CancellationToken cancellationToken) + { + using (JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait(false)) + { + JsonElement root = json.RootElement; + return root.GetRawText(); + } + } + + private static HttpPipeline CreatePipeline(ResolverClientOptions options) + { + return HttpPipelineBuilder.Build(options); + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs new file mode 100644 index 000000000000..fac3b5fbf36f --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; + +namespace Azure.Iot.ModelsRepository +{ + internal class ModelMetadata + { + public string Id { get; } + public IList Extends { get; } + public IList ComponentSchemas { get; } + public IList Dependencies { get { return Extends.Union(ComponentSchemas).ToList(); } } + + public ModelMetadata(string id, IList extends, IList componentSchemas) + { + this.Id = id; + this.Extends = extends; + this.ComponentSchemas = componentSchemas; + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelQuery.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelQuery.cs new file mode 100644 index 000000000000..ddbea223d2cb --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelQuery.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Azure.Iot.ModelsRepository +{ + internal class ModelQuery + { + private readonly string _content; + private readonly JsonDocumentOptions _parseOptions; + + public ModelQuery(string content) + { + _content = content; + _parseOptions = new JsonDocumentOptions + { + AllowTrailingCommas = true + }; + } + + public ModelMetadata GetMetadata() + { + return new ModelMetadata(GetId(), GetExtends(), GetComponentSchemas()); + } + + public string GetId() + { + using (JsonDocument document = JsonDocument.Parse(_content, _parseOptions)) + { + JsonElement _root = document.RootElement; + + if (_root.ValueKind == JsonValueKind.Object && _root.TryGetProperty("@id", out JsonElement id)) + { + if (id.ValueKind == JsonValueKind.String) + { + return id.GetString(); + } + } + } + + return string.Empty; + } + + public IList GetExtends() + { + List dependencies = new List(); + + using (JsonDocument document = JsonDocument.Parse(_content, _parseOptions)) + { + JsonElement _root = document.RootElement; + + if (_root.ValueKind == JsonValueKind.Object && _root.TryGetProperty("extends", out JsonElement extends)) + { + if (extends.ValueKind == JsonValueKind.Array) + { + foreach (JsonElement extendElement in extends.EnumerateArray()) + { + if (extendElement.ValueKind == JsonValueKind.String) + { + dependencies.Add(extendElement.GetString()); + } + else if (extendElement.ValueKind == JsonValueKind.Object) + { + // extends can have multiple levels and can contain components. + // TODO: Support object ctor - inefficient serialize. + ModelMetadata nested_interface = new ModelQuery(JsonSerializer.Serialize(extendElement)).GetMetadata(); + dependencies.AddRange(nested_interface.Dependencies); + } + } + } + else if (extends.ValueKind == JsonValueKind.String) + { + dependencies.Add(extends.GetString()); + } + } + } + + return dependencies; + } + + // TODO: Consider refactor to an object type based processing. + public IList GetComponentSchemas() + { + List componentSchemas = new List(); + + using (JsonDocument document = JsonDocument.Parse(_content, _parseOptions)) + { + JsonElement _root = document.RootElement; + + if (_root.ValueKind == JsonValueKind.Object && _root.TryGetProperty("contents", out JsonElement contents)) + { + if (contents.ValueKind == JsonValueKind.Array) + { + foreach (JsonElement element in contents.EnumerateArray()) + { + if (element.TryGetProperty("@type", out JsonElement type)) + { + if (type.ValueKind == JsonValueKind.String && type.GetString() == "Component") + { + if (element.TryGetProperty("schema", out JsonElement schema)) + { + if (schema.ValueKind == JsonValueKind.String) + { + componentSchemas.Add(schema.GetString()); + } + else if (schema.ValueKind == JsonValueKind.Array) + { + foreach (JsonElement schemaElement in schema.EnumerateArray()) + { + if (schemaElement.ValueKind == JsonValueKind.String) + { + componentSchemas.Add(schemaElement.GetString()); + } + } + } + else if (schema.ValueKind == JsonValueKind.Object) + { + if (schema.TryGetProperty("extends", out JsonElement schemaObjExtends)) + { + if (schemaObjExtends.ValueKind == JsonValueKind.String) + { + componentSchemas.Add(schemaObjExtends.GetString()); + } + } + } + } + } + } + } + } + } + } + + return componentSchemas; + } + + public async Task> ListToDictAsync() + { + Dictionary result = new Dictionary(); + + using (JsonDocument document = JsonDocument.Parse(_content, _parseOptions)) + { + JsonElement _root = document.RootElement; + + if (_root.ValueKind == JsonValueKind.Array) + { + foreach (JsonElement element in _root.EnumerateArray()) + { + if (element.ValueKind == JsonValueKind.Object) + { + using (MemoryStream stream = new MemoryStream()) + { + await JsonSerializer.SerializeAsync(stream, element).ConfigureAwait(false); + stream.Position = 0; + + using (StreamReader streamReader = new StreamReader(stream)) + { + string serialized = await streamReader.ReadToEndAsync().ConfigureAwait(false); + + string id = new ModelQuery(serialized).GetId(); + result.Add(id, serialized); + } + } + } + } + } + } + + return result; + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Properties/AssemblyInfo.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..0128a60627e3 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Azure.Iot.ModelsRepository.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")] diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs new file mode 100644 index 000000000000..e2ee13d72d0d --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Iot.ModelsRepository.Fetchers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Iot.ModelsRepository +{ + internal class RepositoryHandler + { + private readonly IModelFetcher _modelFetcher; + private readonly Guid _clientId; + + public Uri RepositoryUri { get; } + public ResolverClientOptions ClientOptions { get; } + + public RepositoryHandler(Uri repositoryUri, ResolverClientOptions options = null) + { + ClientOptions = options ?? new ResolverClientOptions(); + RepositoryUri = repositoryUri; + _modelFetcher = repositoryUri.Scheme == "file" ? + _modelFetcher = new LocalModelFetcher(ClientOptions) : + _modelFetcher = new RemoteModelFetcher(ClientOptions); + _clientId = Guid.NewGuid(); + ResolverEventSource.Shared.InitFetcher(_clientId, repositoryUri.Scheme); + } + + public async Task> ProcessAsync(string dtmi, CancellationToken cancellationToken) + { + return await ProcessAsync(new List() { dtmi }, cancellationToken).ConfigureAwait(false); + } + + public async Task> ProcessAsync(IEnumerable dtmis, CancellationToken cancellationToken) + { + Dictionary processedModels = new Dictionary(); + Queue toProcessModels = new Queue(); + + foreach (string dtmi in dtmis) + { + if (!DtmiConventions.IsDtmi(dtmi)) + { + ResolverEventSource.Shared.InvalidDtmiInput(dtmi); + string invalidArgMsg = string.Format(CultureInfo.InvariantCulture, StandardStrings.InvalidDtmiFormat, dtmi); + throw new ResolverException(dtmi, invalidArgMsg, new ArgumentException(invalidArgMsg)); + } + + toProcessModels.Enqueue(dtmi); + } + + while (toProcessModels.Count != 0 && !cancellationToken.IsCancellationRequested) + { + string targetDtmi = toProcessModels.Dequeue(); + if (processedModels.ContainsKey(targetDtmi)) + { + ResolverEventSource.Shared.SkippingPreprocessedDtmi(targetDtmi); + continue; + } + ResolverEventSource.Shared.ProcessingDtmi(targetDtmi); + + FetchResult result = await FetchAsync(targetDtmi, cancellationToken).ConfigureAwait(false); + if (result.FromExpanded) + { + Dictionary expanded = await new ModelQuery(result.Definition).ListToDictAsync().ConfigureAwait(false); + foreach (KeyValuePair kvp in expanded) + { + if (!processedModels.ContainsKey(kvp.Key)) + processedModels.Add(kvp.Key, kvp.Value); + } + + continue; + } + + ModelMetadata metadata = new ModelQuery(result.Definition).GetMetadata(); + + if (ClientOptions.DependencyResolution >= DependencyResolutionOption.Enabled) + { + IList dependencies = metadata.Dependencies; + + if (dependencies.Count > 0) + ResolverEventSource.Shared.DiscoveredDependencies(string.Join("\", \"", dependencies)); + + foreach (string dep in dependencies) + { + toProcessModels.Enqueue(dep); + } + } + + string parsedDtmi = metadata.Id; + if (!parsedDtmi.Equals(targetDtmi, StringComparison.Ordinal)) + { + ResolverEventSource.Shared.IncorrectDtmiCasing(targetDtmi, parsedDtmi); + string formatErrorMsg = string.Format(CultureInfo.InvariantCulture, StandardStrings.IncorrectDtmiCasing, targetDtmi, parsedDtmi); + throw new ResolverException(targetDtmi, formatErrorMsg, new FormatException(formatErrorMsg)); + } + + processedModels.Add(targetDtmi, result.Definition); + } + + return processedModels; + } + + private async Task FetchAsync(string dtmi, CancellationToken cancellationToken) + { + try + { + return await _modelFetcher.FetchAsync(dtmi, RepositoryUri, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new ResolverException(dtmi, ex.Message, ex); + } + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs new file mode 100644 index 000000000000..fd550a1577d3 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Iot.ModelsRepository +{ + /// + /// The ResolverClient class supports DTDL model resolution providing functionality to + /// resolve models by retrieving model definitions and their dependencies. + /// + public class ResolverClient + { + internal const string DefaultRepository = "https://devicemodels.azure.com"; + private readonly RepositoryHandler _repositoryHandler; + + /// + /// Initializes the ResolverClient with default client options while pointing to + /// the Azure IoT Plug and Play Model repository https://devicemodels.azure.com for resolution. + /// + public ResolverClient() : this(new Uri(DefaultRepository), null) { } + + /// + /// Initializes the ResolverClient with default client options while pointing to + /// a custom for resolution. + /// + /// + /// The model repository Uri value. This can be a remote endpoint or local directory. + /// + public ResolverClient(Uri repositoryUri) : this(repositoryUri, null) { } + + /// + /// Initializes the ResolverClient with custom client while pointing to + /// the Azure IoT Plug and Play Model repository https://devicemodels.azure.com for resolution. + /// + /// + /// ResolverClientOptions to configure resolution and client behavior. + /// + public ResolverClient(ResolverClientOptions options) : this(new Uri(DefaultRepository), options) { } + + /// + /// Initializes the ResolverClient with custom client while pointing to + /// a custom for resolution. + /// + /// + /// The model repository Uri. This can be a remote endpoint or local directory. + /// + /// + /// ResolverClientOptions to configure resolution and client behavior. + /// + public ResolverClient(Uri repositoryUri, ResolverClientOptions options) + { + _repositoryHandler = new RepositoryHandler(repositoryUri, options); + } + + /// + /// Initializes the ResolverClient with default client options while pointing to + /// a custom for resolution. + /// + /// + /// The model repository Uri in string format. This can be a remote endpoint or local directory. + /// + public ResolverClient(string repositoryUriStr) : this(repositoryUriStr, null) { } + + /// + /// Initializes the ResolverClient with custom client while pointing to + /// a custom for resolution. + /// + /// + /// The model repository Uri in string format. This can be a remote endpoint or local directory. + /// + /// + /// ResolverClientOptions to configure resolution and client behavior. + /// + public ResolverClient(string repositoryUriStr, ResolverClientOptions options) : this(new Uri(repositoryUriStr), options) { } + + /// + /// Resolves a model definition identified by and optionally its dependencies. + /// + /// + /// An IDictionary containing the model definition(s) where the key is the dtmi + /// and the value is the raw model definition string. + /// + /// Thrown when a resolution failure occurs. + /// A well-formed DTDL model Id. For example 'dtmi:com:example:Thermostat;1'. + /// The cancellationToken. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "AZC0004:DO provide both asynchronous and synchronous variants for all service methods.", Justification = "")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "AZC0015:Unexpected client method return type.", Justification = "")] + public virtual async Task> ResolveAsync(string dtmi, CancellationToken cancellationToken = default) + { + return await _repositoryHandler.ProcessAsync(dtmi, cancellationToken).ConfigureAwait(false); + } + + /// + /// Resolves a collection of model definitions identified by and optionally their dependencies. + /// + /// + /// An IDictionary containing the model definition(s) where the key is the dtmi + /// and the value is the raw model definition string. + /// + /// Thrown when a resolution failure occurs. + /// A collection of well-formed DTDL model Ids. + /// The cancellationToken. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "AZC0004:DO provide both asynchronous and synchronous variants for all service methods.", Justification = "")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "AZC0015:Unexpected client method return type.", Justification = "")] + public virtual async Task> ResolveAsync(IEnumerable dtmis, CancellationToken cancellationToken = default) + { + return await _repositoryHandler.ProcessAsync(dtmis, cancellationToken).ConfigureAwait(false); + } + + /// + /// Evaluates whether an input is valid. + /// + public static bool IsValidDtmi(string dtmi) => DtmiConventions.IsDtmi(dtmi); + + /// + /// Gets the Uri associated with the ResolverClient instance. + /// + public Uri RepositoryUri => _repositoryHandler.RepositoryUri; + + /// + /// Gets the ResolverClientOptions associated with the ResolverClient instance. + /// + public ResolverClientOptions ClientOptions => _repositoryHandler.ClientOptions; + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs new file mode 100644 index 000000000000..843f92203455 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core; + +namespace Azure.Iot.ModelsRepository +{ + /// + /// Options that allow configuration of requests sent to the ModelRepositoryService. + /// + public class ResolverClientOptions : ClientOptions + { + internal const ServiceVersion LatestVersion = ServiceVersion.V2021_02_11; + + /// + /// The versions of Azure Digital Twins supported by this client + /// library. + /// + public enum ServiceVersion + { + /// + /// 2021_02_11 + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "")] + V2021_02_11 = 1 + } + + /// + /// Gets the of the service API used when + /// making requests. + /// + public ServiceVersion Version { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The of the service API used when + /// making requests. + /// + /// The dependency processing options. + public ResolverClientOptions(ServiceVersion version = LatestVersion, DependencyResolutionOption resolutionOption = DependencyResolutionOption.Enabled) + { + DependencyResolution = resolutionOption; + Version = version; + } + + /// + /// The dependency processing options. + /// + public DependencyResolutionOption DependencyResolution { get; } + } + + /// + /// The dependency processing options. + /// + public enum DependencyResolutionOption + { + /// + /// Do not process external dependencies. + /// + Disabled, + /// + /// Enable external dependencies. + /// + Enabled, + /// + /// Try to get external dependencies using .expanded.json. + /// + TryFromExpanded + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs new file mode 100644 index 000000000000..62b47ef3e2a1 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.Diagnostics; +using System; +using System.Diagnostics.Tracing; + +namespace Azure.Iot.ModelsRepository +{ + [EventSource(Name = EventSourceName)] + internal sealed class ResolverEventSource : EventSource + { + // Set EventSource name to package name replacing . with - + private const string EventSourceName = "Azure-Iot-ModelsRepository"; + + // Event ids defined as constants to makes it easy to keep track of them + private const int InitFetcherEventId = 1000; + private const int ProcessingDtmiEventId = 2000; + private const int FetchingModelContentEventId = 2001; + private const int DiscoveredDependenciesEventId = 2002; + private const int SkippingPreprocessedDtmiEventId = 2003; + private const int InvalidDtmiInputEventId = 4000; + private const int ErrorFetchingModelContentEventId = 4004; + private const int IncorrectDtmiCasingEventId = 4006; + + public static ResolverEventSource Shared { get; } = new ResolverEventSource(); + + private ResolverEventSource() : base(EventSourceName, EventSourceSettings.Default, AzureEventSourceListener.TraitName, AzureEventSourceListener.TraitValue) { } + + [Event(InitFetcherEventId, Level = EventLevel.Informational, Message = StandardStrings.ClientInitWithFetcher)] + public void InitFetcher(Guid clientId, string scheme) + { + // We are calling Guid.ToString make sure anyone is listening before spending resources + if (IsEnabled(EventLevel.Informational, EventKeywords.None)) + { + WriteEvent(InitFetcherEventId, clientId.ToString("N"), scheme); + } + } + + [Event(InvalidDtmiInputEventId, Level = EventLevel.Error, Message = StandardStrings.InvalidDtmiFormat)] + public void InvalidDtmiInput(string dtmi) + { + if (IsEnabled(EventLevel.Error, EventKeywords.None)) + { + WriteEvent(InvalidDtmiInputEventId, dtmi); + } + } + + [Event(SkippingPreprocessedDtmiEventId, Level = EventLevel.Informational, Message = StandardStrings.SkippingPreProcessedDtmi)] + public void SkippingPreprocessedDtmi(string dtmi) + { + if (IsEnabled(EventLevel.Informational, EventKeywords.None)) + { + WriteEvent(SkippingPreprocessedDtmiEventId, dtmi); + } + } + + [Event(ProcessingDtmiEventId, Level = EventLevel.Informational, Message = StandardStrings.ProcessingDtmi)] + public void ProcessingDtmi(string dtmi) + { + if (IsEnabled(EventLevel.Informational, EventKeywords.None)) + { + WriteEvent(ProcessingDtmiEventId, dtmi); + } + } + + [Event(DiscoveredDependenciesEventId, Level = EventLevel.Informational, Message = StandardStrings.DiscoveredDependencies)] + public void DiscoveredDependencies(string dependencies) + { + if (IsEnabled(EventLevel.Informational, EventKeywords.None)) + { + WriteEvent(DiscoveredDependenciesEventId, dependencies); + } + } + + [Event(IncorrectDtmiCasingEventId, Level = EventLevel.Error, Message = StandardStrings.IncorrectDtmiCasing)] + public void IncorrectDtmiCasing(string expected, string parsed) + { + if (IsEnabled(EventLevel.Error, EventKeywords.None)) + { + WriteEvent(IncorrectDtmiCasingEventId, expected, parsed); + } + } + + [Event(FetchingModelContentEventId, Level = EventLevel.Informational, Message = StandardStrings.FetchingModelContent)] + public void FetchingModelContent(string path) + { + if (IsEnabled(EventLevel.Informational, EventKeywords.None)) + { + WriteEvent(FetchingModelContentEventId, path); + } + } + + [Event(ErrorFetchingModelContentEventId, Level = EventLevel.Error, Message = StandardStrings.ErrorFetchingModelContent)] + public void ErrorFetchingModelContent(string path) + { + if (IsEnabled(EventLevel.Error, EventKeywords.None)) + { + WriteEvent(ErrorFetchingModelContentEventId, path); + } + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverException.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverException.cs new file mode 100644 index 000000000000..ceb4217c2d21 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverException.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Globalization; + +namespace Azure.Iot.ModelsRepository +{ + /// + /// TODO: Paymaun: Exception comments. + /// + public class ResolverException : Exception + { + /// + /// TODO: Paymaun: Exception comments. + /// + /// + public ResolverException(string dtmi) : base(string.Format(CultureInfo.InvariantCulture, StandardStrings.GenericResolverError, dtmi)) + { + } + + /// + /// TODO: Paymaun: Exception comments. + /// + /// + /// + public ResolverException(string dtmi, string message) : + base($"{string.Format(CultureInfo.InvariantCulture, StandardStrings.GenericResolverError, dtmi)}{message}") + { + } + + /// + /// TODO: Paymaun: Exception comments. + /// + /// + /// + public ResolverException(string dtmi, Exception innerException) : + base(string.Format(CultureInfo.InvariantCulture, StandardStrings.GenericResolverError, dtmi), innerException) + { + } + + /// + /// TODO: Paymaun: Exception comments. + /// + /// + /// + /// + public ResolverException(string dtmi, string message, Exception innerException) : + base($"{string.Format(CultureInfo.InvariantCulture, StandardStrings.GenericResolverError, dtmi)}{message}", innerException) + { + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs new file mode 100644 index 000000000000..4906aab19b65 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Iot.ModelsRepository +{ + internal class StandardStrings + { + public const string GenericResolverError = "Unable to resolve \"{0}\". "; + public const string InvalidDtmiFormat = "Invalid DTMI format \"{0}\". "; + public const string ClientInitWithFetcher = "Client session {0} initialized with {1} content fetcher. "; + public const string ProcessingDtmi = "Processing DTMI \"{0}\". "; + public const string SkippingPreProcessedDtmi = "Already processed DTMI \"{0}\". Skipping. "; + public const string DiscoveredDependencies = "Discovered dependencies \"{0}\". "; + public const string FetchingModelContent = "Attempting to retrieve model content from \"{0}\". "; + public const string ErrorFetchingModelContent = "Model file \"{0}\" not found or not accessible in target repository. "; + public const string IncorrectDtmiCasing = + "Retrieved model has incorrect DTMI casing. Expected \"{0}\", parsed \"{1}\". "; + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/Azure.Iot.ModelsRepository.Tests.csproj b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/Azure.Iot.ModelsRepository.Tests.csproj new file mode 100644 index 000000000000..3142e95ed80e --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/Azure.Iot.ModelsRepository.Tests.csproj @@ -0,0 +1,35 @@ + + + + $(RequiredTargetFrameworks) + $(DefineConstants);TESTFRAMEWORK + true + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + + + + diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs new file mode 100644 index 000000000000..437c0231de9d --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NUnit.Framework; +using System; +using System.Runtime.InteropServices; + +namespace Azure.Iot.ModelsRepository.Tests +{ + public class ClientTests + { + [Test] + public void CtorOverloads() + { + string remoteUriStr = "https://dtmi.com"; + Uri remoteUri = new Uri(remoteUriStr); + + ResolverClientOptions options = new ResolverClientOptions(); + + Assert.AreEqual(new Uri(ResolverClient.DefaultRepository), new ResolverClient().RepositoryUri); + Assert.AreEqual($"{ResolverClient.DefaultRepository}/", new ResolverClient().RepositoryUri.AbsoluteUri); + Assert.AreEqual(new Uri(ResolverClient.DefaultRepository), new ResolverClient(options).RepositoryUri); + + Assert.AreEqual(remoteUri, new ResolverClient(remoteUri).RepositoryUri); + Assert.AreEqual(remoteUri, new ResolverClient(remoteUri, options).RepositoryUri); + Assert.AreEqual(remoteUri, new ResolverClient(remoteUri, null).RepositoryUri); + + Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr).RepositoryUri); + Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr, options).RepositoryUri); + Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr, null).RepositoryUri); + + string localUriStr = TestHelpers.TestLocalModelRepository; + Uri localUri = new Uri(localUriStr); + + Assert.AreEqual(localUri, new ResolverClient(localUri).RepositoryUri); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + localUriStr = localUriStr.Replace("\\", "/"); + } + + Assert.AreEqual(localUriStr, new ResolverClient(localUri).RepositoryUri.AbsolutePath); + } + + [TestCase("dtmi:com:example:Thermostat;1", true)] + [TestCase("dtmi:contoso:scope:entity;2", true)] + [TestCase("dtmi:com:example:Thermostat:1", false)] + [TestCase("dtmi:com:example::Thermostat;1", false)] + [TestCase("com:example:Thermostat;1", false)] + [TestCase("", false)] + [TestCase(null, false)] + public void ClientIsValidDtmi(string dtmi, bool expected) + { + Assert.AreEqual(expected, ResolverClient.IsValidDtmi(dtmi)); + } + + [Test] + public void ClientOptions() + { + DependencyResolutionOption defaultResolutionOption = DependencyResolutionOption.Enabled; + ResolverClientOptions customOptions = + new ResolverClientOptions(resolutionOption: DependencyResolutionOption.TryFromExpanded); + int maxRetries = 10; + customOptions.Retry.MaxRetries = maxRetries; + + string repositoryUriString = "https://localhost/myregistry/"; + Uri repositoryUri = new Uri(repositoryUriString); + + ResolverClient defaultClient = new ResolverClient(repositoryUri); + Assert.AreEqual(defaultResolutionOption, defaultClient.ClientOptions.DependencyResolution); + + ResolverClient customClient = new ResolverClient(repositoryUriString, customOptions); + Assert.AreEqual(DependencyResolutionOption.TryFromExpanded, customClient.ClientOptions.DependencyResolution); + Assert.AreEqual(maxRetries, customClient.ClientOptions.Retry.MaxRetries); + } + + [Test] + public void EvaluateEventSourceKPIs() + { + Type eventSourceType = typeof(ResolverEventSource); + + Assert.NotNull(eventSourceType); + Assert.AreEqual("Azure-Iot-ModelsRepository", ResolverEventSource.GetName(eventSourceType)); + Assert.AreEqual(Guid.Parse("7678f8d4-81db-5fd2-39fc-23552d86b171"), ResolverEventSource.GetGuid(eventSourceType)); + Assert.IsNotEmpty(ResolverEventSource.GenerateManifest(eventSourceType, "assemblyPathToIncludeInManifest")); + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/DtmiConversionTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/DtmiConversionTests.cs new file mode 100644 index 000000000000..a8919a1ddcaf --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/DtmiConversionTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NUnit.Framework; +using System; +using System.Runtime.InteropServices; + +namespace Azure.Iot.ModelsRepository.Tests +{ + public class DtmiConversionTests + { + [TestCase("dtmi:com:Example:Model;1", "dtmi/com/example/model-1.json")] + [TestCase("dtmi:com:example:Model;1", "dtmi/com/example/model-1.json")] + [TestCase("dtmi:com:example:Model:1", null)] + [TestCase("", null)] + [TestCase(null, null)] + public void DtmiToPath(string dtmi, string expectedPath) + { + Assert.AreEqual(expectedPath, DtmiConventions.DtmiToPath(dtmi)); + } + + [TestCase("dtmi:com:example:Thermostat;1", "dtmi/com/example/thermostat-1.json", "https://localhost/repository")] + [TestCase("dtmi:com:example:Thermostat;1", "dtmi/com/example/thermostat-1.json", @"C:\fakeRegistry")] + [TestCase("dtmi:com:example:Thermostat;1", "dtmi/com/example/thermostat-1.json", "/me/fakeRegistry")] + [TestCase("dtmi:com:example:Thermostat:1", null, "https://localhost/repository")] + [TestCase("dtmi:com:example:Thermostat:1", null, "/me/fakeRegistry")] + public void DtmiToQualifiedPath(string dtmi, string expectedPath, string repository) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + repository = repository.Replace("\\", "/"); + } + + if (string.IsNullOrEmpty(expectedPath)) + { + ArgumentException re = Assert.Throws(() => DtmiConventions.DtmiToQualifiedPath(dtmi, repository)); + Assert.AreEqual(re.Message, string.Format(StandardStrings.InvalidDtmiFormat, dtmi)); + return; + } + + string modelPath = DtmiConventions.DtmiToQualifiedPath(dtmi, repository); + Assert.AreEqual($"{repository}/{expectedPath}", modelPath); + + string expandedModelPath = DtmiConventions.DtmiToQualifiedPath(dtmi, repository, true); + Assert.AreEqual($"{repository}/{expectedPath.Replace(".json", ".expanded.json")}", expandedModelPath); + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs new file mode 100644 index 000000000000..1d16d26e7781 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Azure.Iot.ModelsRepository.Tests +{ + public class ModelQueryTests + { + private readonly string _modelTemplate = @"{{ + {0} + ""@type"": ""Interface"", + ""displayName"": ""Phone"", + {1} + {2} + ""@context"": ""dtmi:dtdl:context;2"" + }}"; + + [TestCase("\"@id\": \"dtmi:com:example:thermostat;1\",", "dtmi:com:example:thermostat;1")] + [TestCase("\"@id\": \"\",", "")] + [TestCase("", "")] + public void GetId(string formatId, string expectedId) + { + string modelContent = string.Format(_modelTemplate, formatId, "", ""); + ModelQuery query = new ModelQuery(modelContent); + Assert.AreEqual(query.GetId(), expectedId); + } + + [TestCase( + @" + ""contents"": + [{ + ""@type"": ""Property"", + ""name"": ""capacity"", + ""schema"": ""integer"" + }, + { + ""@type"": ""Component"", + ""name"": ""frontCamera"", + ""schema"": ""dtmi:com:example:Camera;3"" + }, + { + ""@type"": ""Component"", + ""name"": ""backCamera"", + ""schema"": ""dtmi:com:example:Camera;3"" + }, + { + ""@type"": ""Component"", + ""name"": ""deviceInfo"", + ""schema"": ""dtmi:azure:DeviceManagement:DeviceInformation;1"" + }],", + "dtmi:com:example:Camera;3,dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1")] + [TestCase( + @" + ""contents"": + [{ + ""@type"": ""Property"", + ""name"": ""capacity"", + ""schema"": ""integer"" + }],", "")] + [TestCase(@"""contents"":[],", "")] + [TestCase("", "")] + public void GetComponentSchema(string contents, string expected) + { + string[] expectedDtmis = expected.Split(new[] { "," }, System.StringSplitOptions.RemoveEmptyEntries); + string modelContent = string.Format(_modelTemplate, "", "", contents); + ModelQuery query = new ModelQuery(modelContent); + IList componentSchemas = query.GetComponentSchemas(); + Assert.AreEqual(componentSchemas.Count, expectedDtmis.Length); + + foreach (string schema in componentSchemas) + { + Assert.Contains(schema, expectedDtmis); + } + } + + [TestCase( + "\"extends\": [\"dtmi:com:example:Camera;3\",\"dtmi:azure:DeviceManagement:DeviceInformation;1\"],", + "dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1")] + [TestCase("\"extends\": [],", "")] + [TestCase("\"extends\": \"dtmi:com:example:Camera;3\",", "dtmi:com:example:Camera;3")] + [TestCase("", "")] + public void GetExtends(string extends, string expected) + { + string[] expectedDtmis = expected.Split(new[] { "," }, System.StringSplitOptions.RemoveEmptyEntries); + string modelContent = string.Format(_modelTemplate, "", extends, ""); + ModelQuery query = new ModelQuery(modelContent); + IList extendsDtmis = query.GetExtends(); + Assert.AreEqual(extendsDtmis.Count, expectedDtmis.Length); + + foreach (string dtmi in extendsDtmis) + { + Assert.Contains(dtmi, expectedDtmis); + } + } + + [TestCase( + "\"@id\": \"dtmi:com:example:thermostat;1\",", + "\"extends\": [\"dtmi:com:example:Camera;3\",\"dtmi:azure:DeviceManagement:DeviceInformation;1\"],", + @"""contents"": + [{ + ""@type"": ""Property"", + ""name"": ""capacity"", + ""schema"": ""integer"" + }, + { + ""@type"": ""Component"", + ""name"": ""frontCamera"", + ""schema"": ""dtmi:com:example:Camera;3"" + }, + { + ""@type"": ""Component"", + ""name"": ""backCamera"", + ""schema"": ""dtmi:com:example:Camera;3"" + }],", + "dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1" + ), + TestCase( + "\"@id\": \"dtmi:example:Interface1;1\",", + @"""extends"": [""dtmi:example:Interface2;1"", { + ""@id"": ""dtmi:example:Interface3;1"", + ""@type"": ""Interface"", + ""contents"": [{ + ""@type"": ""Component"", + ""name"": ""comp1"", + ""schema"": [""dtmi:example:Interface4;1""] + }, + { + ""@type"": ""Component"", + ""name"": ""comp2"", + ""schema"": { + ""@id"": ""dtmi:example:Interface5;1"", + ""@type"": ""Interface"", + ""extends"": ""dtmi:example:Interface6;1"" + } + } + ] + }],", + "", + "dtmi:example:Interface2;1,dtmi:example:Interface4;1,dtmi:example:Interface6;1" + ) + ] + public void GetModelDependencies(string id, string extends, string contents, string expected) + { + string[] expectedDtmis = expected.Split(new[] { "," }, System.StringSplitOptions.RemoveEmptyEntries); + string modelContent = string.Format(_modelTemplate, id, extends, contents); + ModelMetadata metadata = new ModelQuery(modelContent).GetMetadata(); + + IList dependencies = metadata.Dependencies; + + Assert.AreEqual(dependencies.Count, expectedDtmis.Length); + + foreach (string dtmi in dependencies) + { + Assert.Contains(dtmi, expectedDtmis); + } + } + + [Test] + public async Task ListToDictAsync() + { + string testRepoPath = TestHelpers.TestLocalModelRepository; + string expandedContent = File.ReadAllText( + $"{testRepoPath}/dtmi/com/example/temperaturecontroller-1.expanded.json", Encoding.UTF8); + ModelQuery query = new ModelQuery(expandedContent); + Dictionary transformResult = await query.ListToDictAsync(); + + // Assert KPI's for TemperatureController;1. + // Ensure transform of expanded content to dictionary is what we'd expect. + string[] expectedIds = new string[] { + "dtmi:azure:DeviceManagement:DeviceInformation;1", + "dtmi:com:example:Thermostat;1", + "dtmi:com:example:TemperatureController;1" }; + + Assert.True(transformResult.Keys.Count == expectedIds.Length); + + foreach (string id in expectedIds) + { + Assert.True(transformResult.ContainsKey(id)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(transformResult[id]).Equals(id, System.StringComparison.Ordinal)); + } + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ResolveIntegrationTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ResolveIntegrationTests.cs new file mode 100644 index 000000000000..4eb95f2bd338 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ResolveIntegrationTests.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Azure.Iot.ModelsRepository.Tests +{ + public class ResolveIntegrationTests + { + [TestCase("dtmi:com:example:thermostat;1", TestHelpers.ClientType.Local)] + [TestCase("dtmi:com:example:thermostat;1", TestHelpers.ClientType.Remote)] + public void ResolveWithWrongCasingThrowsException(string dtmi, TestHelpers.ClientType clientType) + { + ResolverClient client = TestHelpers.GetTestClient(clientType); + string expectedExMsg = + string.Format(StandardStrings.GenericResolverError, "dtmi:com:example:thermostat;1") + + string.Format(StandardStrings.IncorrectDtmiCasing, "dtmi:com:example:thermostat;1", "dtmi:com:example:Thermostat;1"); + + ResolverException re = Assert.ThrowsAsync(async () => await client.ResolveAsync(dtmi)); + Assert.AreEqual(re.Message, expectedExMsg); + } + + [TestCase("dtmi:com:example:Thermostat:1")] + [TestCase("dtmi:com:example::Thermostat;1")] + [TestCase("com:example:Thermostat;1")] + public void ResolveInvalidDtmiFormatThrowsException(string dtmi) + { + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local); + string expectedExMsg = $"{string.Format(StandardStrings.GenericResolverError, dtmi)}{string.Format(StandardStrings.InvalidDtmiFormat, dtmi)}"; + ResolverException re = Assert.ThrowsAsync(async () => await client.ResolveAsync(dtmi)); + Assert.AreEqual(re.Message, expectedExMsg); + } + + [TestCase("dtmi:com:example:thermojax;999", TestHelpers.ClientType.Local)] + [TestCase("dtmi:com:example:thermojax;999", TestHelpers.ClientType.Remote)] + public void ResolveNoneExistentDtmiFileThrowsException(string dtmi, TestHelpers.ClientType clientType) + { + ResolverClient client = TestHelpers.GetTestClient(clientType); + ResolverException re = Assert.ThrowsAsync(async () => await client.ResolveAsync(dtmi)); + Assert.True(re.Message.StartsWith($"Unable to resolve \"{dtmi}\"")); + } + + [TestCase("dtmi:com:example:invalidmodel;1", "dtmi:azure:fakeDeviceManagement:FakeDeviceInformation;2")] + public void ResolveInvalidDtmiDepsThrowsException(string dtmi, string invalidDep) + { + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local); + ResolverException re = Assert.ThrowsAsync(async () => await client.ResolveAsync(dtmi)); + Assert.True(re.Message.StartsWith($"Unable to resolve \"{invalidDep}\"")); + } + + [TestCase("dtmi:com:example:Thermostat;1", TestHelpers.ClientType.Local)] + [TestCase("dtmi:com:example:Thermostat;1", TestHelpers.ClientType.Remote)] + public async Task ResolveSingleModelNoDeps(string dtmi, TestHelpers.ClientType clientType) + { + ResolverClient client = TestHelpers.GetTestClient(clientType); + var result = await client.ResolveAsync(dtmi); + Assert.True(result.Keys.Count == 1); + Assert.True(result.ContainsKey(dtmi)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi]) == dtmi); + } + + [TestCase("dtmi:com:example:Thermostat;1", "dtmi:azure:DeviceManagement:DeviceInformation;1", TestHelpers.ClientType.Local)] + [TestCase("dtmi:com:example:Thermostat;1", "dtmi:azure:DeviceManagement:DeviceInformation;1", TestHelpers.ClientType.Remote)] + public async Task ResolveMultipleModelsNoDeps(string dtmi1, string dtmi2, TestHelpers.ClientType clientType) + { + ResolverClient client = TestHelpers.GetTestClient(clientType); + var result = await client.ResolveAsync(new string[] { dtmi1, dtmi2 }); + Assert.True(result.Keys.Count == 2); + Assert.True(result.ContainsKey(dtmi1)); + Assert.True(result.ContainsKey(dtmi2)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi1]) == dtmi1); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi2]) == dtmi2); + } + + [TestCase("dtmi:com:example:TemperatureController;1", + "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1", TestHelpers.ClientType.Local)] + [TestCase("dtmi:com:example:TemperatureController;1", + "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1", TestHelpers.ClientType.Remote)] + public async Task ResolveSingleModelWithDeps(string dtmi, string expectedDeps, TestHelpers.ClientType clientType) + { + ResolverClient client = TestHelpers.GetTestClient(clientType); + var result = await client.ResolveAsync(dtmi); + var expectedDtmis = $"{dtmi},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + Assert.True(result.Keys.Count == expectedDtmis.Length); + foreach (var id in expectedDtmis) + { + Assert.True(result.ContainsKey(id)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id); + } + + // TODO: Evaluate using Azure.Core.TestFramework in future iteration. + + /* + // Verifying log entries for a Process(...) run + _logger.ValidateLog($"{StandardStrings.ClientInitWithFetcher(localClient.RepositoryUri.Scheme)}", LogLevel.Trace, Times.Once()); + + _logger.ValidateLog($"{StandardStrings.ProcessingDtmi("dtmi:com:example:TemperatureController;1")}", LogLevel.Trace, Times.Once()); + _logger.ValidateLog($"{StandardStrings.FetchingContent(DtmiConventions.DtmiToQualifiedPath(expectedDtmis[0], localClient.RepositoryUri.AbsolutePath))}", LogLevel.Trace, Times.Once()); + + _logger.ValidateLog($"{StandardStrings.DiscoveredDependencies(new List() { "dtmi:com:example:Thermostat;1", "dtmi:azure:DeviceManagement:DeviceInformation;1" })}", LogLevel.Trace, Times.Once()); + + _logger.ValidateLog($"{StandardStrings.ProcessingDtmi("dtmi:com:example:Thermostat;1")}", LogLevel.Trace, Times.Once()); + _logger.ValidateLog($"{StandardStrings.FetchingContent(DtmiConventions.DtmiToQualifiedPath(expectedDtmis[1], localClient.RepositoryUri.AbsolutePath))}", LogLevel.Trace, Times.Once()); + + _logger.ValidateLog($"{StandardStrings.ProcessingDtmi("dtmi:azure:DeviceManagement:DeviceInformation;1")}", LogLevel.Trace, Times.Once()); + _logger.ValidateLog($"{StandardStrings.FetchingContent(DtmiConventions.DtmiToQualifiedPath(expectedDtmis[2], localClient.RepositoryUri.AbsolutePath))}", LogLevel.Trace, Times.Once()); + */ + } + + [TestCase("dtmi:com:example:Phone;2", + "dtmi:com:example:TemperatureController;1", + "dtmi:com:example:Thermostat;1," + + "dtmi:azure:DeviceManagement:DeviceInformation;1," + + "dtmi:azure:DeviceManagement:DeviceInformation;2," + + "dtmi:com:example:Camera;3")] + public async Task ResolveMultipleModelsWithDeps(string dtmi1, string dtmi2, string expectedDeps) + { + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local); + var result = await client.ResolveAsync(new[] { dtmi1, dtmi2 }); + var expectedDtmis = $"{dtmi1},{dtmi2},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + Assert.True(result.Keys.Count == expectedDtmis.Length); + foreach (var id in expectedDtmis) + { + Assert.True(result.ContainsKey(id)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id); + } + } + + [TestCase("dtmi:com:example:TemperatureController;1", + "dtmi:com:example:ConferenceRoom;1", // Model uses extends + "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1,dtmi:com:example:Room;1")] + public async Task ResolveMultipleModelsWithDepsFromExtends(string dtmi1, string dtmi2, string expectedDeps) + { + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local); + var result = await client.ResolveAsync(new[] { dtmi1, dtmi2 }); + var expectedDtmis = $"{dtmi1},{dtmi2},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + Assert.True(result.Keys.Count == expectedDtmis.Length); + foreach (var id in expectedDtmis) + { + Assert.True(result.ContainsKey(id)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id); + } + } + + [TestCase("dtmi:com:example:TemperatureController;1", + "dtmi:com:example:ColdStorage;1", // Model uses extends[] + "dtmi:com:example:Thermostat;1," + + "dtmi:azure:DeviceManagement:DeviceInformation;1," + + "dtmi:com:example:Room;1," + + "dtmi:com:example:Freezer;1")] + public async Task ResolveMultipleModelsWithDepsFromExtendsVariant(string dtmi1, string dtmi2, string expectedDeps) + { + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local); + var result = await client.ResolveAsync(new[] { dtmi1, dtmi2 }); + var expectedDtmis = $"{dtmi1},{dtmi2},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + Assert.True(result.Keys.Count == expectedDtmis.Length); + foreach (var id in expectedDtmis) + { + Assert.True(result.ContainsKey(id)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id); + } + } + + [TestCase("dtmi:com:example:base;1")] + public async Task ResolveSingleModelWithDepsFromExtendsInline(string dtmi) + { + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local); + var result = await client.ResolveAsync(dtmi); + + Assert.True(result.Keys.Count == 1); + Assert.True(result.ContainsKey(dtmi)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi]) == dtmi); + } + + [TestCase("dtmi:com:example:base;2", + "dtmi:com:example:Freezer;1," + + "dtmi:com:example:Thermostat;1")] + public async Task ResolveSingleModelWithDepsFromExtendsInlineVariant(string dtmi, string expected) + { + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local); + var result = await client.ResolveAsync(dtmi); + var expectedDtmis = $"{dtmi},{expected}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + Assert.True(result.Keys.Count == expectedDtmis.Length); + foreach (var id in expectedDtmis) + { + Assert.True(result.ContainsKey(id)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id); + } + } + + [TestCase("dtmi:azure:DeviceManagement:DeviceInformation;1", "dtmi:azure:DeviceManagement:DeviceInformation;1")] + public async Task ResolveEnsuresNoDupes(string dtmiDupe1, string dtmiDupe2) + { + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local); + var result = await client.ResolveAsync(new[] { dtmiDupe1, dtmiDupe2 }); + Assert.True(result.Keys.Count == 1); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmiDupe1]) == dtmiDupe1); + } + + [TestCase("dtmi:com:example:Thermostat;1", TestHelpers.ClientType.Local)] + [TestCase("dtmi:com:example:Thermostat;1", TestHelpers.ClientType.Remote)] + public async Task ResolveSingleModelWithDepsDisableDependencyResolution(string dtmi, TestHelpers.ClientType clientType) + { + ResolverClientOptions options = new ResolverClientOptions(resolutionOption: DependencyResolutionOption.Disabled); + ResolverClient client = TestHelpers.GetTestClient(clientType, options); + + IDictionary result = await client.ResolveAsync(dtmi); + + Assert.True(result.Keys.Count == 1); + Assert.True(result.ContainsKey(dtmi)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi]) == dtmi); + } + + [TestCase( + "dtmi:com:example:TemperatureController;1", // .expanded.json available locally. + "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1", + TestHelpers.ClientType.Local)] + [TestCase( + "dtmi:com:example:TemperatureController;1", // .expanded.json available remotely. + "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1", + TestHelpers.ClientType.Remote)] + public async Task ResolveSingleModelTryFromExpanded(string dtmi, string expectedDeps, TestHelpers.ClientType clientType) + { + var expectedDtmis = $"{dtmi},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + ResolverClientOptions options = new ResolverClientOptions(resolutionOption: DependencyResolutionOption.TryFromExpanded); + ResolverClient client = TestHelpers.GetTestClient(clientType, options); + + var result = await client.ResolveAsync(dtmi); + + Assert.True(result.Keys.Count == expectedDtmis.Length); + foreach (var id in expectedDtmis) + { + Assert.True(result.ContainsKey(id)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id); + } + + // TODO: Evaluate using Azure.Core.TestFramework in future iteration. + + /* + string expectedPath = DtmiConventions.DtmiToQualifiedPath( + dtmi, + repoType == "local" ? client.RepositoryUri.AbsolutePath : client.RepositoryUri.AbsoluteUri, + fromExpanded: true); + _logger.ValidateLog(StandardStrings.FetchingContent(expectedPath), LogLevel.Trace, Times.Once()); + */ + } + + [TestCase("dtmi:com:example:TemperatureController;1," + // Expanded available. + "dtmi:com:example:Thermostat;1," + + "dtmi:azure:DeviceManagement:DeviceInformation;1", + "dtmi:com:example:ColdStorage;1," + // Model uses extends[], No Expanded available. + "dtmi:com:example:Room;1," + + "dtmi:com:example:Freezer;1")] + public async Task ResolveMultipleModelsTryFromExpandedPartial(string dtmisExpanded, string dtmisNonExpanded) + { + string[] expandedDtmis = dtmisExpanded.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + string[] nonExpandedDtmis = dtmisNonExpanded.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + string[] totalDtmis = expandedDtmis.Concat(nonExpandedDtmis).ToArray(); + + ResolverClientOptions options = new ResolverClientOptions(resolutionOption: DependencyResolutionOption.TryFromExpanded); + ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local, options); + + // Multi-resolve dtmi:com:example:TemperatureController;1 + dtmi:com:example:ColdStorage;1 + IDictionary result = await client.ResolveAsync(new[] { expandedDtmis[0], nonExpandedDtmis[0] }); + + Assert.True(result.Keys.Count == totalDtmis.Length); + foreach (string id in totalDtmis) + { + Assert.True(result.ContainsKey(id)); + Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id); + } + + // TODO: Evaluate using Azure.Core.TestFramework in future iteration. + + /* + string expandedModelPath = DtmiConventions.DtmiToQualifiedPath(expandedDtmis[0], localClient.RepositoryUri.AbsolutePath, fromExpanded: true); + _logger.ValidateLog(StandardStrings.FetchingContent(expandedModelPath), LogLevel.Trace, Times.Once()); + + foreach (string dtmi in nonExpandedDtmis) + { + string expectedPath = DtmiConventions.DtmiToQualifiedPath(dtmi, localClient.RepositoryUri.AbsolutePath, fromExpanded: true); + _logger.ValidateLog(StandardStrings.FetchingContent(expectedPath), LogLevel.Trace, Times.Once()); + _logger.ValidateLog(StandardStrings.ErrorAccessLocalRepositoryModel(expectedPath), LogLevel.Warning, Times.Once()); + } + */ + } + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs new file mode 100644 index 000000000000..a1e3e3020fb7 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NUnit.Framework; +using System; +using System.IO; +using System.Reflection; +using System.Text.Json; + +namespace Azure.Iot.ModelsRepository.Tests +{ + public class TestHelpers + { + private static readonly string s_fallbackTestRemoteRepo = "https://devicemodels.azure.com/"; + public enum ClientType + { + Local, + Remote + } + + public static string ParseRootDtmiFromJson(string json) + { + var options = new JsonDocumentOptions + { + AllowTrailingCommas = true + }; + + string dtmi = string.Empty; + using (JsonDocument document = JsonDocument.Parse(json, options)) + { + dtmi = document.RootElement.GetProperty("@id").GetString(); + } + return dtmi; + } + + public static ResolverClient GetTestClient(ClientType clientType, ResolverClientOptions clientOptions = null) + { + if (clientType == ClientType.Local) + return new ResolverClient(TestLocalModelRepository, clientOptions); + if (clientType == ClientType.Remote) + return new ResolverClient(TestRemoteModelRepository, clientOptions); + + throw new ArgumentException(); + } + + public static string TestDirectoryPath => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + public static string TestLocalModelRepository => Path.Combine(TestDirectoryPath, "TestModelRepo"); + + public static string TestRemoteModelRepository => Environment.GetEnvironmentVariable("PNP_TEST_REMOTE_REPO") ?? s_fallbackTestRemoteRepo; + } +} diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-1.json new file mode 100644 index 000000000000..8a37e6d2c2c3 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-1.json @@ -0,0 +1,64 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "@type": "Interface", + "displayName": "Device Information", + "contents": [ + { + "@type": "Property", + "name": "manufacturer", + "displayName": "Manufacturer", + "schema": "string", + "description": "Company name of the device manufacturer. This could be the same as the name of the original equipment manufacturer (OEM). Ex. Contoso." + }, + { + "@type": "Property", + "name": "model", + "displayName": "Device model", + "schema": "string", + "description": "Device model name or ID. Ex. Surface Book 2." + }, + { + "@type": "Property", + "name": "swVersion", + "displayName": "Software version", + "schema": "string", + "description": "Version of the software on your device. This could be the version of your firmware. Ex. 1.3.45" + }, + { + "@type": "Property", + "name": "osName", + "displayName": "Operating system name", + "schema": "string", + "description": "Name of the operating system on the device. Ex. Windows 10 IoT Core." + }, + { + "@type": "Property", + "name": "processorArchitecture", + "displayName": "Processor architecture", + "schema": "string", + "description": "Architecture of the processor on the device. Ex. x64 or ARM." + }, + { + "@type": "Property", + "name": "processorManufacturer", + "displayName": "Processor manufacturer", + "schema": "string", + "description": "Name of the manufacturer of the processor on the device. Ex. Intel." + }, + { + "@type": "Property", + "name": "totalStorage", + "displayName": "Total storage", + "schema": "double", + "description": "Total available storage on the device in kilobytes. Ex. 2048000 kilobytes." + }, + { + "@type": "Property", + "name": "totalMemory", + "displayName": "Total memory", + "schema": "double", + "description": "Total available memory on the device in kilobytes. Ex. 256000 kilobytes." + } + ] +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-2.json new file mode 100644 index 000000000000..d35b8a3e3a1d --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-2.json @@ -0,0 +1,16 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:azure:DeviceManagement:DeviceInformation;2", + "@type": "Interface", + "extends": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "displayName": "Device Information", + "contents": [ + { + "@type": "Property", + "name": "osKernelVersion", + "displayName": "OS Kernel Version", + "schema": "string", + "description": "OS Kernel Version. Ex. Linux 4.15.0-54-generic x86_64." + } + ] +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-1.json new file mode 100644 index 000000000000..85424e8a229e --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-1.json @@ -0,0 +1,56 @@ +{ + "@id": "dtmi:com:example:base;1", + "@type": "Interface", + "contents": [ + { + "@type": "Property", + "name": "baseSerialNumber", + "schema": "string" + } + ], + "displayName": { + "en": "mybaseProp" + }, + "extends": [ + { + "@id": "dtmi:com:example:basic;1", + "@type": "Interface", + "contents": [ + { + "@type": "Property", + "name": "serialNumber", + "schema": "string", + "writable": false + }, + { + "@type": [ + "Telemetry", + "Temperature" + ], + "displayName": { + "en": "temperature" + }, + "name": "temperature", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": "Property", + "displayName": { + "en": "targetTemperature" + }, + "name": "targetTemperature", + "schema": "double", + "writable": true + } + ], + "displayName": { + "en": "Basic" + } + } + ], + "@context": [ + "dtmi:iotcentral:context;2", + "dtmi:dtdl:context;2" + ] +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-2.json new file mode 100644 index 000000000000..29accafcc252 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-2.json @@ -0,0 +1,57 @@ +{ + "@id": "dtmi:com:example:base;2", + "@type": "Interface", + "contents": [ + { + "@type": "Property", + "name": "baseSerialNumber", + "schema": "string" + } + ], + "displayName": { + "en": "mybaseProp" + }, + "extends": [ + { + "@id": "dtmi:com:example:basic;1", + "@type": "Interface", + "contents": [ + { + "@type": "Property", + "name": "serialNumber", + "schema": "string", + "writable": false + }, + { + "@type": [ + "Telemetry", + "Temperature" + ], + "displayName": { + "en": "temperature" + }, + "name": "temperature", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": "Property", + "displayName": { + "en": "targetTemperature" + }, + "name": "targetTemperature", + "schema": "double", + "writable": true + } + ], + "displayName": { + "en": "Basic" + } + }, + "dtmi:com:example:Freezer;1" + ], + "@context": [ + "dtmi:iotcentral:context;2", + "dtmi:dtdl:context;2" + ] +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/building-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/building-1.json new file mode 100644 index 000000000000..c8b6bc6f7375 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/building-1.json @@ -0,0 +1,19 @@ +{ + "@id": "dtmi:com:example:Building;1", + "@type": "Interface", + "displayName": "Building", + "contents": [ + { + "@type": "Property", + "name": "name", + "schema": "string", + "writable": true + }, + { + "@type": "Relationship", + "name": "contains", + "target": "dtmi:com:example:Room;1" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/camera-3.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/camera-3.json new file mode 100644 index 000000000000..f912746c0040 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/camera-3.json @@ -0,0 +1,13 @@ +{ + "@id": "dtmi:com:example:Camera;3", + "@type": "Interface", + "displayName": "Phone", + "contents": [ + { + "@type": "Component", + "name": "deviceInfo", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/coldstorage-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/coldstorage-1.json new file mode 100644 index 000000000000..a3b8466118a9 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/coldstorage-1.json @@ -0,0 +1,13 @@ +{ + "@id": "dtmi:com:example:ColdStorage;1", + "@type": "Interface", + "extends": ["dtmi:com:example:Room;1", "dtmi:com:example:Freezer;1"], + "contents": [ + { + "@type": "Property", + "name": "capacity", + "schema": "integer" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/conferenceroom-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/conferenceroom-1.json new file mode 100644 index 000000000000..2e756ee73b6e --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/conferenceroom-1.json @@ -0,0 +1,13 @@ +{ + "@id": "dtmi:com:example:ConferenceRoom;1", + "@type": "Interface", + "extends": "dtmi:com:example:Room;1", + "contents": [ + { + "@type": "Property", + "name": "capacity", + "schema": "integer" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/freezer-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/freezer-1.json new file mode 100644 index 000000000000..6006b6673299 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/freezer-1.json @@ -0,0 +1,12 @@ +{ + "@id": "dtmi:com:example:Freezer;1", + "@type": "Interface", + "contents": [ + { + "@type": "Component", + "name": "deviceInfo", + "schema": "dtmi:com:example:Thermostat;1" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/incompleteexpanded-1.expanded.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/incompleteexpanded-1.expanded.json new file mode 100644 index 000000000000..e91626b56e44 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/incompleteexpanded-1.expanded.json @@ -0,0 +1,151 @@ +[ + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:com:example:IncompleteExpanded;1", + "@type": "Interface", + "displayName": "Incomplete Expanded Temperature Controller", + "description": "Device with two thermostats and remote reboot.", + "contents": [ + { + "@type": [ + "Telemetry", + "DataSize" + ], + "name": "workingSet", + "displayName": "Working Set", + "description": "Current working set of the device memory in KiB.", + "schema": "double", + "unit": "kibibyte" + }, + { + "@type": "Property", + "name": "serialNumber", + "displayName": "Serial Number", + "description": "Serial number of the device.", + "schema": "string" + }, + { + "@type": "Command", + "name": "reboot", + "displayName": "Reboot", + "description": "Reboots the device after waiting the number of seconds specified.", + "request": { + "name": "delay", + "displayName": "Delay", + "description": "Number of seconds to wait before rebooting the device.", + "schema": "integer" + } + }, + { + "@type": "Component", + "schema": "dtmi:com:example:Thermostat;1", + "name": "thermostat1", + "displayName": "Thermostat One", + "description": "Thermostat One of Two." + }, + { + "@type": "Component", + "schema": "dtmi:com:example:Thermostat;1", + "name": "thermostat2", + "displayName": "Thermostat Two", + "description": "Thermostat Two of Two." + }, + { + "@type": "Component", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "name": "deviceInformation", + "displayName": "Device Information interface", + "description": "Optional interface with basic device hardware information." + } + ] + }, + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:com:example:Thermostat;1", + "@type": "Interface", + "displayName": "Thermostat", + "description": "Reports current temperature and provides desired temperature control.", + "contents": [ + { + "@type": [ + "Telemetry", + "Temperature" + ], + "name": "temperature", + "displayName": "Temperature", + "description": "Temperature in degrees Celsius.", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": [ + "Property", + "Temperature" + ], + "name": "targetTemperature", + "schema": "double", + "displayName": "Target Temperature", + "description": "Allows to remotely specify the desired target temperature.", + "unit": "degreeCelsius", + "writable": true + }, + { + "@type": [ + "Property", + "Temperature" + ], + "name": "maxTempSinceLastReboot", + "schema": "double", + "unit": "degreeCelsius", + "displayName": "Max temperature since last reboot.", + "description": "Returns the max temperature since last device reboot." + }, + { + "@type": "Command", + "name": "getMaxMinReport", + "displayName": "Get Max-Min report.", + "description": "This command returns the max, min and average temperature from the specified time to the current time.", + "request": { + "name": "since", + "displayName": "Since", + "description": "Period to return the max-min report.", + "schema": "dateTime" + }, + "response": { + "name": "tempReport", + "displayName": "Temperature Report", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "maxTemp", + "displayName": "Max temperature", + "schema": "double" + }, + { + "name": "minTemp", + "displayName": "Min temperature", + "schema": "double" + }, + { + "name": "avgTemp", + "displayName": "Average Temperature", + "schema": "double" + }, + { + "name": "startTime", + "displayName": "Start Time", + "schema": "dateTime" + }, + { + "name": "endTime", + "displayName": "End Time", + "schema": "dateTime" + } + ] + } + } + } + ] + } +] \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-1.json new file mode 100644 index 000000000000..4f18d7b17658 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-1.json @@ -0,0 +1,13 @@ +{ + "@id": "dtmi:com:example:invalidmodel;1", + "@type": "Interface", + "displayName": "Phone", + "contents": [ + { + "@type": "Component", + "name": "deviceInfo", + "schema": "dtmi:azure:fakeDeviceManagement:FakeDeviceInformation;2" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-2.json new file mode 100644 index 000000000000..61443734cd90 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-2.json @@ -0,0 +1,23 @@ +{ + "@id": "dtmi:com:example:Phone;2", + "@type": "Interfacez", + "displayName": "Phone", + "contentsz": [ + { + "@type": "Component", + "name": "frontCamera", + "schema": "dtmi:com:example:Camera;3" + }, + { + "@type": "Component", + "name": "backCamera", + "schema": "dtmi:com:example:Camera;3" + }, + { + "@type": "Component", + "name": "deviceInfo", + "schema": "dtmi:azure:deviceManagement:DeviceInformation;2" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/phone-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/phone-2.json new file mode 100644 index 000000000000..26c7efbdedc0 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/phone-2.json @@ -0,0 +1,23 @@ +{ + "@id": "dtmi:com:example:Phone;2", + "@type": "Interface", + "displayName": "Phone", + "contents": [ + { + "@type": "Component", + "name": "frontCamera", + "schema": "dtmi:com:example:Camera;3" + }, + { + "@type": "Component", + "name": "backCamera", + "schema": "dtmi:com:example:Camera;3" + }, + { + "@type": "Component", + "name": "deviceInfo", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;2" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/room-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/room-1.json new file mode 100644 index 000000000000..1a07edec4d98 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/room-1.json @@ -0,0 +1,12 @@ +{ + "@id": "dtmi:com:example:Room;1", + "@type": "Interface", + "contents": [ + { + "@type": "Property", + "name": "occupied", + "schema": "boolean" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.expanded.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.expanded.json new file mode 100644 index 000000000000..14e8e294189e --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.expanded.json @@ -0,0 +1,215 @@ +[ + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:com:example:TemperatureController;1", + "@type": "Interface", + "displayName": "Temperature Controller", + "description": "Device with two thermostats and remote reboot.", + "contents": [ + { + "@type": [ + "Telemetry", + "DataSize" + ], + "name": "workingSet", + "displayName": "Working Set", + "description": "Current working set of the device memory in KiB.", + "schema": "double", + "unit": "kibibyte" + }, + { + "@type": "Property", + "name": "serialNumber", + "displayName": "Serial Number", + "description": "Serial number of the device.", + "schema": "string" + }, + { + "@type": "Command", + "name": "reboot", + "displayName": "Reboot", + "description": "Reboots the device after waiting the number of seconds specified.", + "request": { + "name": "delay", + "displayName": "Delay", + "description": "Number of seconds to wait before rebooting the device.", + "schema": "integer" + } + }, + { + "@type": "Component", + "schema": "dtmi:com:example:Thermostat;1", + "name": "thermostat1", + "displayName": "Thermostat One", + "description": "Thermostat One of Two." + }, + { + "@type": "Component", + "schema": "dtmi:com:example:Thermostat;1", + "name": "thermostat2", + "displayName": "Thermostat Two", + "description": "Thermostat Two of Two." + }, + { + "@type": "Component", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "name": "deviceInformation", + "displayName": "Device Information interface", + "description": "Optional interface with basic device hardware information." + } + ] + }, + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:com:example:Thermostat;1", + "@type": "Interface", + "displayName": "Thermostat", + "description": "Reports current temperature and provides desired temperature control.", + "contents": [ + { + "@type": [ + "Telemetry", + "Temperature" + ], + "name": "temperature", + "displayName": "Temperature", + "description": "Temperature in degrees Celsius.", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": [ + "Property", + "Temperature" + ], + "name": "targetTemperature", + "schema": "double", + "displayName": "Target Temperature", + "description": "Allows to remotely specify the desired target temperature.", + "unit": "degreeCelsius", + "writable": true + }, + { + "@type": [ + "Property", + "Temperature" + ], + "name": "maxTempSinceLastReboot", + "schema": "double", + "unit": "degreeCelsius", + "displayName": "Max temperature since last reboot.", + "description": "Returns the max temperature since last device reboot." + }, + { + "@type": "Command", + "name": "getMaxMinReport", + "displayName": "Get Max-Min report.", + "description": "This command returns the max, min and average temperature from the specified time to the current time.", + "request": { + "name": "since", + "displayName": "Since", + "description": "Period to return the max-min report.", + "schema": "dateTime" + }, + "response": { + "name": "tempReport", + "displayName": "Temperature Report", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "maxTemp", + "displayName": "Max temperature", + "schema": "double" + }, + { + "name": "minTemp", + "displayName": "Min temperature", + "schema": "double" + }, + { + "name": "avgTemp", + "displayName": "Average Temperature", + "schema": "double" + }, + { + "name": "startTime", + "displayName": "Start Time", + "schema": "dateTime" + }, + { + "name": "endTime", + "displayName": "End Time", + "schema": "dateTime" + } + ] + } + } + } + ] + }, + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "@type": "Interface", + "displayName": "Device Information", + "contents": [ + { + "@type": "Property", + "name": "manufacturer", + "displayName": "Manufacturer", + "schema": "string", + "description": "Company name of the device manufacturer. This could be the same as the name of the original equipment manufacturer (OEM). Ex. Contoso." + }, + { + "@type": "Property", + "name": "model", + "displayName": "Device model", + "schema": "string", + "description": "Device model name or ID. Ex. Surface Book 2." + }, + { + "@type": "Property", + "name": "swVersion", + "displayName": "Software version", + "schema": "string", + "description": "Version of the software on your device. This could be the version of your firmware. Ex. 1.3.45" + }, + { + "@type": "Property", + "name": "osName", + "displayName": "Operating system name", + "schema": "string", + "description": "Name of the operating system on the device. Ex. Windows 10 IoT Core." + }, + { + "@type": "Property", + "name": "processorArchitecture", + "displayName": "Processor architecture", + "schema": "string", + "description": "Architecture of the processor on the device. Ex. x64 or ARM." + }, + { + "@type": "Property", + "name": "processorManufacturer", + "displayName": "Processor manufacturer", + "schema": "string", + "description": "Name of the manufacturer of the processor on the device. Ex. Intel." + }, + { + "@type": "Property", + "name": "totalStorage", + "displayName": "Total storage", + "schema": "double", + "description": "Total available storage on the device in kilobytes. Ex. 2048000 kilobytes." + }, + { + "@type": "Property", + "name": "totalMemory", + "displayName": "Total memory", + "schema": "double", + "description": "Total available memory on the device in kilobytes. Ex. 256000 kilobytes." + } + ] + } +] \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.json new file mode 100644 index 000000000000..c455ddf8bae6 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.json @@ -0,0 +1,60 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:com:example:TemperatureController;1", + "@type": "Interface", + "displayName": "Temperature Controller", + "description": "Device with two thermostats and remote reboot.", + "contents": [ + { + "@type": [ + "Telemetry", + "DataSize" + ], + "name": "workingSet", + "displayName": "Working Set", + "description": "Current working set of the device memory in KiB.", + "schema": "double", + "unit": "kibibyte" + }, + { + "@type": "Property", + "name": "serialNumber", + "displayName": "Serial Number", + "description": "Serial number of the device.", + "schema": "string" + }, + { + "@type": "Command", + "name": "reboot", + "displayName": "Reboot", + "description": "Reboots the device after waiting the number of seconds specified.", + "request": { + "name": "delay", + "displayName": "Delay", + "description": "Number of seconds to wait before rebooting the device.", + "schema": "integer" + } + }, + { + "@type": "Component", + "schema": "dtmi:com:example:Thermostat;1", + "name": "thermostat1", + "displayName": "Thermostat One", + "description": "Thermostat One of Two." + }, + { + "@type": "Component", + "schema": "dtmi:com:example:Thermostat;1", + "name": "thermostat2", + "displayName": "Thermostat Two", + "description": "Thermostat Two of Two." + }, + { + "@type": "Component", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "name": "deviceInformation", + "displayName": "Device Information interface", + "description": "Optional interface with basic device hardware information." + } + ] +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/thermostat-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/thermostat-1.json new file mode 100644 index 000000000000..315a307bbcb3 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/thermostat-1.json @@ -0,0 +1,19 @@ +{ + "@id": "dtmi:com:example:Thermostat;1", + "@type": "Interface", + "displayName": "Thermostat", + "contents": [ + { + "@type": "Telemetry", + "name": "temp", + "schema": "double" + }, + { + "@type": "Property", + "name": "setPointTemp", + "writable": true, + "schema": "double" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-1.json new file mode 100644 index 000000000000..33c9554664a6 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-1.json @@ -0,0 +1,31 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:company:demodevice;1", + "@type": "Interface", + "displayName": "demodevice", + "contents": [ + { + "@type": "Component", + "name": "c1", + "schema": "dtmi:azure:deviceManagement:DeviceInformation;1" + }, + { + "@type": "Telemetry", + "name": "temperature", + "schema": "double" + }, + { + "@type": "Property", + "name": "deviceStatus", + "schema": "string" + }, + { + "@type": "Command", + "name": "reboot", + "request": { + "name": "delay", + "schema": "integer" + } + } + ] +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-2.json new file mode 100644 index 000000000000..9d9b3bd2322a --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-2.json @@ -0,0 +1,31 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:company:demodevice;1", + "@type": "Interface", + "displayName": "demodevice", + "contents": [ + { + "@type": "Component", + "name": "c1", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1" + }, + { + "@type": "Telemetry", + "name": "temperature", + "schema": "double" + }, + { + "@type": "Property", + "name": "deviceStatus", + "schema": "string" + }, + { + "@type": "Command", + "name": "reboot", + "request": { + "name": "delay", + "schema": "integer" + } + } + ] +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/badfilepath-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/badfilepath-1.json new file mode 100644 index 000000000000..6006b6673299 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/badfilepath-1.json @@ -0,0 +1,12 @@ +{ + "@id": "dtmi:com:example:Freezer;1", + "@type": "Interface", + "contents": [ + { + "@type": "Component", + "name": "deviceInfo", + "schema": "dtmi:com:example:Thermostat;1" + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/emptyarray-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/emptyarray-1.json new file mode 100644 index 000000000000..0637a088a01e --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/emptyarray-1.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/namespaceconflict-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/namespaceconflict-1.json new file mode 100644 index 000000000000..6f2df812a67f --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/namespaceconflict-1.json @@ -0,0 +1,37 @@ +{ + "@id": "dtmi:strict:namespaceconflict;1", + "@type": "Interface", + "contents": [ + { + "@type": "Telemetry", + "name": "accelerometer1", + "schema": "dtmi:com:example:acceleration;1" + }, + { + "@type": "Telemetry", + "name": "accelerometer2", + "schema": "dtmi:com:example:acceleration;1" + } + ], + "schemas": [ + { + "@id": "dtmi:com:example:acceleration;1", + "@type": "Object", + "fields": [ + { + "name": "x", + "schema": "double" + }, + { + "name": "y", + "schema": "double" + }, + { + "name": "z", + "schema": "double" + } + ] + } + ], + "@context": "dtmi:dtdl:context;2" +} \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/nondtdl-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/nondtdl-1.json new file mode 100644 index 000000000000..b25ba2ac3af6 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/nondtdl-1.json @@ -0,0 +1 @@ +"content" \ No newline at end of file diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/unsupportedrootarray-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/unsupportedrootarray-1.json new file mode 100644 index 000000000000..1f282307a8c3 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/unsupportedrootarray-1.json @@ -0,0 +1,91 @@ +[ + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:strict:unsupportedrootarray;1", + "@type": "Interface", + "displayName": "Thermostat", + "description": "Reports current temperature and provides desired temperature control.", + "contents": [ + { + "@type": [ + "Telemetry", + "Temperature" + ], + "name": "temperature", + "displayName": "Temperature", + "description": "Temperature in degrees Celsius.", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": [ + "Property", + "Temperature" + ], + "name": "targetTemperature", + "schema": "double", + "displayName": "Target Temperature", + "description": "Allows to remotely specify the desired target temperature.", + "unit": "degreeCelsius", + "writable": true + }, + { + "@type": [ + "Property", + "Temperature" + ], + "name": "maxTempSinceLastReboot", + "schema": "double", + "unit": "degreeCelsius", + "displayName": "Max temperature since last reboot.", + "description": "Returns the max temperature since last device reboot." + }, + { + "@type": "Command", + "name": "getMaxMinReport", + "displayName": "Get Max-Min report.", + "description": "This command returns the max, min and average temperature from the specified time to the current time.", + "request": { + "name": "since", + "displayName": "Since", + "description": "Period to return the max-min report.", + "schema": "dateTime" + }, + "response": { + "name": "tempReport", + "displayName": "Temperature Report", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "maxTemp", + "displayName": "Max temperature", + "schema": "double" + }, + { + "name": "minTemp", + "displayName": "Min temperature", + "schema": "double" + }, + { + "name": "avgTemp", + "displayName": "Average Temperature", + "schema": "double" + }, + { + "name": "startTime", + "displayName": "Start Time", + "schema": "dateTime" + }, + { + "name": "endTime", + "displayName": "End Time", + "schema": "dateTime" + } + ] + } + } + } + ] + } +] \ No newline at end of file From 3f112acd9e031e4ca928dad38c2d432c8765badd Mon Sep 17 00:00:00 2001 From: Azad Abbasi Date: Thu, 11 Feb 2021 12:20:55 -0800 Subject: [PATCH 2/5] Update ResolverClientOptions.cs --- .../Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs index 843f92203455..f49cb34ce9b0 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs @@ -13,7 +13,7 @@ public class ResolverClientOptions : ClientOptions internal const ServiceVersion LatestVersion = ServiceVersion.V2021_02_11; /// - /// The versions of Azure Digital Twins supported by this client + /// The versions of Azure IoT Model Repository by this client /// library. /// public enum ServiceVersion From 5f917dc881fca597d6d05c3e7aa5703efaeb752d Mon Sep 17 00:00:00 2001 From: Azad Abbasi Date: Thu, 11 Feb 2021 13:39:42 -0800 Subject: [PATCH 3/5] CR comments. --- .../src/Azure.Iot.ModelsRepository.csproj | 4 ++-- .../src/DtmiConventions.cs | 2 +- .../src/Fetchers/FetchResult.cs | 5 +---- .../src/Fetchers/LocalModelFetcher.cs | 13 ++++--------- .../src/Fetchers/RemoteModelFetcher.cs | 2 ++ .../src/ModelMetadata.cs | 16 ++++++++-------- .../src/RepositoryHandler.cs | 6 +++--- 7 files changed, 21 insertions(+), 27 deletions(-) diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj index 467935155015..1c9e5fa60dd7 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj @@ -9,8 +9,8 @@ - IoT;ModelsRepository;$(PackageCommonTags) - SDK for the Azure IoT Models Repository Tools + IoT;ModelsRepository;Pnp;DigitalTwins$(PackageCommonTags) + SDK for the Azure IoT Models Repository 1.0.0-beta.1 diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs index 9709a6359234..13636761c71e 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs @@ -7,7 +7,7 @@ namespace Azure.Iot.ModelsRepository { - internal class DtmiConventions + internal static class DtmiConventions { public static bool IsDtmi(string dtmi) => !string.IsNullOrEmpty(dtmi) && new Regex(@"^dtmi:[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?(?::[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)*;[1-9][0-9]{0,8}$").IsMatch(dtmi); public static string DtmiToPath(string dtmi) => IsDtmi(dtmi) ? $"{dtmi.ToLowerInvariant().Replace(":", "/").Replace(";", "-")}.json" : null; diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs index 6fb78ac8d4d3..30dd40088aff 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs @@ -7,9 +7,6 @@ internal class FetchResult { public string Definition { get; set; } public string Path { get; set; } - public bool FromExpanded - { - get { return Path.EndsWith("expanded.json", System.StringComparison.InvariantCultureIgnoreCase); } - } + public bool FromExpanded => Path.EndsWith("expanded.json", System.StringComparison.InvariantCultureIgnoreCase); } } diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs index cb38de270c98..4eea403dd414 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs @@ -20,14 +20,14 @@ public LocalModelFetcher(ResolverClientOptions clientOptions) _tryExpanded = clientOptions.DependencyResolution == DependencyResolutionOption.TryFromExpanded; } - public async Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) + public Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) { - return await Task.Run(() => Fetch(dtmi, repositoryUri, cancellationToken)).ConfigureAwait(false); + return Task.FromResult(Fetch(dtmi, repositoryUri, cancellationToken)); } public FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) { - Queue work = new Queue(); + var work = new Queue(); if (_tryExpanded) work.Enqueue(GetPath(dtmi, repositoryUri, true)); @@ -40,7 +40,7 @@ public FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cance string tryContentPath = work.Dequeue(); ResolverEventSource.Shared.FetchingModelContent(tryContentPath); - if (EvaluatePath(tryContentPath)) + if (File.Exists(tryContentPath)) { return new FetchResult() { @@ -61,10 +61,5 @@ private static string GetPath(string dtmi, Uri repositoryUri, bool expanded = fa string registryPath = repositoryUri.AbsolutePath; return DtmiConventions.DtmiToQualifiedPath(dtmi, registryPath, expanded); } - - private static bool EvaluatePath(string path) - { - return File.Exists(path); - } } } diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs index b1379e5cbbe1..a68ee01bf37c 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs @@ -75,6 +75,7 @@ private async Task EvaluatePathAsync(string path, CancellationToken canc request.Uri.Reset(new Uri(path)); Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); + if (response.Status >= 200 && response.Status <= 299) { return await GetContentAsync(response.ContentStream, cancellationToken).ConfigureAwait(false); @@ -82,6 +83,7 @@ private async Task EvaluatePathAsync(string path, CancellationToken canc return null; } + private static async Task GetContentAsync(Stream content, CancellationToken cancellationToken) { using (JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait(false)) diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs index fac3b5fbf36f..bf0cc568bf0f 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs @@ -8,16 +8,16 @@ namespace Azure.Iot.ModelsRepository { internal class ModelMetadata { - public string Id { get; } - public IList Extends { get; } - public IList ComponentSchemas { get; } - public IList Dependencies { get { return Extends.Union(ComponentSchemas).ToList(); } } - public ModelMetadata(string id, IList extends, IList componentSchemas) { - this.Id = id; - this.Extends = extends; - this.ComponentSchemas = componentSchemas; + Id = id; + Extends = extends; + ComponentSchemas = componentSchemas; } + + public string Id { get; } + public IList Extends { get; } + public IList ComponentSchemas { get; } + public IList Dependencies => Extends.Union(ComponentSchemas).ToList(); } } diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs index e2ee13d72d0d..9350237b07ed 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs @@ -22,9 +22,9 @@ public RepositoryHandler(Uri repositoryUri, ResolverClientOptions options = null { ClientOptions = options ?? new ResolverClientOptions(); RepositoryUri = repositoryUri; - _modelFetcher = repositoryUri.Scheme == "file" ? - _modelFetcher = new LocalModelFetcher(ClientOptions) : - _modelFetcher = new RemoteModelFetcher(ClientOptions); + _modelFetcher = repositoryUri.Scheme == "file" + ? _modelFetcher = new LocalModelFetcher(ClientOptions) + : _modelFetcher = new RemoteModelFetcher(ClientOptions); _clientId = Guid.NewGuid(); ResolverEventSource.Shared.InitFetcher(_clientId, repositoryUri.Scheme); } From 434c0146de2b0f075e86628a1d74f91d20e36d49 Mon Sep 17 00:00:00 2001 From: Azad Abbasi Date: Thu, 11 Feb 2021 13:51:27 -0800 Subject: [PATCH 4/5] Add ci and test yaml files and add changelog --- .../Azure.Iot.ModelsRepository/CHANGELOG.md | 19 +++++++++ .../CodeMaid.config | 41 +++++++++++++++++++ .../Directory.Build.props | 6 +++ .../src/Azure.Iot.ModelsRepository.csproj | 2 +- sdk/modelsrepository/ci.yml | 32 +++++++++++++++ sdk/modelsrepository/tests.yml | 8 ++++ 6 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/CHANGELOG.md create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/CodeMaid.config create mode 100644 sdk/modelsrepository/Azure.Iot.ModelsRepository/Directory.Build.props create mode 100644 sdk/modelsrepository/ci.yml create mode 100644 sdk/modelsrepository/tests.yml diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/CHANGELOG.md b/sdk/modelsrepository/Azure.Iot.ModelsRepository/CHANGELOG.md new file mode 100644 index 000000000000..8f770daed613 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/CHANGELOG.md @@ -0,0 +1,19 @@ +# Release History + +## 1.0.0-preview.1 (Unreleased) + +### New features + +- Initial preview of Azure.IoT.ModelRepository SDK + +### Breaking changes + +- N/A + +### Added + +- N/A + +### Fixes and improvements + +- N/A diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/CodeMaid.config b/sdk/modelsrepository/Azure.Iot.ModelsRepository/CodeMaid.config new file mode 100644 index 000000000000..5382ce6fd8f0 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/CodeMaid.config @@ -0,0 +1,41 @@ + + + + +
+ + + + + + True + + + True + + + True + + + 120 + + + False + + + // Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + + + True + + + 1 + + + False + + + + diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/Directory.Build.props b/sdk/modelsrepository/Azure.Iot.ModelsRepository/Directory.Build.props new file mode 100644 index 000000000000..1a9611bd4924 --- /dev/null +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/Directory.Build.props @@ -0,0 +1,6 @@ + + + + diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj index 1c9e5fa60dd7..f522da43641c 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj @@ -11,7 +11,7 @@ IoT;ModelsRepository;Pnp;DigitalTwins$(PackageCommonTags) SDK for the Azure IoT Models Repository - 1.0.0-beta.1 + 1.0.0-preview.1 diff --git a/sdk/modelsrepository/ci.yml b/sdk/modelsrepository/ci.yml new file mode 100644 index 000000000000..49b8441618fc --- /dev/null +++ b/sdk/modelsrepository/ci.yml @@ -0,0 +1,32 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - master + - hotfix/* + - release/* + paths: + include: + - sdk/modelRepository/ + +pr: + branches: + include: + - master + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/modelRepository/ + +extends: + template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: modelRepository + ArtifactName: packages + Artifacts: + - name: Azure.IoT.ModelRepository + safeName: AzureIoTModelRepository + diff --git a/sdk/modelsrepository/tests.yml b/sdk/modelsrepository/tests.yml new file mode 100644 index 000000000000..c88bde775291 --- /dev/null +++ b/sdk/modelsrepository/tests.yml @@ -0,0 +1,8 @@ +trigger: none + +extends: + template: ../../eng/pipelines/templates/stages/archetype-sdk-tests.yml + parameters: + ServiceDirectory: modelrepository + Location: westus2 + Clouds: Preview From 4afb86b9518dbf0398e633c4291ad7cd30844a5d Mon Sep 17 00:00:00 2001 From: Azad Abbasi Date: Thu, 11 Feb 2021 16:25:22 -0800 Subject: [PATCH 5/5] More CR comments --- .../src/ResolverClient.cs | 11 +++++++++-- .../src/StandardStrings.cs | 16 ++++++++-------- .../tests/ModelQueryTests.cs | 12 +++++++----- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs index fd550a1577d3..a6bddeb27c52 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -87,8 +88,14 @@ public ResolverClient(string repositoryUriStr, ResolverClientOptions options) : /// Thrown when a resolution failure occurs. /// A well-formed DTDL model Id. For example 'dtmi:com:example:Thermostat;1'. /// The cancellationToken. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "AZC0004:DO provide both asynchronous and synchronous variants for all service methods.", Justification = "")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "AZC0015:Unexpected client method return type.", Justification = "")] + [SuppressMessage( + "Usage", + "AZC0004:DO provide both asynchronous and synchronous variants for all service methods.", + Justification = "")] + [SuppressMessage( + "Usage", + "AZC0015:Unexpected client method return type.", + Justification = "")] public virtual async Task> ResolveAsync(string dtmi, CancellationToken cancellationToken = default) { return await _repositoryHandler.ProcessAsync(dtmi, cancellationToken).ConfigureAwait(false); diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs index 4906aab19b65..24bc475ee9c2 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs @@ -5,15 +5,15 @@ namespace Azure.Iot.ModelsRepository { internal class StandardStrings { - public const string GenericResolverError = "Unable to resolve \"{0}\". "; - public const string InvalidDtmiFormat = "Invalid DTMI format \"{0}\". "; - public const string ClientInitWithFetcher = "Client session {0} initialized with {1} content fetcher. "; + public const string GenericResolverError = "Unable to resolve \"{0}\"."; + public const string InvalidDtmiFormat = "Invalid DTMI format \"{0}\"."; + public const string ClientInitWithFetcher = "Client session {0} initialized with {1} content fetcher."; public const string ProcessingDtmi = "Processing DTMI \"{0}\". "; - public const string SkippingPreProcessedDtmi = "Already processed DTMI \"{0}\". Skipping. "; - public const string DiscoveredDependencies = "Discovered dependencies \"{0}\". "; - public const string FetchingModelContent = "Attempting to retrieve model content from \"{0}\". "; - public const string ErrorFetchingModelContent = "Model file \"{0}\" not found or not accessible in target repository. "; + public const string SkippingPreProcessedDtmi = "Already processed DTMI \"{0}\". Skipping."; + public const string DiscoveredDependencies = "Discovered dependencies \"{0}\"."; + public const string FetchingModelContent = "Attempting to retrieve model content from \"{0}\"."; + public const string ErrorFetchingModelContent = "Model file \"{0}\" not found or not accessible in target repository."; public const string IncorrectDtmiCasing = - "Retrieved model has incorrect DTMI casing. Expected \"{0}\", parsed \"{1}\". "; + "Retrieved model has incorrect DTMI casing. Expected \"{0}\", parsed \"{1}\"."; } } diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs index 1d16d26e7781..674ee96b31c6 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs @@ -53,7 +53,8 @@ public void GetId(string formatId, string expectedId) ""name"": ""deviceInfo"", ""schema"": ""dtmi:azure:DeviceManagement:DeviceInformation;1"" }],", - "dtmi:com:example:Camera;3,dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1")] + "dtmi:com:example:Camera;3,dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1" + )] [TestCase( @" ""contents"": @@ -61,7 +62,8 @@ public void GetId(string formatId, string expectedId) ""@type"": ""Property"", ""name"": ""capacity"", ""schema"": ""integer"" - }],", "")] + }],", "" + )] [TestCase(@"""contents"":[],", "")] [TestCase("", "")] public void GetComponentSchema(string contents, string expected) @@ -80,7 +82,8 @@ public void GetComponentSchema(string contents, string expected) [TestCase( "\"extends\": [\"dtmi:com:example:Camera;3\",\"dtmi:azure:DeviceManagement:DeviceInformation;1\"],", - "dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1")] + "dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1" + )] [TestCase("\"extends\": [],", "")] [TestCase("\"extends\": \"dtmi:com:example:Camera;3\",", "dtmi:com:example:Camera;3")] [TestCase("", "")] @@ -142,8 +145,7 @@ public void GetExtends(string extends, string expected) }],", "", "dtmi:example:Interface2;1,dtmi:example:Interface4;1,dtmi:example:Interface6;1" - ) - ] + )] public void GetModelDependencies(string id, string extends, string contents, string expected) { string[] expectedDtmis = expected.Split(new[] { "," }, System.StringSplitOptions.RemoveEmptyEntries);