diff --git a/README.md b/README.md index a85c2c5..be958a1 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,18 @@ Add the Lexicala configuration to your `appsettings.json`: ```json { "Lexicala": { - "ApiKey": "your-rapidapi-key-here" + "ApiKey": "your-rapidapi-key-here", + "UseLiteEndpoints": false } } ``` +Set `UseLiteEndpoints` to `true` if your subscription only allows Lite entry/sense endpoints. When enabled, the client automatically uses: + +- `/search-entries-lite` instead of `/search-entries` +- `/entries-lite/{entryId}` instead of `/entries/{entryId}` +- `/senses-lite/{senseId}` instead of `/senses/{senseId}` + ### 3. Register Services In your `Program.cs` (for .NET 6+): @@ -219,7 +226,8 @@ The repository includes an ASP.NET Core minimal Web API demo host with Swagger U ```json { "Lexicala": { - "ApiKey": "your-rapidapi-key-here" + "ApiKey": "your-rapidapi-key-here", + "UseLiteEndpoints": false } } ``` @@ -242,16 +250,37 @@ Available endpoints: - `GET /languages` - Get available languages - `GET /search` - Basic search - `GET /search-entries` - Basic search with full entries +- `GET /search-entries-lite` - Basic search with full entries in lite mode (`UseLiteEndpoints=true`) - `GET /search-rdf` - Basic search in RDF/JSON-LD format - `GET /search-definitions` - Free-text search in definitions - `GET /fluky-search` - Random word discovery -- `GET /entry/{entryId}` - Get dictionary entry by ID -- `GET /sense/{senseId}` - Get sense by ID +- `GET /entries/{entryId}` - Get dictionary entry by ID +- `GET /entries-lite/{entryId}` - Get dictionary entry by ID in lite mode (`UseLiteEndpoints=true`) +- `GET /senses/{senseId}` - Get sense by ID +- `GET /senses-lite/{senseId}` - Get sense by ID in lite mode (`UseLiteEndpoints=true`) - `GET /rdf/{entryId}` - Get entry in RDF/JSON-LD format - `POST /search-advanced` - Advanced search - `POST /search-entries-advanced` - Advanced search with full entries - `POST /search-rdf-advanced` - Advanced search in RDF/JSON-LD format +Missing endpoints compared to Rapid Api test console / Lexicala MCP tooling - these endpoints are NOT listed in the official documentation!: + +- `GET /abbreviations` +- `GET /reverse-abbreviations` +- `GET /antonyms` +- `GET /definitions` +- `GET /examples` +- `GET /frequencies` +- `GET /phrases` +- `GET /pronunciations` +- `GET /registers` +- `GET /semantic-categories` +- `GET /subcategorizations` +- `GET /synonyms` +- `GET /translate-to` +- `GET /translate-example` +- `GET /translate-phrase` + For React frontend development, CORS is enabled for: - `http://localhost:3000` @@ -312,6 +341,16 @@ For complete game documentation, features, and tips, see the [Sense Sprint READM The legacy `source/Lexicala.NET.sln` file has been removed in favor of `source/Lexicala.NET.slnx`. +## Supported API Values + +The library validates and supports these commonly used API parameter values: + +- `source` values for `FlukySearchAsync` and `AdvancedSearch*Async`: `global`, `password`, `random`, `multigloss` +- Language parameters (`language`, `sourceLanguage`) must be 2-character language codes +- `AdvancedSearchRequest.Page` accepts values up to `1000` +- `AdvancedSearchRequest.Sample` accepts values up to `1000` +- `AdvancedSearchRequest.PageLength` accepts values between `1` and `30` (default `10`) + ## API Coverage The library implements the following Lexicala API endpoints: @@ -325,6 +364,7 @@ The library implements the following Lexicala API endpoints: - `/search` - Basic search - `/search-entries` - Search with full entries +- `/search-entries-lite` - Search with full entries in lite mode (`UseLiteEndpoints=true`) - `/search-rdf` - Search in RDF/JSON-LD format - `/search-definitions` - Free-text search in definitions - `/fluky-search` - Random word discovery @@ -338,7 +378,9 @@ The library implements the following Lexicala API endpoints: **Entry and Sense Endpoints** - `/entries` - Get entry details by ID +- `/entries-lite` - Get entry details by ID in lite mode (`UseLiteEndpoints=true`) - `/senses` - Get sense details by ID +- `/senses-lite` - Get sense details by ID in lite mode (`UseLiteEndpoints=true`) - `/rdf` - Get entry in RDF/JSON-LD format For complete API documentation, visit the [Lexicala API Documentation](https://api.lexicala.com/documentation). diff --git a/source/Demo/Lexicala.NET.Demo.Api/Program.cs b/source/Demo/Lexicala.NET.Demo.Api/Program.cs index 271fd58..93231ad 100644 --- a/source/Demo/Lexicala.NET.Demo.Api/Program.cs +++ b/source/Demo/Lexicala.NET.Demo.Api/Program.cs @@ -86,6 +86,13 @@ await client.BasicSearchAsync(text, language, etag, cancellationToken)) .WithTags("Search") .WithSummary("Search by headword") .WithDescription("Search for entries in the Global source by headword. Returns partial lexical information. Use /entry/{entryId} to retrieve a full entry."); + + app.MapGet("/search-with-parser", async (ILexicalaSearchParser parser, string text, string language) => + await parser.SearchAsync(text, language)) + .WithName("ParserSearch") + .WithTags("Search") + .WithSummary("Search using ILexicalaSearchParser instead of ILexicalaClient") + .WithDescription("Search for entries in the Global source by headword. Returns partial lexical information"); app.MapGet("/search-entries", async (ILexicalaClient client, string text, string language, string? etag, CancellationToken cancellationToken) => await client.SearchEntriesAsync(text, language, etag, cancellationToken)) diff --git a/source/Lexicala.NET.Tests/LexicalaClientEntryTests.cs b/source/Lexicala.NET.Tests/LexicalaClientEntryTests.cs index 9739565..c746c4d 100644 --- a/source/Lexicala.NET.Tests/LexicalaClientEntryTests.cs +++ b/source/Lexicala.NET.Tests/LexicalaClientEntryTests.cs @@ -135,6 +135,24 @@ public async Task LexicalaClient_SearchEntries_Basic_IncludesSearchEntriesEndpoi ItExpr.IsAny()); } + [TestMethod] + public async Task LexicalaClient_SearchEntries_Basic_UsesLiteEndpoint_WhenConfigured() + { + InitializeClient(useLiteEndpoints: true); + string response = await LoadResponseFromFile("Entry_EN_DE00009032.json"); + + HandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(SetupOkResponseMessage($"[{response}]")); + + var result = await Client.SearchEntriesAsync("text", "xx"); + + result.ShouldNotBeNull(); + HandlerMock.Protected().Verify("SendAsync", Times.Once(), + ItExpr.Is(req => req.RequestUri.ToString() == "http://www.tempuri.org/search-entries-lite?language=xx&text=text"), + ItExpr.IsAny()); + } + [TestMethod] public async Task LexicalaClient_AdvancedSearchEntries_IncludesSearchEntriesEndpoint() { @@ -159,6 +177,31 @@ public async Task LexicalaClient_AdvancedSearchEntries_IncludesSearchEntriesEndp ItExpr.IsAny()); } + [TestMethod] + public async Task LexicalaClient_AdvancedSearchEntries_UsesLiteEndpoint_WhenConfigured() + { + InitializeClient(useLiteEndpoints: true); + string response = await LoadResponseFromFile("Entry_EN_DE00009032.json"); + + HandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(SetupOkResponseMessage($"[{response}]")); + + var searchRequest = new AdvancedSearchRequest + { + Language = "xx", + SearchText = "text", + Synonyms = true + }; + + var result = await Client.AdvancedSearchEntriesAsync(searchRequest); + + result.ShouldNotBeNull(); + HandlerMock.Protected().Verify("SendAsync", Times.Once(), + ItExpr.Is(req => req.RequestUri.ToString() == "http://www.tempuri.org/search-entries-lite?language=xx&text=text&source=global&synonyms=true"), + ItExpr.IsAny()); + } + [TestMethod] public async Task LexicalaClient_GetEntryAsync_EncodesEntryIdInPath() { @@ -175,6 +218,23 @@ public async Task LexicalaClient_GetEntryAsync_EncodesEntryIdInPath() ItExpr.IsAny()); } + [TestMethod] + public async Task LexicalaClient_GetEntryAsync_UsesLitePath_WhenConfigured() + { + InitializeClient(useLiteEndpoints: true); + const string response = "{\"id\":\"id\",\"headword\":[],\"senses\":[],\"related_entries\":[]}"; + + HandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(SetupOkResponseMessage(response)); + + await Client.GetEntryAsync("EN_DE/unsafe"); + + HandlerMock.Protected().Verify("SendAsync", Times.Once(), + ItExpr.Is(req => req.RequestUri.ToString() == "http://www.tempuri.org/entries-lite/EN_DE%2Funsafe"), + ItExpr.IsAny()); + } + [TestMethod] public async Task LexicalaClient_GetSenseAsync_EncodesSenseIdInPath() { @@ -191,6 +251,23 @@ public async Task LexicalaClient_GetSenseAsync_EncodesSenseIdInPath() ItExpr.IsAny()); } + [TestMethod] + public async Task LexicalaClient_GetSenseAsync_UsesLitePath_WhenConfigured() + { + InitializeClient(useLiteEndpoints: true); + const string response = "{\"id\":\"sense-id\"}"; + + HandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(SetupOkResponseMessage(response)); + + await Client.GetSenseAsync("EN_SE/unsafe"); + + HandlerMock.Protected().Verify("SendAsync", Times.Once(), + ItExpr.Is(req => req.RequestUri.ToString() == "http://www.tempuri.org/senses-lite/EN_SE%2Funsafe"), + ItExpr.IsAny()); + } + [TestMethod] public void Headword_PartOfSpeeches_ReturnsEmpty_WhenPosIsAbsent() { diff --git a/source/Lexicala.NET.Tests/LexicalaClientTestBase.cs b/source/Lexicala.NET.Tests/LexicalaClientTestBase.cs index 17f2c07..91f41bd 100644 --- a/source/Lexicala.NET.Tests/LexicalaClientTestBase.cs +++ b/source/Lexicala.NET.Tests/LexicalaClientTestBase.cs @@ -24,10 +24,26 @@ public void Initialize() BaseAddress = new Uri("http://www.tempuri.org") }; + Client = CreateClient(httpClient, useLiteEndpoints: false); + } + + protected void InitializeClient(bool useLiteEndpoints) + { + var httpClient = new HttpClient(HandlerMock.Object) + { + BaseAddress = new Uri("http://www.tempuri.org") + }; + + Client = CreateClient(httpClient, useLiteEndpoints); + } + + private static LexicalaClient CreateClient(HttpClient httpClient, bool useLiteEndpoints) + { var mocker = new AutoMocker(MockBehavior.Loose); mocker.Use(httpClient); + mocker.Use(new LexicalaConfig("test-key", useLiteEndpoints)); - Client = mocker.CreateInstance(); + return mocker.CreateInstance(); } protected static HttpResponseMessage SetupOkResponseMessage(string content) diff --git a/source/Lexicala.NET/Constants.cs b/source/Lexicala.NET/Constants.cs index 8d1ba15..e24ffff 100644 --- a/source/Lexicala.NET/Constants.cs +++ b/source/Lexicala.NET/Constants.cs @@ -20,11 +20,21 @@ internal static class Constants /// internal const string SearchEntries = "/search-entries"; + /// + /// Search entries lite endpoint path. + /// + internal const string SearchEntriesLite = "/search-entries-lite"; + /// /// Entries endpoint path. /// internal const string Entries = "/entries"; + /// + /// Entries lite endpoint path. + /// + internal const string EntriesLite = "/entries-lite"; + /// /// Search RDF endpoint path. /// @@ -45,6 +55,11 @@ internal static class Constants /// internal const string Senses = "/senses"; + /// + /// Senses lite endpoint path. + /// + internal const string SensesLite = "/senses-lite"; + /// /// Search definitions endpoint path. /// diff --git a/source/Lexicala.NET/DependencyRegistration.cs b/source/Lexicala.NET/DependencyRegistration.cs index 27d8840..07bbc75 100644 --- a/source/Lexicala.NET/DependencyRegistration.cs +++ b/source/Lexicala.NET/DependencyRegistration.cs @@ -57,6 +57,7 @@ public static IServiceCollection RegisterLexicala(this IServiceCollection servic }) .AddPolicyHandler(CreateRetryPolicy()); + services.AddSingleton(config); services.AddMemoryCache(); services.AddSingleton(); diff --git a/source/Lexicala.NET/Lexicala.NET.csproj b/source/Lexicala.NET/Lexicala.NET.csproj index 48c21b0..debe416 100644 --- a/source/Lexicala.NET/Lexicala.NET.csproj +++ b/source/Lexicala.NET/Lexicala.NET.csproj @@ -17,17 +17,17 @@ A .NET client for the Lexicala api. See readme file on project page for further details. - 3.0.0 + 3.1.0 latest - - - - + + + + diff --git a/source/Lexicala.NET/LexicalaClient.cs b/source/Lexicala.NET/LexicalaClient.cs index 2fb7b0c..01cc4a5 100644 --- a/source/Lexicala.NET/LexicalaClient.cs +++ b/source/Lexicala.NET/LexicalaClient.cs @@ -22,6 +22,11 @@ public class LexicalaClient : ILexicalaClient { private readonly HttpClient _httpClient; private readonly ILogger _logger; + private readonly bool _useLiteEndpoints; + + private string SearchEntriesEndpoint => _useLiteEndpoints ? Constants.SearchEntriesLite : Constants.SearchEntries; + private string EntriesEndpoint => _useLiteEndpoints ? Constants.EntriesLite : Constants.Entries; + private string SensesEndpoint => _useLiteEndpoints ? Constants.SensesLite : Constants.Senses; /// /// Creates a new instance of the class. @@ -30,9 +35,18 @@ public class LexicalaClient : ILexicalaClient /// This class should not be instantiated directly, but registered as implementation of the interface in the dependency injection framework. /// public LexicalaClient(HttpClient httpClient, ILogger logger) + : this(httpClient, logger, new LexicalaConfig()) + { + } + + /// + /// Creates a new instance of the class with endpoint mode configuration. + /// + public LexicalaClient(HttpClient httpClient, ILogger logger, LexicalaConfig config) { _httpClient = httpClient; _logger = logger; + _useLiteEndpoints = config?.UseLiteEndpoints ?? false; } /// @@ -81,7 +95,7 @@ public Task> SearchEntriesAsync(string searchText, string sou ValidateLanguageCode(sourceLanguage, nameof(sourceLanguage)); ArgumentException.ThrowIfNullOrEmpty(searchText, nameof(searchText)); - var queryString = $"{Constants.SearchEntries}?language={Uri.EscapeDataString(sourceLanguage)}&text={Uri.EscapeDataString(searchText)}"; + var queryString = $"{SearchEntriesEndpoint}?language={Uri.EscapeDataString(sourceLanguage)}&text={Uri.EscapeDataString(searchText)}"; return ExecuteSearchEntries(queryString, etag, cancellationToken); } @@ -90,7 +104,7 @@ public Task> AdvancedSearchEntriesAsync(AdvancedSearchRequest { ValidateSearchRequest(searchRequest); - var queryString = BuildAdvancedSearchQueryString(Constants.SearchEntries, searchRequest); + var queryString = BuildAdvancedSearchQueryString(SearchEntriesEndpoint, searchRequest); return ExecuteSearchEntries(queryString, searchRequest.ETag, cancellationToken); } @@ -124,7 +138,7 @@ public Task GetRdfAsync(string entryId, string etag = null, Cancellation public async Task GetEntryAsync(string entryId, string etag = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(entryId, nameof(entryId)); - using var response = await ExecuteRequestAsync(HttpMethod.Get, $"{Constants.Entries}/{Uri.EscapeDataString(entryId)}", etag, cancellationToken); + using var response = await ExecuteRequestAsync(HttpMethod.Get, $"{EntriesEndpoint}/{Uri.EscapeDataString(entryId)}", etag, cancellationToken); var content = await response.Content.ReadAsStringAsync(cancellationToken); var responseObject = JsonSerializer.Deserialize(content, JsonSerializerDefaults.Options); responseObject.Metadata = GetResponseMetadata(response.Headers); @@ -135,7 +149,7 @@ public async Task GetEntryAsync(string entryId, string etag = null, Cance public async Task GetSenseAsync(string senseId, string etag = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(senseId, nameof(senseId)); - using var response = await ExecuteRequestAsync(HttpMethod.Get, $"{Constants.Senses}/{Uri.EscapeDataString(senseId)}", etag, cancellationToken); + using var response = await ExecuteRequestAsync(HttpMethod.Get, $"{SensesEndpoint}/{Uri.EscapeDataString(senseId)}", etag, cancellationToken); var content = await response.Content.ReadAsStringAsync(cancellationToken); var responseObject = JsonSerializer.Deserialize(content, JsonSerializerDefaults.Options); responseObject.Metadata = GetResponseMetadata(response.Headers); diff --git a/source/Lexicala.NET/LexicalaConfig.cs b/source/Lexicala.NET/LexicalaConfig.cs index fa7eb92..5bebed6 100644 --- a/source/Lexicala.NET/LexicalaConfig.cs +++ b/source/Lexicala.NET/LexicalaConfig.cs @@ -43,10 +43,24 @@ public LexicalaConfig(string apiKey) ApiKey = apiKey; } + /// + /// Creates a new instance of the class, with specified API key and endpoint mode. + /// + public LexicalaConfig(string apiKey, bool useLiteEndpoints) + { + ApiKey = apiKey; + UseLiteEndpoints = useLiteEndpoints; + } + /// /// The RapidAPI Api key. /// public string ApiKey { get; set; } + /// + /// When true, uses the Lite API variants for entry and sense retrieval/search where available. + /// + public bool UseLiteEndpoints { get; set; } + } } \ No newline at end of file