diff --git a/build.ps1 b/build.ps1 index 78bdbdd..1591563 100644 --- a/build.ps1 +++ b/build.ps1 @@ -118,7 +118,12 @@ function Write-Manifest { 'Get-InfisicalFolder', 'New-InfisicalFolder', 'Update-InfisicalFolder', - 'Remove-InfisicalFolder' + 'Remove-InfisicalFolder', + 'Get-InfisicalTags', + 'Get-InfisicalTag', + 'New-InfisicalTag', + 'Update-InfisicalTag', + 'Remove-InfisicalTag' ) AliasesToExport = @() VariablesToExport = @() @@ -178,7 +183,7 @@ if (`$null -eq `$manifest) { Import-Module -Name '$($ModuleDirectory.FullName)' -Force -`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder') +`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag') foreach (`$c in `$cmds) { if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { throw "Cmdlet not found: `$c" diff --git a/src/PSInfisicalAPI.Tests/TagMapperTests.cs b/src/PSInfisicalAPI.Tests/TagMapperTests.cs new file mode 100644 index 0000000..12af553 --- /dev/null +++ b/src/PSInfisicalAPI.Tests/TagMapperTests.cs @@ -0,0 +1,77 @@ +using System.Reflection; +using PSInfisicalAPI.Models; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class TagMapperTests + { + private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly + .GetType("PSInfisicalAPI.Tags.InfisicalTagMapper", true); + + private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly + .GetType("PSInfisicalAPI.Tags.InfisicalTagResponseDto", true); + + private static InfisicalTag InvokeMap(object dto, string fallbackProjectId) + { + MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static); + return (InfisicalTag)map.Invoke(null, new object[] { dto, fallbackProjectId }); + } + + [Fact] + public void Map_Null_Returns_Null() + { + Assert.Null(InvokeMap(null, "proj-x")); + } + + [Fact] + public void Map_Populates_Fields_With_Explicit_ProjectId() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("Id").SetValue(dto, "tag-001"); + DtoType.GetProperty("Slug").SetValue(dto, "critical"); + DtoType.GetProperty("Name").SetValue(dto, "Critical"); + DtoType.GetProperty("Color").SetValue(dto, "#FF0000"); + DtoType.GetProperty("ProjectId").SetValue(dto, "proj-001"); + + InfisicalTag tag = InvokeMap(dto, "fallback-proj"); + + Assert.Equal("tag-001", tag.Id); + Assert.Equal("critical", tag.Slug); + Assert.Equal("Critical", tag.Name); + Assert.Equal("#FF0000", tag.Color); + Assert.Equal("proj-001", tag.ProjectId); + } + + [Fact] + public void Map_Uses_WorkspaceId_When_ProjectId_Empty() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("Id").SetValue(dto, "tag-002"); + DtoType.GetProperty("WorkspaceId").SetValue(dto, "wks-002"); + + InfisicalTag tag = InvokeMap(dto, "fallback-proj"); + Assert.Equal("wks-002", tag.ProjectId); + } + + [Fact] + public void Map_Uses_Fallback_When_No_ProjectId_Or_WorkspaceId() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("Id").SetValue(dto, "tag-003"); + + InfisicalTag tag = InvokeMap(dto, "fallback-proj"); + Assert.Equal("fallback-proj", tag.ProjectId); + } + + [Fact] + public void Map_Falls_Back_To_InternalId_For_Id() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("InternalId").SetValue(dto, "internal-tag"); + + InfisicalTag tag = InvokeMap(dto, "p"); + Assert.Equal("internal-tag", tag.Id); + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs new file mode 100644 index 0000000..cc8d32e --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs @@ -0,0 +1,37 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Tags; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalTag")] + [OutputType(typeof(InfisicalTag))] + public sealed class GetInfisicalTagCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Slug", "Id")] + public string TagSlugOrId { get; set; } + + [Parameter] public string ProjectId { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); + InfisicalTag tag = client.Retrieve(connection, ProjectId, TagSlugOrId); + if (tag != null) + { + WriteObject(tag); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalTagCmdlet", "RetrieveTag", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagsCmdlet.cs new file mode 100644 index 0000000..8b10649 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagsCmdlet.cs @@ -0,0 +1,33 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Tags; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalTags")] + [OutputType(typeof(InfisicalTag))] + public sealed class GetInfisicalTagsCmdlet : InfisicalCmdletBase + { + [Parameter] public string ProjectId { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); + InfisicalTag[] tags = client.List(connection, ProjectId); + foreach (InfisicalTag tag in tags) + { + WriteObject(tag); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalTagsCmdlet", "ListTags", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalTagCmdlet.cs new file mode 100644 index 0000000..21d99d0 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalTagCmdlet.cs @@ -0,0 +1,41 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Tags; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.New, "InfisicalTag", SupportsShouldProcess = true)] + [OutputType(typeof(InfisicalTag))] + public sealed class NewInfisicalTagCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, Position = 0)] public string Slug { get; set; } + [Parameter] public string Name { get; set; } + [Parameter] public string Color { get; set; } + [Parameter] public string ProjectId { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(Slug, "Create Infisical tag")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); + InfisicalTag tag = client.Create(connection, ProjectId, Slug, Name, Color); + if (tag != null) + { + WriteObject(tag); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("NewInfisicalTagCmdlet", "CreateTag", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalTagCmdlet.cs new file mode 100644 index 0000000..bb14432 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalTagCmdlet.cs @@ -0,0 +1,42 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Tags; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Remove, "InfisicalTag", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + public sealed class RemoveInfisicalTagCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Id")] + public string TagId { get; set; } + + [Parameter] public string ProjectId { get; set; } + [Parameter] public SwitchParameter PassThru { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(TagId, "Remove Infisical tag")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); + client.Delete(connection, ProjectId, TagId); + + if (PassThru.IsPresent) + { + WriteObject(TagId); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("RemoveInfisicalTagCmdlet", "DeleteTag", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalTagCmdlet.cs new file mode 100644 index 0000000..9a6c45b --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalTagCmdlet.cs @@ -0,0 +1,45 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Tags; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsData.Update, "InfisicalTag", SupportsShouldProcess = true)] + [OutputType(typeof(InfisicalTag))] + public sealed class UpdateInfisicalTagCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Id")] + public string TagId { get; set; } + + [Parameter] public string Slug { get; set; } + [Parameter] public string Name { get; set; } + [Parameter] public string Color { get; set; } + [Parameter] public string ProjectId { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(TagId, "Update Infisical tag")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); + InfisicalTag tag = client.Update(connection, ProjectId, TagId, Slug, Name, Color); + if (tag != null) + { + WriteObject(tag); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("UpdateInfisicalTagCmdlet", "UpdateTag", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalTag.cs b/src/PSInfisicalAPI/Models/InfisicalTag.cs new file mode 100644 index 0000000..4eec34c --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalTag.cs @@ -0,0 +1,20 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalTag + { + public string Id { get; set; } + public string Slug { get; set; } + public string Name { get; set; } + public string Color { get; set; } + public string ProjectId { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + + public override string ToString() + { + return Slug ?? Name ?? Id; + } + } +} diff --git a/src/PSInfisicalAPI/Tags/InfisicalTagClient.cs b/src/PSInfisicalAPI/Tags/InfisicalTagClient.cs new file mode 100644 index 0000000..9430ae2 --- /dev/null +++ b/src/PSInfisicalAPI/Tags/InfisicalTagClient.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Endpoints; +using PSInfisicalAPI.Errors; +using PSInfisicalAPI.Http; +using PSInfisicalAPI.Logging; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Serialization; + +namespace PSInfisicalAPI.Tags +{ + public sealed class InfisicalTagClient + { + private const string Component = "TagClient"; + + private readonly IInfisicalLogger _logger; + private readonly JsonInfisicalSerializer _serializer; + private readonly InfisicalApiInvoker _invoker; + + public InfisicalTagClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger) + { + if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); } + _logger = logger ?? NullInfisicalLogger.Instance; + _serializer = new JsonInfisicalSerializer(); + _invoker = new InfisicalApiInvoker(httpClient); + } + + public InfisicalTag[] List(InfisicalConnection connection, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId } }; + + try + { + _logger.Information(Component, "Attempting to list Infisical tags. Please Wait..."); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListTags, "ListTags", pathParameters, null, null); + InfisicalTagListResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + List source = dto != null ? (dto.WorkspaceTags ?? dto.Tags) : null; + InfisicalTag[] mapped = InfisicalTagMapper.MapMany(source, resolvedProjectId); + _logger.Information(Component, "Infisical tag list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical tag list retrieval failed."); + throw; + } + } + + public InfisicalTag Retrieve(InfisicalConnection connection, string projectId, string tagSlugOrId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(tagSlugOrId)) { throw new InfisicalConfigurationException("Tag slug or id is required."); } + + InfisicalTag[] all = List(connection, resolvedProjectId); + foreach (InfisicalTag tag in all) + { + if (string.Equals(tag.Id, tagSlugOrId, StringComparison.OrdinalIgnoreCase) || + string.Equals(tag.Slug, tagSlugOrId, StringComparison.OrdinalIgnoreCase)) + { + return tag; + } + } + + return null; + } + + public InfisicalTag Create(InfisicalConnection connection, string projectId, string slug, string name, string color) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(slug)) { throw new InfisicalConfigurationException("Slug is required."); } + + Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId } }; + InfisicalTagCreateRequestDto request = new InfisicalTagCreateRequestDto { Slug = slug, Name = name, Color = color }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to create Infisical tag '", slug, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.CreateTag, "CreateTag", pathParameters, null, body); + InfisicalTagSingleResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalTagResponseDto inner = dto != null ? (dto.WorkspaceTag ?? dto.Tag) : null; + InfisicalTag mapped = InfisicalTagMapper.Map(inner, resolvedProjectId); + _logger.Information(Component, "Infisical tag creation was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical tag creation failed."); + throw; + } + } + + public InfisicalTag Update(InfisicalConnection connection, string projectId, string tagId, string slug, string name, string color) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(tagId)) { throw new InfisicalConfigurationException("TagId is required."); } + + Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId }, { "tagId", tagId } }; + InfisicalTagUpdateRequestDto request = new InfisicalTagUpdateRequestDto { Slug = slug, Name = name, Color = color }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to update Infisical tag '", tagId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.UpdateTag, "UpdateTag", pathParameters, null, body); + InfisicalTagSingleResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalTagResponseDto inner = dto != null ? (dto.WorkspaceTag ?? dto.Tag) : null; + InfisicalTag mapped = InfisicalTagMapper.Map(inner, resolvedProjectId); + _logger.Information(Component, "Infisical tag update was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical tag update failed."); + throw; + } + } + + public void Delete(InfisicalConnection connection, string projectId, string tagId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(tagId)) { throw new InfisicalConfigurationException("TagId is required."); } + + Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId }, { "tagId", tagId } }; + + try + { + _logger.Information(Component, string.Concat("Attempting to delete Infisical tag '", tagId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.DeleteTag, "DeleteTag", pathParameters, null, null); + response.Clear(); + _logger.Information(Component, "Infisical tag deletion was successful."); + } + catch (Exception) + { + _logger.Error(Component, "Infisical tag deletion failed."); + throw; + } + } + + private static string FirstNonEmpty(params string[] values) + { + if (values == null) { return null; } + foreach (string value in values) { if (!string.IsNullOrEmpty(value)) { return value; } } + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Tags/InfisicalTagDtos.cs b/src/PSInfisicalAPI/Tags/InfisicalTagDtos.cs new file mode 100644 index 0000000..622c80a --- /dev/null +++ b/src/PSInfisicalAPI/Tags/InfisicalTagDtos.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Tags +{ + internal sealed class InfisicalTagResponseDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("_id")] public string InternalId { get; set; } + [JsonProperty("slug")] public string Slug { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("color")] public string Color { get; set; } + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("workspaceId")] public string WorkspaceId { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + } + + internal sealed class InfisicalTagListResponseDto + { + [JsonProperty("workspaceTags")] public List WorkspaceTags { get; set; } + [JsonProperty("tags")] public List Tags { get; set; } + } + + internal sealed class InfisicalTagSingleResponseDto + { + [JsonProperty("workspaceTag")] public InfisicalTagResponseDto WorkspaceTag { get; set; } + [JsonProperty("tag")] public InfisicalTagResponseDto Tag { get; set; } + } + + internal sealed class InfisicalTagCreateRequestDto + { + [JsonProperty("slug")] public string Slug { get; set; } + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } + [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] public string Color { get; set; } + } + + internal sealed class InfisicalTagUpdateRequestDto + { + [JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; } + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } + [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] public string Color { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Tags/InfisicalTagMapper.cs b/src/PSInfisicalAPI/Tags/InfisicalTagMapper.cs new file mode 100644 index 0000000..04ac855 --- /dev/null +++ b/src/PSInfisicalAPI/Tags/InfisicalTagMapper.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Tags +{ + internal static class InfisicalTagMapper + { + public static InfisicalTag Map(InfisicalTagResponseDto dto, string fallbackProjectId) + { + if (dto == null) + { + return null; + } + + string projectId = !string.IsNullOrEmpty(dto.ProjectId) + ? dto.ProjectId + : (!string.IsNullOrEmpty(dto.WorkspaceId) ? dto.WorkspaceId : fallbackProjectId); + + return new InfisicalTag + { + Id = !string.IsNullOrEmpty(dto.Id) ? dto.Id : dto.InternalId, + Slug = dto.Slug, + Name = dto.Name, + Color = dto.Color, + ProjectId = projectId, + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + } + + public static InfisicalTag[] MapMany(IEnumerable items, string fallbackProjectId) + { + if (items == null) + { + return Array.Empty(); + } + + List results = new List(); + foreach (InfisicalTagResponseDto dto in items) + { + InfisicalTag mapped = Map(dto, fallbackProjectId); + if (mapped != null) + { + results.Add(mapped); + } + } + + return results.ToArray(); + } + + private static DateTimeOffset? ParseTimestamp(string value) + { + if (string.IsNullOrEmpty(value)) + { + return null; + } + + DateTimeOffset parsed; + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed)) + { + return parsed; + } + + return null; + } + } +}