Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions Foundation.Data.Doublets.Cli.Tests/StorablePatternManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using Xunit;
using Foundation.Data.Doublets.Cli;
using System.IO;
using System.Text.Json;

namespace Foundation.Data.Doublets.Cli.Tests
{
public class StorablePatternManagerTests
{
private readonly string _tempFilePath;

public StorablePatternManagerTests()
{
_tempFilePath = Path.GetTempFileName();
}

public void Dispose()
{
if (File.Exists(_tempFilePath))
{
File.Delete(_tempFilePath);
}
}

[Fact]
public void AddPattern_ShouldStorePatternCorrectly()
{
// Arrange
var manager = new StorablePatternManager(_tempFilePath);
var query = "((1 1)) ((1 2))";

// Act
manager.AddPattern(query, "Test pattern");

// Assert
var patterns = manager.GetActivePatterns();
Assert.Single(patterns);
Assert.Equal(query, patterns[0].Query);
Assert.Equal("Test pattern", patterns[0].Description);
Assert.True(patterns[0].IsActive);
}

[Fact]
public void RemovePattern_ShouldRemoveExistingPattern()
{
// Arrange
var manager = new StorablePatternManager(_tempFilePath);
var query = "((1 1)) ((1 2))";
manager.AddPattern(query);

// Act
bool removed = manager.RemovePattern(query);

// Assert
Assert.True(removed);
var patterns = manager.GetActivePatterns();
Assert.Empty(patterns);
}

[Fact]
public void RemovePattern_ShouldReturnFalseForNonExistentPattern()
{
// Arrange
var manager = new StorablePatternManager(_tempFilePath);
var query = "((1 1)) ((1 2))";

// Act
bool removed = manager.RemovePattern(query);

// Assert
Assert.False(removed);
}

[Fact]
public void GetActivePatterns_ShouldReturnOnlyActivePatterns()
{
// Arrange
var manager = new StorablePatternManager(_tempFilePath);
manager.AddPattern("((1 1)) ((1 2))", "Pattern 1");
manager.AddPattern("((2 2)) ((2 3))", "Pattern 2");

// Act
var patterns = manager.GetActivePatterns();

// Assert
Assert.Equal(2, patterns.Count);
Assert.All(patterns, p => Assert.True(p.IsActive));
}

[Fact]
public void RemoveAllPatterns_ShouldClearAllPatterns()
{
// Arrange
var manager = new StorablePatternManager(_tempFilePath);
manager.AddPattern("((1 1)) ((1 2))");
manager.AddPattern("((2 2)) ((2 3))");

// Act
manager.RemoveAllPatterns();

// Assert
var patterns = manager.GetActivePatterns();
Assert.Empty(patterns);
}

[Fact]
public void LoadPatterns_ShouldHandleNonExistentFile()
{
// Arrange
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());

// Act & Assert (should not throw)
var manager = new StorablePatternManager(nonExistentPath);
var patterns = manager.GetActivePatterns();
Assert.Empty(patterns);
}

[Fact]
public void LoadPatterns_ShouldHandleCorruptedFile()
{
// Arrange
File.WriteAllText(_tempFilePath, "invalid json content");

// Act & Assert (should not throw)
var manager = new StorablePatternManager(_tempFilePath);
var patterns = manager.GetActivePatterns();
Assert.Empty(patterns);
}

[Fact]
public void PersistenceTest_ShouldMaintainPatternsAcrossInstances()
{
// Arrange
var query1 = "((1 1)) ((1 2))";
var query2 = "((2 2)) ((2 3))";

// Act - First instance
var manager1 = new StorablePatternManager(_tempFilePath);
manager1.AddPattern(query1, "Pattern 1");
manager1.AddPattern(query2, "Pattern 2");

// Act - Second instance
var manager2 = new StorablePatternManager(_tempFilePath);
var patterns = manager2.GetActivePatterns();

// Assert
Assert.Equal(2, patterns.Count);
Assert.Contains(patterns, p => p.Query == query1);
Assert.Contains(patterns, p => p.Query == query2);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<Authors>link-foundation</Authors>
<Description>A CLI tool for links manipulation.</Description>
<PackageId>clink</PackageId>
<Version>2.2.2</Version>
<Version>2.3.0</Version>
<PackageLicenseExpression>Unlicense</PackageLicenseExpression>
<RepositoryUrl>https://github.com/link-foundation/link-cli</RepositoryUrl>
</PropertyGroup>
Expand Down
75 changes: 70 additions & 5 deletions Foundation.Data.Doublets.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using Platform.Data;
using Platform.Data.Doublets;
using Platform.Data.Doublets.Memory.United.Generic;
Expand Down Expand Up @@ -70,6 +71,24 @@
afterOption.AddAlias("--links");
afterOption.AddAlias("-a");

var alwaysOption = new Option<bool>(
name: "--always",
description: "Store the query as a transformation pattern to be executed on every data store change",
getDefaultValue: () => false
);

var neverOption = new Option<bool>(
name: "--never",
description: "Remove the stored transformation pattern",
getDefaultValue: () => false
);

var patternsFileOption = new Option<string>(
name: "--patterns-file",
description: "Path to the transformation patterns file",
getDefaultValue: () => "patterns.json"
);

var rootCommand = new RootCommand("LiNo CLI Tool for managing links data store")
{
dbOption,
Expand All @@ -79,12 +98,26 @@
structureOption,
beforeOption,
changesOption,
afterOption
afterOption,
alwaysOption,
neverOption,
patternsFileOption
};

rootCommand.SetHandler(
(string db, string queryOptionValue, string queryArgumentValue, bool trace, uint? structure, bool before, bool changes, bool after) =>
(InvocationContext context) =>
{
var db = context.ParseResult.GetValueForOption(dbOption)!;
var queryOptionValue = context.ParseResult.GetValueForOption(queryOption) ?? "";
var queryArgumentValue = context.ParseResult.GetValueForArgument(queryArgument) ?? "";
var trace = context.ParseResult.GetValueForOption(traceOption);
var structure = context.ParseResult.GetValueForOption(structureOption);
var before = context.ParseResult.GetValueForOption(beforeOption);
var changes = context.ParseResult.GetValueForOption(changesOption);
var after = context.ParseResult.GetValueForOption(afterOption);
var always = context.ParseResult.GetValueForOption(alwaysOption);
var never = context.ParseResult.GetValueForOption(neverOption);
var patternsFile = context.ParseResult.GetValueForOption(patternsFileOption)!;
var decoratedLinks = new NamedLinksDecorator<uint>(db, trace);

// If --structure is provided, handle it separately
Expand Down Expand Up @@ -141,13 +174,45 @@
}
}

// Handle storable transformation patterns
var patternManager = new StorablePatternManager(patternsFile);

if (always && !string.IsNullOrWhiteSpace(effectiveQuery))
{
patternManager.AddPattern(effectiveQuery);
Console.WriteLine($"Transformation pattern stored: {effectiveQuery}");
}

if (never && !string.IsNullOrWhiteSpace(effectiveQuery))
{
bool removed = patternManager.RemovePattern(effectiveQuery);
if (removed)
{
Console.WriteLine($"Transformation pattern removed: {effectiveQuery}");
}
else
{
Console.WriteLine($"Transformation pattern not found: {effectiveQuery}");
}
}

// Apply stored patterns on any data change
if (changesList.Any())
{
patternManager.ApplyStoredPatternsOnChange(decoratedLinks, (status, pattern) =>
{
if (trace)
{
Console.WriteLine($"Applied stored pattern: {pattern.Query} - {status}");
}
});
}

if (after)
{
PrintAllLinks(decoratedLinks);
}
},
// Explicitly specify the type parameters
dbOption, queryOption, queryArgument, traceOption, structureOption, beforeOption, changesOption, afterOption
}
);

await rootCommand.InvokeAsync(args);
Expand Down
121 changes: 121 additions & 0 deletions Foundation.Data.Doublets.Cli/StorablePatternManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System.Text.Json;

namespace Foundation.Data.Doublets.Cli
{
public class StorablePatternManager
{
private readonly string _patternFilePath;
private readonly List<StoredPattern> _patterns;

public StorablePatternManager(string patternFilePath)
{
_patternFilePath = patternFilePath;
_patterns = LoadPatterns();
}

public void AddPattern(string query, string? description = null)
{
var pattern = new StoredPattern
{
Id = Guid.NewGuid().ToString(),
Query = query,
Description = description,
CreatedAt = DateTime.UtcNow,
IsActive = true
};

_patterns.Add(pattern);
SavePatterns();
}

public bool RemovePattern(string query)
{
var pattern = _patterns.FirstOrDefault(p => p.Query == query);
if (pattern != null)
{
_patterns.Remove(pattern);
SavePatterns();
return true;
}
return false;
}

public void RemoveAllPatterns()
{
_patterns.Clear();
SavePatterns();
}

public IReadOnlyList<StoredPattern> GetActivePatterns()
{
return _patterns.Where(p => p.IsActive).ToList().AsReadOnly();
}

public void ApplyStoredPatternsOnChange(NamedLinksDecorator<uint> links, Action<string, StoredPattern> onPatternExecution)
{
var activePatterns = GetActivePatterns();
foreach (var pattern in activePatterns)
{
try
{
var options = new AdvancedMixedQueryProcessor.Options
{
Query = pattern.Query,
Trace = false
};

AdvancedMixedQueryProcessor.ProcessQuery(links, options);
onPatternExecution?.Invoke("Success", pattern);
}
catch (Exception ex)
{
onPatternExecution?.Invoke($"Error: {ex.Message}", pattern);
}
}
}

private List<StoredPattern> LoadPatterns()
{
if (!File.Exists(_patternFilePath))
{
return new List<StoredPattern>();
}

try
{
var json = File.ReadAllText(_patternFilePath);
var patterns = JsonSerializer.Deserialize<List<StoredPattern>>(json);
return patterns ?? new List<StoredPattern>();
}
catch (Exception)
{
return new List<StoredPattern>();
}
}

private void SavePatterns()
{
try
{
var json = JsonSerializer.Serialize(_patterns, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_patternFilePath, json);
}
catch (Exception)
{
// Log or handle error as needed
}
}
}

public class StoredPattern
{
public string Id { get; set; } = string.Empty;
public string Query { get; set; } = string.Empty;
public string? Description { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; } = true;
}
}
Loading