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