diff --git a/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj b/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj
index 1d8f9ab..a3c0bac 100644
--- a/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj
+++ b/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj
@@ -13,19 +13,24 @@
link-foundation
- A CLI tool for links manipulation.
+ A CLI tool for links manipulation with REST API support.
clink
- 2.2.2
+ 2.3.0
Unlicense
https://github.com/link-foundation/link-cli
+
+
+
+
+
diff --git a/Foundation.Data.Doublets.Cli/LinksController.cs b/Foundation.Data.Doublets.Cli/LinksController.cs
new file mode 100644
index 0000000..716df3d
--- /dev/null
+++ b/Foundation.Data.Doublets.Cli/LinksController.cs
@@ -0,0 +1,214 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Platform.Data.Doublets;
+using System.Text;
+using DoubletLink = Platform.Data.Doublets.Link;
+using QueryProcessor = Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor;
+
+namespace Foundation.Data.Doublets.Cli
+{
+ [ApiController]
+ [Route("api/[controller]")]
+ public class LinksController : ControllerBase
+ {
+ private readonly string _dbPath;
+
+ public LinksController(IConfiguration configuration)
+ {
+ _dbPath = configuration.GetValue("Database:Path") ?? "db.links";
+ }
+
+ [HttpGet]
+ public async Task GetAllLinks([FromQuery] bool trace = false)
+ {
+ try
+ {
+ var decoratedLinks = new NamedLinksDecorator(_dbPath, trace);
+ var query = "((($i: $s $t)) (($i: $s $t)))";
+ var result = await ProcessLinoQueryAsync(decoratedLinks, query, trace);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Error: {ex.Message}");
+ }
+ }
+
+ [HttpPost]
+ public async Task CreateLinks([FromBody] LinoRequest request)
+ {
+ if (string.IsNullOrWhiteSpace(request.Query))
+ {
+ return BadRequest("Query is required");
+ }
+
+ try
+ {
+ var decoratedLinks = new NamedLinksDecorator(_dbPath, request.Trace);
+ var result = await ProcessLinoQueryAsync(decoratedLinks, request.Query, request.Trace);
+ return Created("", result);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Error: {ex.Message}");
+ }
+ }
+
+ [HttpPut]
+ public async Task UpdateLinks([FromBody] LinoRequest request)
+ {
+ if (string.IsNullOrWhiteSpace(request.Query))
+ {
+ return BadRequest("Query is required");
+ }
+
+ try
+ {
+ var decoratedLinks = new NamedLinksDecorator(_dbPath, request.Trace);
+ var result = await ProcessLinoQueryAsync(decoratedLinks, request.Query, request.Trace);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Error: {ex.Message}");
+ }
+ }
+
+ [HttpDelete]
+ public async Task DeleteLinks([FromBody] LinoRequest request)
+ {
+ if (string.IsNullOrWhiteSpace(request.Query))
+ {
+ return BadRequest("Query is required");
+ }
+
+ try
+ {
+ var decoratedLinks = new NamedLinksDecorator(_dbPath, request.Trace);
+ var result = await ProcessLinoQueryAsync(decoratedLinks, request.Query, request.Trace);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Error: {ex.Message}");
+ }
+ }
+
+ [HttpPost("query")]
+ public async Task ExecuteQuery([FromBody] LinoRequest request)
+ {
+ if (string.IsNullOrWhiteSpace(request.Query))
+ {
+ return BadRequest("Query is required");
+ }
+
+ try
+ {
+ var decoratedLinks = new NamedLinksDecorator(_dbPath, request.Trace);
+ var result = await ProcessLinoQueryAsync(decoratedLinks, request.Query, request.Trace);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Error: {ex.Message}");
+ }
+ }
+
+ private async Task ProcessLinoQueryAsync(NamedLinksDecorator decoratedLinks, string query, bool trace)
+ {
+ var changesList = new List<(DoubletLink Before, DoubletLink After)>();
+ var linksBeforeQuery = GetAllLinksAsString(decoratedLinks);
+
+ var options = new QueryProcessor.Options
+ {
+ Query = query,
+ Trace = trace,
+ ChangesHandler = (beforeLink, afterLink) =>
+ {
+ changesList.Add((new DoubletLink(beforeLink), new DoubletLink(afterLink)));
+ return decoratedLinks.Constants.Continue;
+ }
+ };
+
+ // Execute the query
+ await Task.Run(() => QueryProcessor.ProcessQuery(decoratedLinks, options));
+
+ var linksAfterQuery = GetAllLinksAsString(decoratedLinks);
+ var changes = FormatChanges(decoratedLinks, changesList);
+
+ return new LinoResponse
+ {
+ Query = query,
+ LinksBefore = linksBeforeQuery,
+ LinksAfter = linksAfterQuery,
+ Changes = changes,
+ ChangeCount = changesList.Count
+ };
+ }
+
+ private string GetAllLinksAsString(NamedLinksDecorator links)
+ {
+ var sb = new StringBuilder();
+ var any = links.Constants.Any;
+ var query = new DoubletLink(index: any, source: any, target: any);
+
+ links.Each(query, link =>
+ {
+ var formattedLink = links.Format(link);
+ var namedLink = Namify(links, formattedLink);
+ sb.AppendLine(namedLink);
+ return links.Constants.Continue;
+ });
+
+ return sb.ToString().Trim();
+ }
+
+ private List FormatChanges(NamedLinksDecorator links, List<(DoubletLink Before, DoubletLink After)> changesList)
+ {
+ var changes = new List();
+
+ foreach (var (linkBefore, linkAfter) in changesList)
+ {
+ var beforeText = linkBefore.IsNull() ? "" : links.Format(linkBefore);
+ var afterText = linkAfter.IsNull() ? "" : links.Format(linkAfter);
+ var formattedChange = $"({beforeText}) ({afterText})";
+ changes.Add(Namify(links, formattedChange));
+ }
+
+ return changes;
+ }
+
+ private static string Namify(NamedLinksDecorator namedLinks, string linksNotation)
+ {
+ var numberGlobalRegex = new System.Text.RegularExpressions.Regex(@"\d+");
+ var matches = numberGlobalRegex.Matches(linksNotation);
+ var newLinksNotation = linksNotation;
+ foreach (System.Text.RegularExpressions.Match match in matches)
+ {
+ var number = match.Value;
+ var numberLink = uint.Parse(number);
+ var name = namedLinks.GetName(numberLink);
+ if (name != null)
+ {
+ newLinksNotation = newLinksNotation.Replace(number, name);
+ }
+ }
+ return newLinksNotation;
+ }
+ }
+
+ public class LinoRequest
+ {
+ public string Query { get; set; } = "";
+ public bool Trace { get; set; } = false;
+ }
+
+ public class LinoResponse
+ {
+ public string Query { get; set; } = "";
+ public string LinksBefore { get; set; } = "";
+ public string LinksAfter { get; set; } = "";
+ public List Changes { get; set; } = new();
+ public int ChangeCount { get; set; } = 0;
+ }
+}
\ No newline at end of file
diff --git a/Foundation.Data.Doublets.Cli/Program.cs b/Foundation.Data.Doublets.Cli/Program.cs
index 1f9bfed..4892a26 100644
--- a/Foundation.Data.Doublets.Cli/Program.cs
+++ b/Foundation.Data.Doublets.Cli/Program.cs
@@ -3,6 +3,10 @@
using Platform.Data.Doublets;
using Platform.Data.Doublets.Memory.United.Generic;
using Platform.Protocols.Lino;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.Builder;
using static Foundation.Data.Doublets.Cli.ChangesSimplifier;
using DoubletLink = Platform.Data.Doublets.Link;
@@ -70,6 +74,30 @@
afterOption.AddAlias("--links");
afterOption.AddAlias("-a");
+// Server command options
+var portOption = new Option(
+ name: "--port",
+ description: "Port for the REST API server",
+ getDefaultValue: () => 5000
+);
+portOption.AddAlias("-p");
+
+var hostOption = new Option(
+ name: "--host",
+ description: "Host address for the REST API server",
+ getDefaultValue: () => "localhost"
+);
+hostOption.AddAlias("-h");
+
+// Create server command
+var serverCommand = new Command("serve", "Start REST API server")
+{
+ dbOption,
+ portOption,
+ hostOption,
+ traceOption
+};
+
var rootCommand = new RootCommand("LiNo CLI Tool for managing links data store")
{
dbOption,
@@ -82,6 +110,47 @@
afterOption
};
+rootCommand.AddCommand(serverCommand);
+
+// Server command handler
+serverCommand.SetHandler(
+ async (string db, int port, string host, bool trace) =>
+ {
+ Console.WriteLine($"Starting LINO REST API server on {host}:{port}");
+ Console.WriteLine($"Database: {db}");
+ Console.WriteLine($"Trace mode: {trace}");
+
+ var builder = WebApplication.CreateBuilder();
+
+ // Configure services
+ builder.Services.AddControllers();
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddSwaggerGen();
+
+ // Configure database path
+ builder.Configuration["Database:Path"] = db;
+
+ // Configure web server
+ builder.WebHost.UseUrls($"http://{host}:{port}");
+
+ var app = builder.Build();
+
+ // Configure pipeline
+ if (app.Environment.IsDevelopment())
+ {
+ app.UseSwagger();
+ app.UseSwaggerUI();
+ }
+
+ app.MapControllers();
+
+ Console.WriteLine($"Server started! Visit http://{host}:{port}/swagger for API documentation");
+
+ await app.RunAsync();
+ },
+ dbOption, portOption, hostOption, traceOption
+);
+
rootCommand.SetHandler(
(string db, string queryOptionValue, string queryArgumentValue, bool trace, uint? structure, bool before, bool changes, bool after) =>
{
diff --git a/README.md b/README.md
index 9399258..2e75708 100644
--- a/README.md
+++ b/README.md
@@ -215,6 +215,61 @@ clink '((($index: $source $target)) (($index: $target $source)))' --changes --af
clink '((1: 2 1) (2: 1 2)) ()' --changes --after
```
+## REST API Server
+
+The CLI tool can also run as a REST API server that accepts LINO queries instead of JSON.
+
+### Start the server
+
+```bash
+clink serve --port 5000 --host localhost
+```
+
+Or with custom database:
+```bash
+clink serve --db custom.links --port 8080 --trace
+```
+
+### API Endpoints
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | `/api/links` | Get all links |
+| POST | `/api/links` | Create links |
+| PUT | `/api/links` | Update links |
+| DELETE | `/api/links` | Delete links |
+| POST | `/api/links/query` | Execute arbitrary LINO query |
+
+### Examples
+
+**Get all links:**
+```bash
+curl -X GET http://localhost:5000/api/links
+```
+
+**Create links:**
+```bash
+curl -X POST http://localhost:5000/api/links \
+ -H "Content-Type: application/json" \
+ -d '{"query": "() ((1 1) (2 2))"}'
+```
+
+**Update links:**
+```bash
+curl -X PUT http://localhost:5000/api/links \
+ -H "Content-Type: application/json" \
+ -d '{"query": "((1: 1 1)) ((1: 1 2))"}'
+```
+
+**Execute custom query:**
+```bash
+curl -X POST http://localhost:5000/api/links/query \
+ -H "Content-Type: application/json" \
+ -d '{"query": "((($i: $s $t)) (($i: $s $t)))", "trace": true}'
+```
+
+Visit `http://localhost:5000/swagger` for interactive API documentation.
+
## All options and arguments
| Parameter | Type | Default Value | Aliases | Description |
@@ -228,6 +283,13 @@ clink '((1: 2 1) (2: 1 2)) ()' --changes --after
| `--changes` | bool | `false` | `-c` | Print the changes applied by the query |
| `--after` | bool | `false` | `--links`, `-a` | Print the state of the database after applying changes |
+### Server command options
+
+| Parameter | Type | Default Value | Aliases | Description |
+|-----------|------|---------------|---------|-------------|
+| `--port` | int | `5000` | `-p` | Port for the REST API server |
+| `--host` | string | `localhost` | `-h` | Host address for the REST API server |
+
## For developers and debugging
### Execute from root
diff --git a/examples/lino-examples.md b/examples/lino-examples.md
new file mode 100644
index 0000000..e6f4448
--- /dev/null
+++ b/examples/lino-examples.md
@@ -0,0 +1,122 @@
+# LINO REST API Examples
+
+This document provides examples of using the LINO REST API.
+
+## API Endpoints
+
+### GET /api/links
+Get all links from the database.
+
+**Example Request:**
+```bash
+curl -X GET http://localhost:5000/api/links
+```
+
+**Example Response:**
+```json
+{
+ "query": "((($i: $s $t)) (($i: $s $t)))",
+ "linksBefore": "",
+ "linksAfter": "(1: 1 1)\n(2: 2 2)",
+ "changes": [],
+ "changeCount": 0
+}
+```
+
+### POST /api/links
+Create new links using LINO syntax.
+
+**Example Request - Create single link:**
+```bash
+curl -X POST http://localhost:5000/api/links \
+ -H "Content-Type: application/json" \
+ -d '{"query": "() ((1 1))"}'
+```
+
+**Example Request - Create multiple links:**
+```bash
+curl -X POST http://localhost:5000/api/links \
+ -H "Content-Type: application/json" \
+ -d '{"query": "() ((1 1) (2 2))"}'
+```
+
+### PUT /api/links
+Update existing links using LINO syntax.
+
+**Example Request - Update link target:**
+```bash
+curl -X PUT http://localhost:5000/api/links \
+ -H "Content-Type: application/json" \
+ -d '{"query": "((1: 1 1)) ((1: 1 2))"}'
+```
+
+### DELETE /api/links
+Delete links using LINO syntax.
+
+**Example Request - Delete specific link:**
+```bash
+curl -X DELETE http://localhost:5000/api/links \
+ -H "Content-Type: application/json" \
+ -d '{"query": "((1 2)) ()"}'
+```
+
+**Example Request - Delete all links:**
+```bash
+curl -X DELETE http://localhost:5000/api/links \
+ -H "Content-Type: application/json" \
+ -d '{"query": "((* *)) ()"}'
+```
+
+### POST /api/links/query
+Execute arbitrary LINO query.
+
+**Example Request - Read with variables:**
+```bash
+curl -X POST http://localhost:5000/api/links/query \
+ -H "Content-Type: application/json" \
+ -d '{"query": "((($index: $source $target)) (($index: $target $source)))"}'
+```
+
+**Example Request - With trace enabled:**
+```bash
+curl -X POST http://localhost:5000/api/links/query \
+ -H "Content-Type: application/json" \
+ -d '{"query": "((($i: $s $t)) (($i: $s $t)))", "trace": true}'
+```
+
+## LINO Syntax Reference
+
+### Basic Link Format
+- `(source target)` - Link with source and target, auto-assigned index
+- `(index: source target)` - Link with specific index, source, and target
+
+### Variables
+- `$i` or `$index` - Variable for index
+- `$s` or `$source` - Variable for source
+- `$t` or `$target` - Variable for target
+
+### Wildcards
+- `*` - Matches any value
+- `((* *))` - Matches all links
+
+### Query Structure
+LINO queries have two parts:
+1. **Matching pattern** - What to find
+2. **Substitution pattern** - What to replace it with
+
+Format: `((matching pattern)) ((substitution pattern))`
+
+### Examples
+- `() ((1 1))` - Create link with source=1, target=1
+- `((1: 1 1)) ((1: 1 2))` - Update link 1 to have target=2
+- `((1 2)) ()` - Delete link with source=1, target=2
+- `((($i: $s $t)) (($i: $s $t)))` - Read all links (no change)
+
+## Response Format
+
+All endpoints return a JSON response with:
+- `query` - The LINO query that was executed
+- `linksBefore` - State of database before query (if applicable)
+- `linksAfter` - State of database after query
+- `changes` - List of changes made
+- `changeCount` - Number of changes made
\ No newline at end of file
diff --git a/examples/test-rest-api.sh b/examples/test-rest-api.sh
new file mode 100755
index 0000000..97b4f7d
--- /dev/null
+++ b/examples/test-rest-api.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+# Test script for LINO REST API
+# This script tests the REST API functionality with various LINO queries
+
+BASE_URL="http://localhost:5000"
+SERVER_PID=""
+
+# Function to start the server
+start_server() {
+ echo "Starting LINO REST API server..."
+ dotnet run --project ../Foundation.Data.Doublets.Cli -- serve --port 5000 > server.log 2>&1 &
+ SERVER_PID=$!
+ echo "Server started with PID: $SERVER_PID"
+ sleep 5 # Give server time to start
+}
+
+# Function to stop the server
+stop_server() {
+ if [ ! -z "$SERVER_PID" ]; then
+ echo "Stopping server with PID: $SERVER_PID"
+ kill $SERVER_PID 2>/dev/null || true
+ wait $SERVER_PID 2>/dev/null || true
+ fi
+}
+
+# Function to test API endpoint
+test_api() {
+ local method="$1"
+ local endpoint="$2"
+ local data="$3"
+ local description="$4"
+
+ echo "Testing: $description"
+ echo "Method: $method, Endpoint: $endpoint"
+
+ if [ "$method" = "GET" ]; then
+ response=$(curl -s -w "%{http_code}" "$BASE_URL$endpoint")
+ else
+ response=$(curl -s -w "%{http_code}" -X "$method" -H "Content-Type: application/json" -d "$data" "$BASE_URL$endpoint")
+ fi
+
+ http_code="${response: -3}"
+ body="${response%???}"
+
+ echo "HTTP Code: $http_code"
+ echo "Response: $body"
+ echo "---"
+}
+
+# Cleanup function
+cleanup() {
+ stop_server
+ rm -f server.log
+}
+
+# Set trap for cleanup
+trap cleanup EXIT
+
+# Start the server
+start_server
+
+# Test 1: Get all links (empty database)
+test_api "GET" "/api/links" "" "Get all links from empty database"
+
+# Test 2: Create links
+test_api "POST" "/api/links" '{"query":"() ((1 1) (2 2))"}' "Create two links"
+
+# Test 3: Get all links (should show created links)
+test_api "GET" "/api/links" "" "Get all links after creation"
+
+# Test 4: Update links
+test_api "PUT" "/api/links" '{"query":"((1: 1 1)) ((1: 1 2))"}' "Update first link"
+
+# Test 5: Execute arbitrary query
+test_api "POST" "/api/links/query" '{"query":"((($i: $s $t)) (($i: $s $t)))", "trace": true}' "Execute read query with trace"
+
+# Test 6: Delete links
+test_api "DELETE" "/api/links" '{"query":"((1 2)) ()"}' "Delete link"
+
+echo "All tests completed!"
\ No newline at end of file