diff --git a/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs b/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs new file mode 100644 index 0000000..d34547c --- /dev/null +++ b/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs @@ -0,0 +1,245 @@ +using GuardianClient.Options.Search; +using Shouldly; + +namespace GuardianClient.Tests; + +[TestClass] +public class GetItemAsyncTests : TestBase +{ + [TestMethod] + public async Task GetItemAsync_WithValidId_ReturnsItem() + { + // First get a valid item ID from search + var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "technology", + PageOptions = new GuardianApiContentPageOptions { PageSize = 1 } + }); + searchResult.ShouldNotBeNull("Search should return results"); + searchResult.Results.Count.ShouldBe(1, "Should return exactly one result"); + + var contentItem = searchResult.Results.First(); + var itemId = contentItem.Id; + + // Now get the specific item + var singleItemResult = await ApiClient.GetItemAsync(itemId, + new GuardianApiContentAdditionalInformationOptions { ShowFields = [GuardianApiContentShowFieldsOption.Body] }); + + singleItemResult.ShouldNotBeNull("GetItem result should not be null"); + singleItemResult.Status.ShouldBe("ok", "API response status should be 'ok'"); + singleItemResult.Content.ShouldNotBeNull("Content should not be null"); + singleItemResult.Content.Id.ShouldBe(itemId, "Returned content ID should match requested ID"); + singleItemResult.Content.WebTitle.ShouldNotBeNullOrEmpty("Content should have a title"); + singleItemResult.Content.Fields.ShouldNotBeNull("Fields should be populated when ShowFields is specified"); + + Console.WriteLine($"Retrieved item: {singleItemResult.Content.WebTitle}"); + Console.WriteLine($"Item ID: {singleItemResult.Content.Id}"); + Console.WriteLine($"Published: {singleItemResult.Content.WebPublicationDate}"); + + if (!string.IsNullOrEmpty(singleItemResult.Content.Fields.Body)) + { + Console.WriteLine($"Body length: {singleItemResult.Content.Fields.Body.Length} characters"); + Console.WriteLine( + $"Body preview: {singleItemResult.Content.Fields.Body[..Math.Min(200, singleItemResult.Content.Fields.Body.Length)]}..."); + } + } + + [TestMethod] + public async Task GetItemAsync_WithInvalidId_ThrowsException() + { + var invalidId = "invalid/article/id/that/does/not/exist"; + + var exception = await Should.ThrowAsync(async () => + { + await ApiClient.GetItemAsync(invalidId); + }); + + Console.WriteLine($"Expected exception for invalid ID: {exception.Message}"); + } + + [TestMethod] + public async Task GetItemAsync_WithNullId_ThrowsArgumentException() + { + var exception = await Should.ThrowAsync(async () => + { + await ApiClient.GetItemAsync(null!); + }); + + Console.WriteLine($"Expected exception for null ID: {exception.Message}"); + } + + [TestMethod] + public async Task GetItemAsync_WithEmptyId_ThrowsArgumentException() + { + var exception = await Should.ThrowAsync(async () => + { + await ApiClient.GetItemAsync(""); + }); + + Console.WriteLine($"Expected exception for empty ID: {exception.Message}"); + } + + [TestMethod] + public async Task GetItemAsync_WithShowFields_ReturnsEnhancedContent() + { + // Get a valid item ID + var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "politics", + PageOptions = new GuardianApiContentPageOptions { PageSize = 1 } + }); + + var itemId = searchResult.Results.First().Id; + + // Request with multiple fields + var result = await ApiClient.GetItemAsync(itemId, + new GuardianApiContentAdditionalInformationOptions + { + ShowFields = + [ + GuardianApiContentShowFieldsOption.Headline, + GuardianApiContentShowFieldsOption.Body, + GuardianApiContentShowFieldsOption.Byline, + GuardianApiContentShowFieldsOption.Thumbnail + ] + }); + + result.ShouldNotBeNull(); + result.Content.Fields.ShouldNotBeNull("Fields should be populated"); + result.Content.Fields.Headline.ShouldNotBeNullOrEmpty("Headline should be populated"); + + Console.WriteLine($"Enhanced content for: {result.Content.WebTitle}"); + Console.WriteLine($"Headline: {result.Content.Fields.Headline}"); + Console.WriteLine($"Has body: {!string.IsNullOrEmpty(result.Content.Fields.Body)}"); + Console.WriteLine($"Has byline: {!string.IsNullOrEmpty(result.Content.Fields.Byline)}"); + } + + [TestMethod] + public async Task GetItemAsync_WithShowTags_ReturnsContentWithTags() + { + // Get a valid item ID + var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "sport", + PageOptions = new GuardianApiContentPageOptions { PageSize = 1 } + }); + + var itemId = searchResult.Results.First().Id; + + // Request with tags + var result = await ApiClient.GetItemAsync(itemId, + new GuardianApiContentAdditionalInformationOptions + { + ShowTags = [GuardianApiContentShowTagsOption.Keyword, GuardianApiContentShowTagsOption.Tone, GuardianApiContentShowTagsOption.Type] + }); + + result.ShouldNotBeNull(); + result.Content.Tags.ShouldNotBeNull("Tags should be populated"); + result.Content.Tags.Count.ShouldBeGreaterThan(0, "Should have at least one tag"); + + Console.WriteLine($"Content '{result.Content.WebTitle}' has {result.Content.Tags.Count} tags:"); + foreach (var tag in result.Content.Tags.Take(5)) // Show first 5 tags + { + Console.WriteLine($" - {tag.WebTitle} ({tag.Type})"); + } + } + + [TestMethod] + public async Task GetItemAsync_WithShowElements_ReturnsContentWithElements() + { + // Get a valid item ID + var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "music", + PageOptions = new GuardianApiContentPageOptions { PageSize = 1 } + }); + + var itemId = searchResult.Results.First().Id; + + // Request with elements + var result = await ApiClient.GetItemAsync(itemId, + new GuardianApiContentAdditionalInformationOptions + { + ShowElements = [GuardianApiContentShowElementsOption.Image, GuardianApiContentShowElementsOption.Video] + }); + + result.ShouldNotBeNull(); + // Elements might be null if the article has no media, so we don't assert their presence + + Console.WriteLine($"Content '{result.Content.WebTitle}' elements:"); + if (result.Content.Elements != null) + { + Console.WriteLine($" Has {result.Content.Elements.Count} media elements"); + } + else + { + Console.WriteLine(" No media elements found"); + } + } + + [TestMethod] + public async Task GetItemAsync_WithShowBlocks_ReturnsContentWithBlocks() + { + // Get a valid item ID + var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "news", + PageOptions = new GuardianApiContentPageOptions { PageSize = 1 } + }); + + var itemId = searchResult.Results.First().Id; + + // Request with blocks using string array (as it's still string-based) + var result = await ApiClient.GetItemAsync(itemId, + new GuardianApiContentAdditionalInformationOptions + { + ShowBlocks = ["main", "body"] + }); + + result.ShouldNotBeNull(); + // Blocks might be null depending on content type + + Console.WriteLine($"Content '{result.Content.WebTitle}' blocks:"); + if (result.Content.Blocks != null) + { + Console.WriteLine($" Requested main and body blocks"); + } + else + { + Console.WriteLine(" No blocks found"); + } + } + + [TestMethod] + public async Task GetItemAsync_WithAllOptions_ReturnsFullyEnhancedContent() + { + // Get a valid item ID + var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "culture", + PageOptions = new GuardianApiContentPageOptions { PageSize = 1 } + }); + + var itemId = searchResult.Results.First().Id; + + // Request with all enhancement options + var result = await ApiClient.GetItemAsync(itemId, + new GuardianApiContentAdditionalInformationOptions + { + ShowFields = [GuardianApiContentShowFieldsOption.All], + ShowTags = [GuardianApiContentShowTagsOption.All], + ShowElements = [GuardianApiContentShowElementsOption.All], + ShowBlocks = ["all"] + }); + + result.ShouldNotBeNull(); + result.Content.Fields.ShouldNotBeNull("All fields should be populated"); + result.Content.Tags.ShouldNotBeNull("All tags should be populated"); + + Console.WriteLine($"Fully enhanced content: {result.Content.WebTitle}"); + Console.WriteLine($" Fields populated: Yes"); + Console.WriteLine($" Tags count: {result.Content.Tags.Count}"); + Console.WriteLine($" Elements: {(result.Content.Elements?.Count ?? 0)}"); + Console.WriteLine($" Has blocks: {result.Content.Blocks != null}"); + } +} diff --git a/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs b/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs new file mode 100644 index 0000000..22bcba9 --- /dev/null +++ b/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs @@ -0,0 +1,172 @@ +using GuardianClient.Options.Search; +using Shouldly; + +namespace GuardianClient.Tests; + +[TestClass] +public class GuardianApiClientIntegrationTests : TestBase +{ + [TestMethod] + public void ApiClient_ShouldNotBeNull() + { + ApiClient.ShouldNotBeNull("API client should be properly initialized"); + ApiClient.ShouldBeAssignableTo("API client should implement the interface"); + } + + [TestMethod] + public async Task EndToEnd_SearchAndGetItem_WorksTogether() + { + // Search for content + var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "artificial intelligence", + PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }, + FilterOptions = new GuardianApiContentFilterOptions + { + Section = "technology" + } + }); + + searchResult.ShouldNotBeNull("Search should return results"); + searchResult.Results.Count.ShouldBe(1, "Should return exactly one result"); + + // Get the specific item with enhanced information + var contentItem = searchResult.Results.First(); + var detailedResult = await ApiClient.GetItemAsync(contentItem.Id, + new GuardianApiContentAdditionalInformationOptions + { + ShowFields = [GuardianApiContentShowFieldsOption.Headline, GuardianApiContentShowFieldsOption.Body], + ShowTags = [GuardianApiContentShowTagsOption.Keyword] + }); + + detailedResult.ShouldNotBeNull("Detailed result should not be null"); + detailedResult.Content!.Id.ShouldBe(contentItem.Id, "IDs should match"); + detailedResult.Content.WebTitle.ShouldBe(contentItem.WebTitle, "Titles should match"); + detailedResult.Content.Fields.ShouldNotBeNull("Detailed fields should be populated"); + detailedResult.Content.Tags.ShouldNotBeNull("Tags should be populated"); + + Console.WriteLine("=== END-TO-END TEST RESULTS ==="); + Console.WriteLine(); + Console.WriteLine($"SEARCH RESULT:"); + Console.WriteLine($" Title: {contentItem.WebTitle}"); + Console.WriteLine($" Section: {contentItem.SectionName} ({contentItem.SectionId})"); + Console.WriteLine($" Published: {contentItem.WebPublicationDate}"); + Console.WriteLine($" URL: {contentItem.WebUrl}"); + Console.WriteLine(); + + Console.WriteLine($"DETAILED CONTENT:"); + Console.WriteLine($" ID: {detailedResult.Content.Id}"); + Console.WriteLine($" Headline: {detailedResult.Content.Fields.Headline ?? "N/A"}"); + Console.WriteLine($" Tags: {detailedResult.Content.Tags.Count} tags"); + + if (detailedResult.Content.Tags.Any()) + { + Console.WriteLine($" Tag List:"); + foreach (var tag in detailedResult.Content.Tags.Take(10)) // Show first 10 tags + { + Console.WriteLine($" - {tag.WebTitle} ({tag.Type})"); + } + if (detailedResult.Content.Tags.Count > 10) + { + Console.WriteLine($" ... and {detailedResult.Content.Tags.Count - 10} more tags"); + } + } + + Console.WriteLine(); + Console.WriteLine($"FULL ARTICLE BODY:"); + Console.WriteLine("=================="); + if (!string.IsNullOrEmpty(detailedResult.Content.Fields.Body)) + { + Console.WriteLine(detailedResult.Content.Fields.Body); + } + else + { + Console.WriteLine("No body content available"); + } + Console.WriteLine("=================="); + Console.WriteLine(); + } + + [TestMethod] + public async Task SearchWithComplexOptions_ReturnsExpectedResults() + { + // Test a complex search with multiple option types + var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "climate change", + QueryFields = ["body", "headline"], + FilterOptions = new GuardianApiContentFilterOptions + { + Section = "environment" + }, + DateOptions = new GuardianApiContentDateOptions + { + FromDate = new DateOnly(2023, 1, 1) + }, + PageOptions = new GuardianApiContentPageOptions + { + Page = 1, + PageSize = 5 + }, + OrderOptions = new GuardianApiContentOrderOptions + { + OrderBy = GuardianApiContentOrderBy.Relevance + }, + AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions + { + ShowFields = [GuardianApiContentShowFieldsOption.Headline, GuardianApiContentShowFieldsOption.Score], + ShowTags = [GuardianApiContentShowTagsOption.Tone] + } + }); + + result.ShouldNotBeNull("Complex search should return results"); + result.Status.ShouldBe("ok", "API should respond successfully"); + result.Results.Count.ShouldBeLessThanOrEqualTo(5, "Should respect page size"); + + // Verify enhanced data is present + if (result.Results.Any()) + { + var firstItem = result.Results.First(); + firstItem.Fields.ShouldNotBeNull("Enhanced fields should be present"); + firstItem.Tags.ShouldNotBeNull("Tags should be present"); + + Console.WriteLine($"Complex search returned {result.Results.Count} results"); + Console.WriteLine($"First result: {firstItem.WebTitle}"); + Console.WriteLine($"Relevance score: {firstItem.Fields.Score ?? "N/A"}"); + } + } + + [TestMethod] + public async Task TypeSafetyTest_EnumsMapToCorrectApiValues() + { + // This test ensures our enums map to the correct API values + var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "test", + PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }, + AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions + { + ShowFields = + [ + GuardianApiContentShowFieldsOption.Headline, + GuardianApiContentShowFieldsOption.TrailText, + GuardianApiContentShowFieldsOption.ShowInRelatedContent + ], + ShowTags = [GuardianApiContentShowTagsOption.Tone, GuardianApiContentShowTagsOption.Type], + ShowElements = [GuardianApiContentShowElementsOption.Image] + } + }); + + result.ShouldNotBeNull("Type safety test should work"); + result.Status.ShouldBe("ok", "API should accept enum-mapped values"); + + if (result.Results.Any()) + { + var item = result.Results.First(); + Console.WriteLine($"Type safety test passed for: {item.WebTitle}"); + Console.WriteLine($" Fields populated: {item.Fields != null}"); + Console.WriteLine($" Tags populated: {item.Tags != null}"); + Console.WriteLine($" Elements populated: {item.Elements != null}"); + } + } +} diff --git a/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs b/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs deleted file mode 100644 index 357e7c0..0000000 --- a/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace GuardianClient.Tests; - -using Shouldly; - -[TestClass] -public class GuardianApiClientTests : TestBase -{ - [TestMethod] - public async Task SearchAsyncSmokeTest() - { - var result = await ApiClient.SearchAsync("climate change", pageSize: 5); - - result.ShouldNotBeNull("Search result should not be null"); - result.Status.ShouldBe("ok", "API response status should be 'ok'"); - result.Results.Count.ShouldBeGreaterThan(0, "Should return at least one result"); - result.Results.Count.ShouldBeLessThanOrEqualTo(5, "Should not return more than requested page size"); - - var firstItem = result.Results.First(); - firstItem.Id.ShouldNotBeNullOrEmpty("Content item should have an ID"); - firstItem.WebTitle.ShouldNotBeNullOrEmpty("Content item should have a title"); - firstItem.WebUrl.ShouldNotBeNullOrEmpty("Content item should have a web URL"); - firstItem.ApiUrl.ShouldNotBeNullOrEmpty("Content item should have an API URL"); - - Console.WriteLine($"Found {result.Results.Count} articles about climate change"); - Console.WriteLine($"First article: {firstItem.WebTitle}"); - Console.WriteLine($"Published: {firstItem.WebPublicationDate}"); - } - - [TestMethod] - public async Task SearchAsyncWithNoResults() - { - var result = await ApiClient.SearchAsync("xyzabc123nonexistentquery456"); - - result.ShouldNotBeNull("Search result should not be null even with no matches"); - result.Status.ShouldBe("ok", "API response status should be 'ok'"); - result.Results.Count.ShouldBe(0, "Should return zero results for non-existent query"); - } - - [TestMethod] - public async Task GetItemAsyncSmokeTest() - { - var searchResult = await ApiClient.SearchAsync("technology", pageSize: 1); - searchResult.ShouldNotBeNull("Search should return results"); - searchResult.Results.Count.ShouldBe(1, "Should return exactly one result"); - - var contentItem = searchResult.Results.First(); - var itemId = contentItem.Id; - var singleItemResult = await ApiClient.GetItemAsync(itemId, new GuardianApiOptions { ShowFields = ["body"] }); - - singleItemResult.ShouldNotBeNull("GetItem result should not be null"); - singleItemResult.Status.ShouldBe("ok", "API response status should be 'ok'"); - singleItemResult.Content.ShouldNotBeNull("Content should not be null"); - singleItemResult.Content.Id.ShouldBe(itemId, "Returned content ID should match requested ID"); - singleItemResult.Content.WebTitle.ShouldNotBeNullOrEmpty("Content should have a title"); - singleItemResult.Content.Fields.ShouldNotBeNull("Fields should be populated when ShowFields is specified"); - - Console.WriteLine($"Retrieved item: {singleItemResult.Content.WebTitle}"); - Console.WriteLine($"Item ID: {singleItemResult.Content.Id}"); - Console.WriteLine($"Published: {singleItemResult.Content.WebPublicationDate}"); - - if (!string.IsNullOrEmpty(singleItemResult.Content.Fields.Body)) - { - Console.WriteLine($"Body length: {singleItemResult.Content.Fields.Body.Length} characters"); - Console.WriteLine( - $"Body preview: {singleItemResult.Content.Fields.Body[..Math.Min(200, singleItemResult.Content.Fields.Body.Length)]}..."); - } - } -} diff --git a/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs b/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs new file mode 100644 index 0000000..02bfbb4 --- /dev/null +++ b/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs @@ -0,0 +1,144 @@ +using GuardianClient.Options.Search; +using Shouldly; + +namespace GuardianClient.Tests; + +[TestClass] +public class SearchAsyncTests : TestBase +{ + [TestMethod] + public async Task SearchAsync_WithQuery_ReturnsResults() + { + var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "climate change", + PageOptions = new GuardianApiContentPageOptions { PageSize = 5 } + }); + + result.ShouldNotBeNull("Search result should not be null"); + result.Status.ShouldBe("ok", "API response status should be 'ok'"); + result.Results.Count.ShouldBeGreaterThan(0, "Should return at least one result"); + result.Results.Count.ShouldBeLessThanOrEqualTo(5, "Should not return more than requested page size"); + + var firstItem = result.Results.First(); + firstItem.Id.ShouldNotBeNullOrEmpty("Content item should have an ID"); + firstItem.WebTitle.ShouldNotBeNullOrEmpty("Content item should have a title"); + firstItem.WebUrl.ShouldNotBeNullOrEmpty("Content item should have a web URL"); + firstItem.ApiUrl.ShouldNotBeNullOrEmpty("Content item should have an API URL"); + + Console.WriteLine($"Found {result.Results.Count} articles about climate change"); + Console.WriteLine($"First article: {firstItem.WebTitle}"); + Console.WriteLine($"Published: {firstItem.WebPublicationDate}"); + } + + [TestMethod] + public async Task SearchAsync_WithNonExistentQuery_ReturnsNoResults() + { + var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "xyzabc123nonexistentquery456" + }); + + result.ShouldNotBeNull("Search result should not be null even with no matches"); + result.Status.ShouldBe("ok", "API response status should be 'ok'"); + result.Results.Count.ShouldBe(0, "Should return zero results for non-existent query"); + } + + [TestMethod] + public async Task SearchAsync_WithNoOptions_ReturnsDefaultResults() + { + var result = await ApiClient.SearchAsync(); + + result.ShouldNotBeNull("Search result should not be null"); + result.Status.ShouldBe("ok", "API response status should be 'ok'"); + result.Results.Count.ShouldBeGreaterThan(0, "Should return results with default options"); + result.Results.Count.ShouldBeLessThanOrEqualTo(10, "Default page size should be 10 or less"); + + Console.WriteLine($"Default search returned {result.Results.Count} results"); + } + + [TestMethod] + public async Task SearchAsync_WithFilterOptions_ReturnsFilteredResults() + { + var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "technology", + FilterOptions = new GuardianApiContentFilterOptions + { + Section = "technology" + }, + PageOptions = new GuardianApiContentPageOptions { PageSize = 3 } + }); + + result.ShouldNotBeNull("Search result should not be null"); + result.Status.ShouldBe("ok", "API response status should be 'ok'"); + result.Results.Count.ShouldBeGreaterThan(0, "Should return technology results"); + + // Check that results are from technology section + foreach (var item in result.Results) + { + item.SectionId.ShouldBe("technology", "All results should be from technology section"); + } + + Console.WriteLine($"Found {result.Results.Count} technology articles"); + } + + [TestMethod] + public async Task SearchAsync_WithOrderOptions_ReturnsOrderedResults() + { + var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "sports", + OrderOptions = new GuardianApiContentOrderOptions + { + OrderBy = GuardianApiContentOrderBy.Oldest + }, + PageOptions = new GuardianApiContentPageOptions { PageSize = 2 } + }); + + result.ShouldNotBeNull("Search result should not be null"); + result.Status.ShouldBe("ok", "API response status should be 'ok'"); + result.Results.Count.ShouldBeGreaterThan(0, "Should return sports results"); + + // Oldest first means first result should be older than or equal to second + if (result.Results.Count >= 2) + { + var first = result.Results.First(); + var second = result.Results.Skip(1).First(); + + if (first.WebPublicationDate.HasValue && second.WebPublicationDate.HasValue) + { + first.WebPublicationDate.Value.ShouldBeLessThanOrEqualTo(second.WebPublicationDate.Value, + "Results should be ordered oldest first"); + } + } + + Console.WriteLine($"Found {result.Results.Count} sports articles (oldest first)"); + } + + [TestMethod] + public async Task SearchAsync_WithAdditionalInformation_ReturnsEnhancedResults() + { + var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "environment", + PageOptions = new GuardianApiContentPageOptions { PageSize = 2 }, + AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions + { + ShowFields = [GuardianApiContentShowFieldsOption.Headline, GuardianApiContentShowFieldsOption.Thumbnail], + ShowTags = [GuardianApiContentShowTagsOption.Keyword, GuardianApiContentShowTagsOption.Tone] + } + }); + + result.ShouldNotBeNull("Search result should not be null"); + result.Status.ShouldBe("ok", "API response status should be 'ok'"); + result.Results.Count.ShouldBeGreaterThan(0, "Should return environment results"); + + var firstItem = result.Results.First(); + firstItem.Fields.ShouldNotBeNull("Fields should be populated"); + firstItem.Tags.ShouldNotBeNull("Tags should be populated"); + + Console.WriteLine($"Enhanced search returned {result.Results.Count} environment articles"); + Console.WriteLine($"First article has {firstItem.Tags.Count} tags"); + } +} diff --git a/GuardianClient/GuardianClient.Tests/TestBase.cs b/GuardianClient/GuardianClient.Tests/TestBase.cs index 5960da5..45d2a50 100644 --- a/GuardianClient/GuardianClient.Tests/TestBase.cs +++ b/GuardianClient/GuardianClient.Tests/TestBase.cs @@ -6,7 +6,7 @@ namespace GuardianClient.Tests; public abstract class TestBase { - protected static GuardianApiClient ApiClient { get; } + protected static IGuardianApiClient ApiClient { get; } static TestBase() { diff --git a/GuardianClient/GuardianClient/GuardianApiClient.cs b/GuardianClient/GuardianClient/GuardianApiClient.cs index 204b3ff..92272ff 100644 --- a/GuardianClient/GuardianClient/GuardianApiClient.cs +++ b/GuardianClient/GuardianClient/GuardianApiClient.cs @@ -1,17 +1,15 @@ -using System.Reflection; using System.Text.Json; +using GuardianClient.Internal; using GuardianClient.Models; +using GuardianClient.Options.Search; namespace GuardianClient; -public class GuardianApiClient : IDisposable +public class GuardianApiClient : IGuardianApiClient, IDisposable { private readonly HttpClient _httpClient; - private readonly string _apiKey; - private readonly bool _ownsHttpClient; - private bool _disposed; private const string BaseUrl = "https://content.guardianapis.com"; @@ -54,81 +52,21 @@ public GuardianApiClient(string apiKey) ConfigureHttpClient(); } - private void ConfigureHttpClient() - { - var packageVersion = GetPackageVersion(); - - _httpClient.BaseAddress = new Uri(BaseUrl); - _httpClient.DefaultRequestHeaders.Add("User-Agent", $"GuardianClient.NET/{packageVersion}"); - } - - private string GetPackageVersion() - { - var packageVersion = Assembly - .GetExecutingAssembly() - .GetCustomAttribute()! - .InformationalVersion; - - return packageVersion; - } - - /// - /// Search for Guardian content - /// - /// Search query (supports AND, OR, NOT operators) - /// Number of results per page (1-50) - /// Page number for pagination - /// API options for including additional response data - /// Cancellation token - /// Content search results public async Task SearchAsync( - string? query = null, - int? pageSize = null, - int? page = null, - GuardianApiOptions? options = null, - CancellationToken cancellationToken = default) + GuardianApiContentSearchOptions? options = null, + CancellationToken cancellationToken = default + ) { - var parameters = new List { $"api-key={Uri.EscapeDataString(_apiKey)}" }; - - if (!string.IsNullOrWhiteSpace(query)) - { - parameters.Add($"q={Uri.EscapeDataString(query)}"); - } - - if (pageSize.HasValue) - { - parameters.Add($"page-size={pageSize.Value}"); - } - - if (page.HasValue) - { - parameters.Add($"page={page.Value}"); - } - - if (options?.ShowFields?.Length > 0) - { - parameters.Add($"show-fields={string.Join(",", options.ShowFields)}"); - } - - if (options?.ShowTags?.Length > 0) - { - parameters.Add($"show-tags={string.Join(",", options.ShowTags)}"); - } - - if (options?.ShowElements?.Length > 0) - { - parameters.Add($"show-elements={string.Join(",", options.ShowElements)}"); - } + options ??= new GuardianApiContentSearchOptions(); - if (options?.ShowReferences?.Length > 0) - { - parameters.Add($"show-references={string.Join(",", options.ShowReferences)}"); - } + var parameters = new List { $"api-key={Uri.EscapeDataString(_apiKey)}" }; - if (options?.ShowBlocks?.Length > 0) - { - parameters.Add($"show-blocks={string.Join(",", options.ShowBlocks)}"); - } + UrlParameterBuilder.AddQueryParameters(options, parameters); + UrlParameterBuilder.AddFilterParameters(options.FilterOptions, parameters); + UrlParameterBuilder.AddDateParameters(options.DateOptions, parameters); + UrlParameterBuilder.AddPageParameters(options.PageOptions, parameters); + UrlParameterBuilder.AddOrderParameters(options.OrderOptions, parameters); + UrlParameterBuilder.AddAdditionalInformationParameters(options.AdditionalInformationOptions, parameters); var url = $"/search?{string.Join("&", parameters)}"; var response = await _httpClient.GetAsync(url, cancellationToken); @@ -141,46 +79,46 @@ private string GetPackageVersion() return wrapper?.Response; } - /// - /// Get a single content item by its ID/path - /// - /// The content item ID (path from Guardian API) - /// API options for including additional response data - /// Cancellation token - /// Single item response with content details public async Task GetItemAsync( string itemId, - GuardianApiOptions? options = null, + GuardianApiContentAdditionalInformationOptions? options = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(itemId); var parameters = new List { $"api-key={Uri.EscapeDataString(_apiKey)}" }; - if (options?.ShowFields?.Length > 0) - { - parameters.Add($"show-fields={string.Join(",", options.ShowFields)}"); - } - - if (options?.ShowTags?.Length > 0) - { - parameters.Add($"show-tags={string.Join(",", options.ShowTags)}"); - } - - if (options?.ShowElements?.Length > 0) - { - parameters.Add($"show-elements={string.Join(",", options.ShowElements)}"); - } - - if (options?.ShowReferences?.Length > 0) - { - parameters.Add($"show-references={string.Join(",", options.ShowReferences)}"); - } - - if (options?.ShowBlocks?.Length > 0) - { - parameters.Add($"show-blocks={string.Join(",", options.ShowBlocks)}"); - } + UrlParameterBuilder.AddParameterIfAny( + parameters, + "show-fields", + options?.ShowFields, + option => option.ToApiString() + ); + + UrlParameterBuilder.AddParameterIfAny( + parameters, + "show-tags", + options?.ShowTags, + option => option.ToApiString() + ); + UrlParameterBuilder.AddParameterIfAny( + parameters, + "show-elements", + options?.ShowElements, + option => option.ToApiString() + ); + UrlParameterBuilder.AddParameterIfAny( + parameters, + "show-references", + options?.ShowReferences, + option => option.ToApiString() + ); + + UrlParameterBuilder.AddParameterIfAny( + parameters, + "show-blocks", + options?.ShowBlocks + ); var url = $"/{itemId}?{string.Join("&", parameters)}"; var response = await _httpClient.GetAsync(url, cancellationToken); @@ -199,6 +137,14 @@ public void Dispose() GC.SuppressFinalize(this); } + private void ConfigureHttpClient() + { + var packageVersion = AssemblyInfo.GetPackageVersion(); + + _httpClient.BaseAddress = new Uri(BaseUrl); + _httpClient.DefaultRequestHeaders.Add("User-Agent", $"GuardianClient.NET/{packageVersion}"); + } + /// /// Disposes the if: /// 1. This instance owns it, and diff --git a/GuardianClient/GuardianClient/GuardianApiOptions.cs b/GuardianClient/GuardianClient/GuardianApiOptions.cs deleted file mode 100644 index 505cc6b..0000000 --- a/GuardianClient/GuardianClient/GuardianApiOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GuardianClient; - -public class GuardianApiOptions -{ - public string[]? ShowFields { get; set; } - public string[]? ShowTags { get; set; } - public string[]? ShowElements { get; set; } - public string[]? ShowReferences { get; set; } - public string[]? ShowBlocks { get; set; } -} diff --git a/GuardianClient/GuardianClient/GuardianClient.csproj b/GuardianClient/GuardianClient/GuardianClient.csproj index e09c9ec..b385d91 100644 --- a/GuardianClient/GuardianClient/GuardianClient.csproj +++ b/GuardianClient/GuardianClient/GuardianClient.csproj @@ -7,13 +7,13 @@ GuardianClient.NET - 0.3.0-alpha + 0.4.0-alpha Andrew Tarr A .NET API wrapper for Guardian services guardian;api;client;wrapper MIT https://github.com/tarrball/GuardianClient.NET - https://github.com/tarrball/GuardianClient.NET.git + https://github.com/tarrball/GuardianClient.NET git main README.md diff --git a/GuardianClient/GuardianClient/IGuardianApiClient.cs b/GuardianClient/GuardianClient/IGuardianApiClient.cs new file mode 100644 index 0000000..d3a3f6b --- /dev/null +++ b/GuardianClient/GuardianClient/IGuardianApiClient.cs @@ -0,0 +1,53 @@ +using GuardianClient.Models; +using GuardianClient.Options.Search; + +namespace GuardianClient; + +/// +/// Interface for the Guardian API client, providing access to Guardian content search and retrieval. +/// +public interface IGuardianApiClient +{ + /// + /// Search for Guardian content using comprehensive search options. + /// + /// + /// Search options including query terms, filters, pagination, ordering, date ranges, and additional information to include. + /// If null, returns all content with default settings (newest first, page 1, 10 items per page). + /// + /// Cancellation token to cancel the HTTP request + /// + /// A containing the search results, pagination info, and metadata. + /// Returns null if the API response cannot be deserialized. + /// + /// Thrown when the API request fails or returns a non-success status code + /// Thrown when the request is cancelled via the cancellation token + /// + /// This method provides access to the full Guardian Content API search functionality, including: + /// + /// Free text search with boolean operators (AND, OR, NOT) + /// Filtering by section, tags, references, production office, language, star rating, and more + /// Date range filtering with different date types (published, first-publication, newspaper-edition, last-modified) + /// Pagination and result ordering options + /// Additional fields, tags, elements, references, and blocks in responses + /// + /// For simple searches, you can create basic options: new GuardianApiContentSearchOptions { Query = "your search terms" } + /// + Task SearchAsync( + GuardianApiContentSearchOptions? options = null, + CancellationToken cancellationToken = default + ); + + /// + /// Get a single content item by its ID/path + /// + /// The content item ID (path from Guardian API) + /// API options for including additional response data + /// Cancellation token + /// Single item response with content details + Task GetItemAsync( + string itemId, + GuardianApiContentAdditionalInformationOptions? options = null, + CancellationToken cancellationToken = default + ); +} diff --git a/GuardianClient/GuardianClient/Internal/AdditionalInformationExtensions.cs b/GuardianClient/GuardianClient/Internal/AdditionalInformationExtensions.cs new file mode 100644 index 0000000..0327232 --- /dev/null +++ b/GuardianClient/GuardianClient/Internal/AdditionalInformationExtensions.cs @@ -0,0 +1,95 @@ +using GuardianClient.Options.Search; + +namespace GuardianClient.Internal; + +/// +/// Extension methods for converting additional information enums to their API string values. +/// +internal static class AdditionalInformationExtensions +{ + internal static string ToApiString(this GuardianApiContentShowFieldsOption option) => option switch + { + GuardianApiContentShowFieldsOption.TrailText => "trailText", + GuardianApiContentShowFieldsOption.Headline => "headline", + GuardianApiContentShowFieldsOption.ShowInRelatedContent => "showInRelatedContent", + GuardianApiContentShowFieldsOption.Body => "body", + GuardianApiContentShowFieldsOption.LastModified => "lastModified", + GuardianApiContentShowFieldsOption.HasStoryPackage => "hasStoryPackage", + GuardianApiContentShowFieldsOption.Score => "score", + GuardianApiContentShowFieldsOption.Standfirst => "standfirst", + GuardianApiContentShowFieldsOption.ShortUrl => "shortUrl", + GuardianApiContentShowFieldsOption.Thumbnail => "thumbnail", + GuardianApiContentShowFieldsOption.Wordcount => "wordcount", + GuardianApiContentShowFieldsOption.Commentable => "commentable", + GuardianApiContentShowFieldsOption.IsPremoderated => "isPremoderated", + GuardianApiContentShowFieldsOption.AllowUgc => "allowUgc", + GuardianApiContentShowFieldsOption.Byline => "byline", + GuardianApiContentShowFieldsOption.Publication => "publication", + GuardianApiContentShowFieldsOption.InternalPageCode => "internalPageCode", + GuardianApiContentShowFieldsOption.ProductionOffice => "productionOffice", + GuardianApiContentShowFieldsOption.ShouldHideAdverts => "shouldHideAdverts", + GuardianApiContentShowFieldsOption.LiveBloggingNow => "liveBloggingNow", + GuardianApiContentShowFieldsOption.CommentCloseDate => "commentCloseDate", + GuardianApiContentShowFieldsOption.StarRating => "starRating", + GuardianApiContentShowFieldsOption.All => "all", + _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) + }; + + internal static string ToApiString(this GuardianApiContentShowTagsOption option) => option switch + { + GuardianApiContentShowTagsOption.Blog => "blog", + GuardianApiContentShowTagsOption.Contributor => "contributor", + GuardianApiContentShowTagsOption.Keyword => "keyword", + GuardianApiContentShowTagsOption.NewspaperBook => "newspaper-book", + GuardianApiContentShowTagsOption.NewspaperBookSection => "newspaper-book-section", + GuardianApiContentShowTagsOption.Publication => "publication", + GuardianApiContentShowTagsOption.Series => "series", + GuardianApiContentShowTagsOption.Tone => "tone", + GuardianApiContentShowTagsOption.Type => "type", + GuardianApiContentShowTagsOption.All => "all", + _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) + }; + + internal static string ToApiString(this GuardianApiContentShowElementsOption option) => option switch + { + GuardianApiContentShowElementsOption.Audio => "audio", + GuardianApiContentShowElementsOption.Image => "image", + GuardianApiContentShowElementsOption.Video => "video", + GuardianApiContentShowElementsOption.All => "all", + _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) + }; + + internal static string ToApiString(this GuardianApiContentShowReferencesOption option) => option switch + { + GuardianApiContentShowReferencesOption.Author => "author", + GuardianApiContentShowReferencesOption.BisacPrefix => "bisac-prefix", + GuardianApiContentShowReferencesOption.EsaCricketMatch => "esa-cricket-match", + GuardianApiContentShowReferencesOption.EsaFootballMatch => "esa-football-match", + GuardianApiContentShowReferencesOption.EsaFootballTeam => "esa-football-team", + GuardianApiContentShowReferencesOption.EsaFootballTournament => "esa-football-tournament", + GuardianApiContentShowReferencesOption.Isbn => "isbn", + GuardianApiContentShowReferencesOption.Imdb => "imdb", + GuardianApiContentShowReferencesOption.Musicbrainz => "musicbrainz", + GuardianApiContentShowReferencesOption.MusicbrainzGenre => "musicbrainzgenre", + GuardianApiContentShowReferencesOption.OptaCricketMatch => "opta-cricket-match", + GuardianApiContentShowReferencesOption.OptaFootballMatch => "opta-football-match", + GuardianApiContentShowReferencesOption.OptaFootballTeam => "opta-football-team", + GuardianApiContentShowReferencesOption.OptaFootballTournament => "opta-football-tournament", + GuardianApiContentShowReferencesOption.PaFootballCompetition => "pa-football-competition", + GuardianApiContentShowReferencesOption.PaFootballMatch => "pa-football-match", + GuardianApiContentShowReferencesOption.PaFootballTeam => "pa-football-team", + GuardianApiContentShowReferencesOption.R1Film => "r1-film", + GuardianApiContentShowReferencesOption.ReutersIndexRic => "reuters-index-ric", + GuardianApiContentShowReferencesOption.ReutersStockRic => "reuters-stock-ric", + GuardianApiContentShowReferencesOption.WitnessAssignment => "witness-assignment", + _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) + }; + + internal static string ToApiString(this GuardianApiContentShowRightsOption option) => option switch + { + GuardianApiContentShowRightsOption.Syndicatable => "syndicatable", + GuardianApiContentShowRightsOption.SubscriptionDatabases => "subscription-databases", + GuardianApiContentShowRightsOption.All => "all", + _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) + }; +} diff --git a/GuardianClient/GuardianClient/Internal/AssemblyInfo.cs b/GuardianClient/GuardianClient/Internal/AssemblyInfo.cs new file mode 100644 index 0000000..bbb6fe7 --- /dev/null +++ b/GuardianClient/GuardianClient/Internal/AssemblyInfo.cs @@ -0,0 +1,16 @@ +using System.Reflection; + +namespace GuardianClient.Internal; + +internal static class AssemblyInfo +{ + internal static string GetPackageVersion() + { + var packageVersion = Assembly + .GetExecutingAssembly() + .GetCustomAttribute()! + .InformationalVersion; + + return packageVersion; + } +} \ No newline at end of file diff --git a/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs b/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs new file mode 100644 index 0000000..73981d1 --- /dev/null +++ b/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs @@ -0,0 +1,125 @@ +using GuardianClient.Options.Search; + +namespace GuardianClient.Internal; + +internal static class UrlParameterBuilder +{ + internal static void AddParameterIfNotEmpty(List parameters, string parameterName, string? value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + parameters.Add($"{parameterName}={Uri.EscapeDataString(value)}"); + } + } + + internal static void AddParameterIfHasValue(List parameters, string parameterName, T? value) where T : struct + { + if (value.HasValue) + { + parameters.Add($"{parameterName}={value.Value}"); + } + } + + internal static void AddParameterIfAny(List parameters, string parameterName, string[]? values) + { + if (values is { Length: > 0 }) + { + parameters.Add($"{parameterName}={string.Join(",", values.Select(Uri.EscapeDataString))}"); + } + } + + internal static void AddParameterIfAny(List parameters, string parameterName, T[]? values, Func converter) + { + if (values is { Length: > 0 }) + { + parameters.Add($"{parameterName}={string.Join(",", values.Select(v => Uri.EscapeDataString(converter(v))))}"); + } + } + + internal static void AddQueryParameters(GuardianApiContentSearchOptions options, List parameters) + { + AddParameterIfNotEmpty(parameters, "q", options.Query); + AddParameterIfAny(parameters, "query-fields", options.QueryFields); + } + + internal static void AddFilterParameters(GuardianApiContentFilterOptions filterOptions, List parameters) + { + AddParameterIfNotEmpty(parameters, "section", filterOptions.Section); + AddParameterIfNotEmpty(parameters, "reference", filterOptions.Reference); + AddParameterIfNotEmpty(parameters, "reference-type", filterOptions.ReferenceType); + AddParameterIfNotEmpty(parameters, "tag", filterOptions.Tag); + AddParameterIfNotEmpty(parameters, "rights", filterOptions.Rights); + AddParameterIfNotEmpty(parameters, "ids", filterOptions.Ids); + AddParameterIfNotEmpty(parameters, "production-office", filterOptions.ProductionOffice); + AddParameterIfNotEmpty(parameters, "lang", filterOptions.Language); + + AddParameterIfHasValue(parameters, "star-rating", filterOptions.StarRating); + } + + internal static void AddDateParameters(GuardianApiContentDateOptions dateOptions, List parameters) + { + if (dateOptions.FromDate != default) + { + parameters.Add($"from-date={dateOptions.FromDate:yyyy-MM-dd}"); + } + + if (dateOptions.ToDate != default) + { + parameters.Add($"to-date={dateOptions.ToDate:yyyy-MM-dd}"); + } + + AddParameterIfNotEmpty(parameters, "use-date", dateOptions.UseDate); + } + + internal static void AddPageParameters(GuardianApiContentPageOptions pageOptions, List parameters) + { + if (pageOptions.Page > 0) + { + parameters.Add($"page={pageOptions.Page}"); + } + + if (pageOptions.PageSize > 0) + { + parameters.Add($"page-size={pageOptions.PageSize}"); + } + } + + internal static void AddOrderParameters(GuardianApiContentOrderOptions orderOptions, List parameters) + { + if (orderOptions.OrderBy.HasValue) + { + var orderByValue = orderOptions.OrderBy.Value switch + { + GuardianApiContentOrderBy.Newest => "newest", + GuardianApiContentOrderBy.Oldest => "oldest", + GuardianApiContentOrderBy.Relevance => "relevance", + _ => "newest" + }; + parameters.Add($"order-by={orderByValue}"); + } + + if (orderOptions.OrderDate.HasValue) + { + var orderDateValue = orderOptions.OrderDate.Value switch + { + GuardianApiContentOrderDate.Published => "published", + GuardianApiContentOrderDate.NewspaperEdition => "newspaper-edition", + GuardianApiContentOrderDate.LastModified => "last-modified", + _ => "published" + }; + parameters.Add($"order-date={orderDateValue}"); + } + } + + internal static void AddAdditionalInformationParameters( + GuardianApiContentAdditionalInformationOptions additionalOptions, + List parameters + ) + { + AddParameterIfAny(parameters, "show-fields", additionalOptions.ShowFields, f => f.ToApiString()); + AddParameterIfAny(parameters, "show-tags", additionalOptions.ShowTags, t => t.ToApiString()); + AddParameterIfAny(parameters, "show-elements", additionalOptions.ShowElements, e => e.ToApiString()); + AddParameterIfAny(parameters, "show-references", additionalOptions.ShowReferences, r => r.ToApiString()); + AddParameterIfAny(parameters, "show-blocks", additionalOptions.ShowBlocks); + } +} \ No newline at end of file diff --git a/GuardianClient/GuardianClient/Models/BaseResponse.cs b/GuardianClient/GuardianClient/Models/BaseResponse.cs index 91a091b..2458949 100644 --- a/GuardianClient/GuardianClient/Models/BaseResponse.cs +++ b/GuardianClient/GuardianClient/Models/BaseResponse.cs @@ -4,12 +4,21 @@ namespace GuardianClient.Models; public class BaseResponse { + /// + /// The status of the response. It refers to the state of the API. Successful calls will receive an "ok" even if your query did not return any results. + /// [JsonPropertyName("status")] public string Status { get; set; } = string.Empty; + /// + /// The user tier for the API account. + /// [JsonPropertyName("userTier")] public string? UserTier { get; set; } + /// + /// The number of results available for your search overall. + /// [JsonPropertyName("total")] public int Total { get; set; } -} \ No newline at end of file +} diff --git a/GuardianClient/GuardianClient/Models/ContentItem.cs b/GuardianClient/GuardianClient/Models/ContentItem.cs index c201d81..6c0aab8 100644 --- a/GuardianClient/GuardianClient/Models/ContentItem.cs +++ b/GuardianClient/GuardianClient/Models/ContentItem.cs @@ -4,27 +4,45 @@ namespace GuardianClient.Models; public class ContentItem { + /// + /// The path to content. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; + /// + /// The id of the section. + /// [JsonPropertyName("sectionId")] public string? SectionId { get; set; } + /// + /// The name of the section. + /// [JsonPropertyName("sectionName")] public string? SectionName { get; set; } + /// + /// The combined date and time of publication. + /// [JsonPropertyName("webPublicationDate")] public DateTime? WebPublicationDate { get; set; } [JsonPropertyName("webTitle")] public string WebTitle { get; set; } = string.Empty; + /// + /// The URL of the html content. + /// [JsonPropertyName("webUrl")] public string WebUrl { get; set; } = string.Empty; + /// + /// The URL of the raw content. + /// [JsonPropertyName("apiUrl")] public string ApiUrl { get; set; } = string.Empty; @@ -51,4 +69,4 @@ public class ContentItem [JsonPropertyName("blocks")] public ContentBlocks? Blocks { get; set; } -} \ No newline at end of file +} diff --git a/GuardianClient/GuardianClient/Models/ContentSearchResponse.cs b/GuardianClient/GuardianClient/Models/ContentSearchResponse.cs index 45b4654..399ea7f 100644 --- a/GuardianClient/GuardianClient/Models/ContentSearchResponse.cs +++ b/GuardianClient/GuardianClient/Models/ContentSearchResponse.cs @@ -4,21 +4,39 @@ namespace GuardianClient.Models; public class ContentSearchResponse : BaseResponse { + /// + /// The starting index for the current result set. + /// [JsonPropertyName("startIndex")] public int StartIndex { get; set; } + /// + /// The number of items returned in this call. + /// [JsonPropertyName("pageSize")] public int PageSize { get; set; } + /// + /// The number of the page you are browsing. + /// [JsonPropertyName("currentPage")] public int CurrentPage { get; set; } + /// + /// The total amount of pages that are in this call. + /// [JsonPropertyName("pages")] public int Pages { get; set; } + /// + /// The sort order used. + /// [JsonPropertyName("orderBy")] public string? OrderBy { get; set; } + /// + /// The collection of content items returned by the search. + /// [JsonPropertyName("results")] - public List Results { get; set; } = new(); -} \ No newline at end of file + public ICollection Results { get; set; } = new HashSet(); +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs new file mode 100644 index 0000000..f0a4f81 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.CodeAnalysis; + +namespace GuardianClient.Options.Search; + +/// +/// Options for requesting additional information to be included with search results. +/// +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] +public class GuardianApiContentAdditionalInformationOptions +{ + /// + /// Add fields associated with the content such as headline, body, thumbnail, etc. + /// + public GuardianApiContentShowFieldsOption[]? ShowFields { get; set; } + + /// + /// Add associated metadata tags such as contributor, keyword, tone, etc. + /// + public GuardianApiContentShowTagsOption[]? ShowTags { get; set; } + + /// + /// Add associated media elements such as images, audio, and video. + /// + public GuardianApiContentShowElementsOption[]? ShowElements { get; set; } + + /// + /// Add associated reference data such as ISBNs, IMDB IDs, author references, etc. + /// + public GuardianApiContentShowReferencesOption[]? ShowReferences { get; set; } + + + /// + /// Add associated blocks (single block for content, one or more for liveblogs). + /// Supported values: + /// + /// "main" - Main content block + /// "body" - Body content blocks + /// "all" - All blocks + /// "body:latest" - Latest body blocks (default limit: 20) + /// "body:latest:10" - Latest 10 body blocks + /// "body:oldest" - Oldest body blocks + /// "body:oldest:10" - Oldest 10 body blocks + /// "body:<block-id>" - Specific block by ID + /// "body:around:<block-id>" - Block and 20 blocks around it + /// "body:around:<block-id>:10" - Block and 10 blocks around it + /// "body:key-events" - Key event blocks + /// "body:published-since:<timestamp>" - Blocks since timestamp (e.g., "body:published-since:1556529318000") + /// + /// + public string[]? ShowBlocks { get; set; } +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentDateOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentDateOptions.cs new file mode 100644 index 0000000..0fe11f5 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentDateOptions.cs @@ -0,0 +1,25 @@ +namespace GuardianClient.Options.Search; + +/// +/// Options for filtering search results by date ranges. +/// +public class GuardianApiContentDateOptions +{ + /// + /// Return only content published on or after that date. + /// Example: 2014-02-16. + /// + public DateOnly FromDate { get; set; } + + /// + /// Return only content published on or before that date. + /// Example: 2014-02-17. + /// + public DateOnly ToDate { get; set; } + + /// + /// Changes which type of date is used to filter the results using FromDate and ToDate. + /// Accepted values: "published" (default - the date the content has been last published), "first-publication" (the date the content has been first published), "newspaper-edition" (the date the content appeared in print), "last-modified" (the date the content was last updated). + /// + public string? UseDate { get; set; } +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs new file mode 100644 index 0000000..e838e60 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs @@ -0,0 +1,64 @@ +using System.Diagnostics.CodeAnalysis; + +namespace GuardianClient.Options.Search; + +/// +/// Options for filtering content search results by various criteria. +/// +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] +public class GuardianApiContentFilterOptions +{ + /// + /// Return only content in those sections. Supports boolean operators. + /// Example: "football". + /// + public string? Section { get; set; } + + /// + /// Return only content with those references. Supports boolean operators. + /// Example: "isbn/9780718178949". + /// + public string? Reference { get; set; } + + /// + /// Return only content with references of those types. Supports boolean operators. + /// Example: "isbn". + /// + public string? ReferenceType { get; set; } + + /// + /// Return only content with those tags. Supports boolean operators. + /// Example: "technology/apple". + /// + public string? Tag { get; set; } + + /// + /// Return only content with those rights. Does not support boolean operators. + /// Accepted values: "syndicatable", "subscription-databases". + /// + public string? Rights { get; set; } + + /// + /// Return only content with those IDs. Does not support boolean operators. + /// Example: "technology/2014/feb/17/flappy-bird-clones-apple-google". + /// + public string? Ids { get; set; } + + /// + /// Return only content from those production offices. Supports boolean operators. + /// Example: "aus". + /// + public string? ProductionOffice { get; set; } + + /// + /// Return only content in those languages. Supports boolean operators. + /// Accepts ISO language codes. Examples: "en", "fr". + /// + public string? Language { get; set; } + + /// + /// Return only content with a given star rating. Does not support boolean operators. + /// Accepted values: 1 to 5. + /// + public int? StarRating { get; set; } +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderBy.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderBy.cs new file mode 100644 index 0000000..b75c022 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderBy.cs @@ -0,0 +1,22 @@ +namespace GuardianClient.Options.Search; + +/// +/// Specifies the order in which search results should be returned. +/// +public enum GuardianApiContentOrderBy +{ + /// + /// Order by newest content first. Default in all other cases. + /// + Newest, + + /// + /// Order by oldest content first. + /// + Oldest, + + /// + /// Order by relevance to search query. Default where q parameter is specified. + /// + Relevance +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderDate.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderDate.cs new file mode 100644 index 0000000..10962a8 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderDate.cs @@ -0,0 +1,22 @@ +namespace GuardianClient.Options.Search; + +/// +/// Specifies which type of date is used to order the results. +/// +public enum GuardianApiContentOrderDate +{ + /// + /// The date the content appeared on the web. Default. + /// + Published, + + /// + /// The date the content appeared in print. + /// + NewspaperEdition, + + /// + /// The date the content was last updated. + /// + LastModified +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs new file mode 100644 index 0000000..606a557 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; + +namespace GuardianClient.Options.Search; + +/// +/// Options for controlling the ordering of search results. +/// +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] +public class GuardianApiContentOrderOptions +{ + /// + /// Returns results in the specified order. Defaults to Newest in most cases, or Relevance when a query parameter is specified. + /// + public GuardianApiContentOrderBy? OrderBy { get; set; } + + /// + /// Changes which type of date is used to order the results. Defaults to Published. + /// + public GuardianApiContentOrderDate? OrderDate { get; set; } +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs new file mode 100644 index 0000000..04ba1ee --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; + +namespace GuardianClient.Options.Search; + +/// +/// Options for controlling pagination of search results. +/// +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] +public class GuardianApiContentPageOptions +{ + /// + /// Return only the result set from a particular page. + /// Example: 5. + /// + public int Page { get; set; } + + /// + /// Modify the number of items displayed per page. + /// Accepted values: 1 to 50. + /// + public int PageSize { get; set; } +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs new file mode 100644 index 0000000..01fbf4d --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; + +namespace GuardianClient.Options.Search; + +/// +/// Options for searching content using the Guardian API. +/// +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] +public class GuardianApiContentSearchOptions +{ + /// + /// Request content containing this free text. Supports AND, OR and NOT operators, and exact phrase queries using double quotes. + /// Examples: "sausages", "pork sausages", "sausages AND (mash OR chips)", "sausages AND NOT (saveloy OR battered)". + /// + public string? Query { get; set; } + + /// + /// Specify in which indexed fields query terms should be searched on. + /// Examples: ["body"], ["body", "thumbnail"]. + /// + public string[]? QueryFields { get; set; } + + /// + /// Options for requesting additional information to be included with search results. + /// + public GuardianApiContentAdditionalInformationOptions AdditionalInformationOptions { get; set; } = new(); + + /// + /// Options for filtering search results by various criteria. + /// + public GuardianApiContentFilterOptions FilterOptions { get; set; } = new(); + + /// + /// Options for filtering search results by date ranges. + /// + public GuardianApiContentDateOptions DateOptions { get; set; } = new(); + + /// + /// Options for controlling pagination of search results. + /// + public GuardianApiContentPageOptions PageOptions { get; set; } = new(); + + /// + /// Options for controlling the ordering of search results. + /// + public GuardianApiContentOrderOptions OrderOptions { get; set; } = new(); +} diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowElementsOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowElementsOption.cs new file mode 100644 index 0000000..275fe03 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowElementsOption.cs @@ -0,0 +1,12 @@ +namespace GuardianClient.Options.Search; + +/// +/// Media element types that can be included with content responses. +/// +public enum GuardianApiContentShowElementsOption +{ + Audio, + Image, + Video, + All +} \ No newline at end of file diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowFieldsOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowFieldsOption.cs new file mode 100644 index 0000000..32e0251 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowFieldsOption.cs @@ -0,0 +1,31 @@ +namespace GuardianClient.Options.Search; + +/// +/// Fields that can be included with content responses. +/// +public enum GuardianApiContentShowFieldsOption +{ + TrailText, + Headline, + ShowInRelatedContent, + Body, + LastModified, + HasStoryPackage, + Score, + Standfirst, + ShortUrl, + Thumbnail, + Wordcount, + Commentable, + IsPremoderated, + AllowUgc, + Byline, + Publication, + InternalPageCode, + ProductionOffice, + ShouldHideAdverts, + LiveBloggingNow, + CommentCloseDate, + StarRating, + All +} \ No newline at end of file diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowReferencesOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowReferencesOption.cs new file mode 100644 index 0000000..663bb09 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowReferencesOption.cs @@ -0,0 +1,29 @@ +namespace GuardianClient.Options.Search; + +/// +/// Reference types that can be included with content responses. +/// +public enum GuardianApiContentShowReferencesOption +{ + Author, + BisacPrefix, + EsaCricketMatch, + EsaFootballMatch, + EsaFootballTeam, + EsaFootballTournament, + Isbn, + Imdb, + Musicbrainz, + MusicbrainzGenre, + OptaCricketMatch, + OptaFootballMatch, + OptaFootballTeam, + OptaFootballTournament, + PaFootballCompetition, + PaFootballMatch, + PaFootballTeam, + R1Film, + ReutersIndexRic, + ReutersStockRic, + WitnessAssignment +} \ No newline at end of file diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowRightsOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowRightsOption.cs new file mode 100644 index 0000000..0a57c62 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowRightsOption.cs @@ -0,0 +1,11 @@ +namespace GuardianClient.Options.Search; + +/// +/// Rights types that can be included with content responses. +/// +public enum GuardianApiContentShowRightsOption +{ + Syndicatable, + SubscriptionDatabases, + All +} \ No newline at end of file diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowTagsOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowTagsOption.cs new file mode 100644 index 0000000..d186193 --- /dev/null +++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowTagsOption.cs @@ -0,0 +1,18 @@ +namespace GuardianClient.Options.Search; + +/// +/// Tag types that can be included with content responses. +/// +public enum GuardianApiContentShowTagsOption +{ + Blog, + Contributor, + Keyword, + NewspaperBook, + NewspaperBookSection, + Publication, + Series, + Tone, + Type, + All +} \ No newline at end of file diff --git a/README.md b/README.md index b8a74d2..8f78dc4 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,21 @@ Guardian knight logo

-A modern .NET client library for The Guardian's Content API. Provides strongly-typed models and simple methods for -searching Guardian articles, tags, sections, and more. +A modern, comprehensive .NET client library for The Guardian's Content API. Provides strongly-typed models, flexible search options, and simple methods for searching Guardian articles with full API feature support. [![NuGet Version](https://img.shields.io/nuget/v/GuardianClient.NET?logo=nuget)](https://www.nuget.org/packages/GuardianClient.NET) [![Build Status](https://img.shields.io/github/actions/workflow/status/tarrball/GuardianClient.NET/deploy-nuget.yml?branch=main)](https://github.com/tarrball/GuardianClient.NET/actions) ## โœจ Features -- ๐Ÿ” **Content Search** - Search Guardian articles with full query support -- ๐Ÿท๏ธ **Strongly Typed** - Complete C# models for all API responses -- ๐Ÿ”ง **Dependency Injection** - Easy setup with `services.AddGuardianApiClient()` -- ๐Ÿ“ฆ **HttpClientFactory** - Proper HttpClient lifecycle management -- โšก **Async/Await** - Modern async patterns with cancellation support +- ๐Ÿ” **Complete Content Search** - Full Guardian Content API search with all parameters +- ๐Ÿท๏ธ **Strongly Typed** - Type-safe enums for fields, tags, elements, and references +- ๐ŸŽฏ **Comprehensive Filtering** - Search by section, tags, date ranges, production office, and more +- ๐Ÿ“‘ **Rich Content Options** - Include fields, tags, media elements, blocks, and references +- ๐Ÿ”ง **Interface-Based Design** - Easy mocking and dependency injection with `IGuardianApiClient` +- ๐Ÿ“ฆ **HttpClientFactory Ready** - Proper HttpClient lifecycle management +- โšก **Modern Async** - Full async/await patterns with cancellation support +- ๐ŸŽจ **Clean API** - Organized option classes for maintainable code ## ๐Ÿš€ Quick Start @@ -28,37 +30,129 @@ searching Guardian articles, tags, sections, and more. dotnet add package GuardianClient.NET ``` -### Setup with Dependency Injection +### Setup with Dependency Injection (Recommended) ```csharp // Program.cs or Startup.cs builder.Services.AddGuardianApiClient("your-api-key"); ``` -### Usage +### Basic Usage ```csharp public class NewsService { - private readonly GuardianApiClient _client; + private readonly IGuardianApiClient _client; - public NewsService(GuardianApiClient client) + public NewsService(IGuardianApiClient client) { _client = client; } - public async Task GetLatestNews() + public async Task GetLatestTechNews() { - return await _client.SearchAsync("politics", pageSize: 10); + return await _client.SearchAsync(new GuardianApiContentSearchOptions + { + Query = "artificial intelligence", + FilterOptions = new GuardianApiContentFilterOptions + { + Section = "technology" + }, + PageOptions = new GuardianApiContentPageOptions + { + PageSize = 10 + } + }); } } ``` +## ๐Ÿ”ง Advanced Usage + +### Comprehensive Search with All Options + +```csharp +var results = await client.SearchAsync(new GuardianApiContentSearchOptions +{ + // Search terms with boolean operators + Query = "climate change AND (policy OR legislation)", + QueryFields = ["body", "headline"], + + // Filtering options + FilterOptions = new GuardianApiContentFilterOptions + { + Section = "environment", + Tag = "climate-change", + Language = "en" + }, + + // Date filtering + DateOptions = new GuardianApiContentDateOptions + { + FromDate = new DateOnly(2023, 1, 1), + UseDate = "published" + }, + + // Pagination + PageOptions = new GuardianApiContentPageOptions + { + Page = 1, + PageSize = 20 + }, + + // Ordering + OrderOptions = new GuardianApiContentOrderOptions + { + OrderBy = GuardianApiContentOrderBy.Relevance, + OrderDate = GuardianApiContentOrderDate.Published + }, + + // Additional content + AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions + { + ShowFields = [ + GuardianApiContentShowFieldsOption.Headline, + GuardianApiContentShowFieldsOption.Body, + GuardianApiContentShowFieldsOption.Thumbnail + ], + ShowTags = [ + GuardianApiContentShowTagsOption.Keyword, + GuardianApiContentShowTagsOption.Tone + ], + ShowElements = [GuardianApiContentShowElementsOption.Image] + } +}); +``` + +### Getting Individual Articles + +```csharp +// Get a specific article by ID +var article = await client.GetItemAsync("world/2023/oct/15/climate-summit-agreement", + new GuardianApiContentAdditionalInformationOptions + { + ShowFields = [ + GuardianApiContentShowFieldsOption.Body, + GuardianApiContentShowFieldsOption.Byline + ], + ShowTags = [GuardianApiContentShowTagsOption.All] + }); + +// Access the rich content +Console.WriteLine(article.Content.WebTitle); +Console.WriteLine(article.Content.Fields.Body); // Full HTML content +Console.WriteLine($"Author: {article.Content.Fields.Byline}"); +``` + ### Manual Setup (without DI) ```csharp using var client = new GuardianApiClient("your-api-key"); -var results = await client.SearchAsync("climate change", pageSize: 5); +var results = await client.SearchAsync(new GuardianApiContentSearchOptions +{ + Query = "sports", + PageOptions = new GuardianApiContentPageOptions { PageSize = 5 } +}); foreach (var article in results.Results) { @@ -69,27 +163,69 @@ foreach (var article in results.Results) ## ๐Ÿ”‘ Getting an API Key 1. Visit [The Guardian Open Platform](https://open-platform.theguardian.com/access/) -2. Sign up for a free developer account +2. Sign up for a free developer account 3. Generate your API key +4. Store it securely (use User Secrets for development) + +## ๐Ÿ—๏ธ Available Options + +### Filter Options +- **Section**: Filter by Guardian sections (e.g., "politics", "sport", "culture") +- **Tags**: Filter by content tags with boolean operators +- **Date Range**: Filter by publication date with flexible date types +- **Language**: Filter by content language (ISO codes) +- **Production Office**: Filter by Guardian production offices +- **Star Rating**: Filter by review ratings (1-5) + +### Additional Content Options +- **ShowFields**: Include extra fields like body content, thumbnails, bylines +- **ShowTags**: Include metadata tags (keywords, contributors, tone) +- **ShowElements**: Include media elements (images, videos, audio) +- **ShowReferences**: Include reference data (ISBNs, IMDB IDs, etc.) +- **ShowBlocks**: Include content blocks (useful for live blogs) + +### Ordering Options +- **OrderBy**: Sort by newest, oldest, or relevance +- **OrderDate**: Choose which date to use for sorting -## ๐Ÿ“‹ Current Status +## ๐Ÿ“Š Current Status -**โœ… Implemented:** +**โœ… Fully Implemented:** +- Complete Content API search with all parameters +- Individual article retrieval +- Strongly-typed models and enums for all options +- Advanced filtering, pagination, and sorting +- Rich content enhancement options +- Interface-based design for easy testing +- Comprehensive documentation and examples -- Content search with basic parameters -- Strongly-typed response models -- Dependency injection support -- HttpClientFactory integration +**๐ŸŽฏ Feature Complete:** This library now supports the full Guardian Content API specification. -**๐Ÿ”„ In Development:** +## ๐Ÿงช Testing -- Additional endpoints (tags, sections, editions) -- Advanced search parameters -- Deep pagination support +The library includes comprehensive test coverage: + +```bash +# Run all tests +dotnet test + +# Run with detailed output +dotnet test --logger "console;verbosity=detailed" +``` + +Tests require a Guardian API key stored in user secrets: +```bash +dotnet user-secrets set "GuardianApiKey" "your-api-key-here" +``` ## ๐Ÿค Contributing -This is an early-stage project. Issues and pull requests are welcome! +Contributions are welcome! Please: +1. Fork the repository +2. Create a feature branch +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit a pull request ## ๐Ÿ“„ License @@ -97,4 +233,4 @@ MIT License - see [LICENSE](LICENSE) for details. --- -*This is an unofficial library and is not affiliated with The Guardian.* +*This is an unofficial library and is not affiliated with The Guardian.* \ No newline at end of file diff --git a/guardian-api-spec.yaml b/guardian-api-spec.yaml deleted file mode 100644 index c92beec..0000000 --- a/guardian-api-spec.yaml +++ /dev/null @@ -1,1148 +0,0 @@ -openapi: 3.0.3 -info: - title: Guardian Content API - description: | - The Guardian Content API provides access to Guardian news content, tags, sections, and editions. - - ## Authentication - All requests require an API key passed as a query parameter: `api-key` - - ## Rate Limiting - The API is rate-limited. For elevated limits, please contact The Guardian. - - ## HTTPS Support - Available at https://content.guardianapis.com/ - version: "1.0.0" - contact: - name: Guardian API Support - url: https://groups.google.com/group/guardian-api-talk/ - license: - name: Guardian API Terms - url: https://open-platform.theguardian.com/access/ - -servers: - - url: https://content.guardianapis.com - description: Production server - -security: - - ApiKeyAuth: [] - -paths: - /search: - get: - summary: Search for content - description: Returns all pieces of content in the API with optional filtering - operationId: searchContent - tags: - - Content - parameters: - - $ref: '#/components/parameters/ApiKey' - - name: q - in: query - description: | - Search query supporting AND, OR, NOT operators and exact phrase queries using double quotes. - Examples: "debate", "debate AND economy", "debate AND NOT immigration", "debate AND (economy OR immigration)" - schema: - type: string - example: "debate AND economy" - - name: query-fields - in: query - description: Specify which indexed fields to search in - schema: - type: array - items: - type: string - enum: [body, thumbnail] - style: form - explode: false - example: ["body", "thumbnail"] - - name: section - in: query - description: Return only content in specified sections (supports boolean operators) - schema: - type: string - example: "football" - - name: reference - in: query - description: Return only content with specified references - schema: - type: string - example: "isbn/9780718178949" - - name: reference-type - in: query - description: Return only content with references of specified types - schema: - type: string - example: "isbn" - - name: tag - in: query - description: Return only content with specified tags (supports boolean operators) - schema: - type: string - example: "technology/apple" - - name: rights - in: query - description: Return only content with specified rights - schema: - type: string - enum: [syndicatable, subscription-databases] - - name: ids - in: query - description: Return only content with specified IDs - schema: - type: string - example: "technology/2014/feb/17/flappy-bird-clones-apple-google" - - name: production-office - in: query - description: Return only content from specified production offices - schema: - type: string - example: "aus" - - name: lang - in: query - description: Return only content in specified languages (ISO language codes) - schema: - type: string - example: "en" - - name: star-rating - in: query - description: Return only content with specified star rating - schema: - type: integer - minimum: 1 - maximum: 5 - - name: from-date - in: query - description: Return only content published on or after this date - schema: - type: string - format: date - example: "2014-02-16" - - name: to-date - in: query - description: Return only content published on or before this date - schema: - type: string - format: date - example: "2014-02-17" - - name: use-date - in: query - description: Type of date to use for filtering - schema: - type: string - enum: [published, first-publication, newspaper-edition, last-modified] - default: published - - name: page - in: query - description: Page number for pagination - schema: - type: integer - minimum: 1 - default: 1 - - name: page-size - in: query - description: Number of items per page - schema: - type: integer - minimum: 1 - maximum: 50 - default: 10 - - name: order-by - in: query - description: Sort order for results - schema: - type: string - enum: [newest, oldest, relevance] - default: newest - - name: order-date - in: query - description: Type of date to use for ordering - schema: - type: string - enum: [published, newspaper-edition, last-modified] - default: published - - name: show-fields - in: query - description: Additional fields to include with content - schema: - type: array - items: - type: string - enum: [ - trailText, headline, showInRelatedContent, body, lastModified, - hasStoryPackage, score, standfirst, shortUrl, thumbnail, wordcount, - commentable, isPremoderated, allowUgc, byline, publication, - internalPageCode, productionOffice, shouldHideAdverts, liveBloggingNow, - commentCloseDate, starRating, all - ] - style: form - explode: false - - name: show-tags - in: query - description: Types of tags to include - schema: - type: array - items: - type: string - enum: [ - blog, contributor, keyword, newspaper-book, newspaper-book-section, - publication, series, tone, type, all - ] - style: form - explode: false - - name: show-section - in: query - description: Include section metadata - schema: - type: boolean - - name: show-blocks - in: query - description: | - Include content blocks. Supports: - - main, body, all - - body:latest (default 20), body:latest:10 (limit 10) - - body:oldest, body:oldest:10 - - body: (specific block) - - body:around: (block + 20 around it) - - body:around::10 (block + 10 around it) - - body:key-events - - body:published-since: - schema: - type: array - items: - type: string - style: form - explode: false - - name: show-elements - in: query - description: Include media elements - schema: - type: array - items: - type: string - enum: [audio, image, video, all] - style: form - explode: false - - name: show-references - in: query - description: Include reference data - schema: - type: array - items: - type: string - enum: [ - author, bisac-prefix, esa-cricket-match, esa-football-match, - esa-football-team, esa-football-tournament, isbn, imdb, musicbrainz, - musicbrainzgenre, opta-cricket-match, opta-football-match, - opta-football-team, opta-football-tournament, pa-football-competition, - pa-football-match, pa-football-team, r1-film, reuters-index-ric, - reuters-stock-ric, witness-assignment - ] - style: form - explode: false - - name: show-rights - in: query - description: Include rights information - schema: - type: array - items: - type: string - enum: [syndicatable, subscription-databases, all] - style: form - explode: false - - name: format - in: query - description: Response format - schema: - type: string - enum: [json] - default: json - responses: - '200': - description: Successful response - content: - application/json: - schema: - $ref: '#/components/schemas/ContentSearchResponse' - '400': - description: Bad request - '401': - description: Unauthorized - invalid API key - '429': - description: Rate limit exceeded - - /content/{itemId}/next: - get: - summary: Get next page of content for deep pagination - description: | - For deep pagination beyond standard page limits. Use the ID of the last content item - from a previous search to get the next set of results. - operationId: getNextContent - tags: - - Content - parameters: - - name: itemId - in: path - required: true - description: ID of the last content item from previous results - schema: - type: string - example: "food/2022/jul/04/peperonata-sausages-recipe-rachel-roddy" - - $ref: '#/components/parameters/ApiKey' - - name: q - in: query - description: Original search query (must match original search) - schema: - type: string - - name: page-size - in: query - description: Number of items per page (must match original search) - schema: - type: integer - minimum: 1 - maximum: 50 - default: 10 - - name: order-by - in: query - description: Sort order (must match original search) - schema: - type: string - enum: [newest, oldest, relevance] - responses: - '200': - description: Successful response - content: - application/json: - schema: - $ref: '#/components/schemas/ContentSearchResponse' - '400': - description: Bad request - '401': - description: Unauthorized - invalid API key - '429': - description: Rate limit exceeded - - /tags: - get: - summary: Search for tags - description: Returns all tags in the API with optional filtering - operationId: searchTags - tags: - - Tags - parameters: - - $ref: '#/components/parameters/ApiKey' - - name: q - in: query - description: Return tags containing this free text - schema: - type: string - example: "sausages" - - name: web-title - in: query - description: Return tags starting with this free text - schema: - type: string - example: "sausa" - - name: type - in: query - description: Return only tags of specified type - schema: - type: string - enum: [keyword, series, contributor, tone, type, blog] - - name: section - in: query - description: Return only tags in specified sections (supports boolean operators) - schema: - type: string - example: "football" - - name: reference - in: query - description: Return only tags with specified references - schema: - type: string - example: "isbn/9780349108391" - - name: reference-type - in: query - description: Return only tags with references of specified types - schema: - type: string - example: "isbn" - - name: page - in: query - description: Page number for pagination - schema: - type: integer - minimum: 1 - default: 1 - - name: page-size - in: query - description: Number of items per page - schema: - type: integer - minimum: 1 - maximum: 50 - default: 10 - - name: show-references - in: query - description: Include reference data - schema: - type: array - items: - type: string - enum: [ - author, bisac-prefix, esa-cricket-match, esa-football-match, - esa-football-team, esa-football-tournament, isbn, imdb, musicbrainz, - musicbrainzgenre, opta-cricket-match, opta-football-match, - opta-football-team, opta-football-tournament, pa-football-competition, - pa-football-match, pa-football-team, r1-film, reuters-index-ric, - reuters-stock-ric, witness-assignment - ] - style: form - explode: false - responses: - '200': - description: Successful response - content: - application/json: - schema: - $ref: '#/components/schemas/TagsResponse' - '400': - description: Bad request - '401': - description: Unauthorized - invalid API key - '429': - description: Rate limit exceeded - - /sections: - get: - summary: Get all sections - description: Returns all sections in the API - operationId: getSections - tags: - - Sections - parameters: - - $ref: '#/components/parameters/ApiKey' - - name: q - in: query - description: Return sections based on query term - schema: - type: string - example: "business" - responses: - '200': - description: Successful response - content: - application/json: - schema: - $ref: '#/components/schemas/SectionsResponse' - '400': - description: Bad request - '401': - description: Unauthorized - invalid API key - '429': - description: Rate limit exceeded - - /editions: - get: - summary: Get all editions - description: Returns all editions in the API (regionalized front pages) - operationId: getEditions - tags: - - Editions - parameters: - - $ref: '#/components/parameters/ApiKey' - - name: q - in: query - description: Return editions based on query term - schema: - type: string - example: "UK" - responses: - '200': - description: Successful response - content: - application/json: - schema: - $ref: '#/components/schemas/EditionsResponse' - '400': - description: Bad request - '401': - description: Unauthorized - invalid API key - '429': - description: Rate limit exceeded - - /{itemPath}: - get: - summary: Get single item by path - description: | - Returns all data for a single item (content, tag, or section) by its path. - The path matches the URL structure on theguardian.com. - operationId: getSingleItem - tags: - - Single Item - parameters: - - name: itemPath - in: path - required: true - description: | - The path to the item. Examples: - - Content: technology/2014/feb/18/doge-such-questions-very-answered - - Tag: world/france - - Section: lifeandstyle - schema: - type: string - example: "technology/2014/feb/18/doge-such-questions-very-answered" - - $ref: '#/components/parameters/ApiKey' - - name: show-story-package - in: query - description: Display related content in same story package - schema: - type: boolean - - name: show-editors-picks - in: query - description: Display editor-chosen content for tags/sections - schema: - type: boolean - - name: show-most-viewed - in: query - description: Display most viewed content - schema: - type: boolean - - name: show-related - in: query - description: Display related content items - schema: - type: boolean - - name: show-fields - in: query - description: Additional fields to include - schema: - type: array - items: - type: string - enum: [ - trailText, headline, showInRelatedContent, body, lastModified, - hasStoryPackage, score, standfirst, shortUrl, thumbnail, wordcount, - commentable, isPremoderated, allowUgc, byline, publication, - internalPageCode, productionOffice, shouldHideAdverts, liveBloggingNow, - commentCloseDate, starRating, all - ] - style: form - explode: false - - name: show-tags - in: query - description: Types of tags to include - schema: - type: array - items: - type: string - enum: [ - blog, contributor, keyword, newspaper-book, newspaper-book-section, - publication, series, tone, type, all - ] - style: form - explode: false - - name: show-sections - in: query - description: Include section metadata - schema: - type: boolean - - name: show-blocks - in: query - description: | - Include content blocks. Supports: - - main, body, all - - body:latest (default 20), body:latest:10 (limit 10) - - body:oldest, body:oldest:10 - - body: (specific block) - - body:around: (block + 20 around it) - - body:around::10 (block + 10 around it) - - body:key-events - - body:published-since: - schema: - type: array - items: - type: string - style: form - explode: false - - name: show-elements - in: query - description: Include media elements - schema: - type: array - items: - type: string - enum: [audio, image, video, all] - style: form - explode: false - - name: show-references - in: query - description: Include reference data - schema: - type: array - items: - type: string - enum: [ - author, bisac-prefix, esa-cricket-match, esa-football-match, - esa-football-team, esa-football-tournament, isbn, imdb, musicbrainz, - musicbrainzgenre, opta-cricket-match, opta-football-match, - opta-football-team, opta-football-tournament, pa-football-competition, - pa-football-match, pa-football-team, r1-film, reuters-index-ric, - reuters-stock-ric, witness-assignment - ] - style: form - explode: false - responses: - '200': - description: Successful response - content: - application/json: - schema: - $ref: '#/components/schemas/SingleItemResponse' - '400': - description: Bad request - '401': - description: Unauthorized - invalid API key - '404': - description: Item not found - '429': - description: Rate limit exceeded - -components: - securitySchemes: - ApiKeyAuth: - type: apiKey - in: query - name: api-key - description: API key for authentication - - parameters: - ApiKey: - name: api-key - in: query - required: true - description: Your Guardian API key - schema: - type: string - example: "test" - - schemas: - BaseResponse: - type: object - properties: - status: - type: string - description: API response status - enum: [ok, error] - example: "ok" - userTier: - type: string - description: User's API tier - example: "developer" - total: - type: integer - description: Total number of results available - example: 1 - required: - - status - - ContentItem: - type: object - properties: - id: - type: string - description: Unique identifier/path to the content - example: "world/2022/oct/21/russia-ukraine-war-latest-what-we-know-on-day-240-of-the-invasion" - type: - type: string - description: Type of content - example: "article" - sectionId: - type: string - description: ID of the section - example: "world" - sectionName: - type: string - description: Name of the section - example: "World news" - webPublicationDate: - type: string - format: date-time - description: Date and time of publication - example: "2022-10-21T14:06:14Z" - webTitle: - type: string - description: Title of the content - example: "Russia-Ukraine war latest: what we know on day 240 of the invasion" - webUrl: - type: string - format: uri - description: URL of the HTML content - example: "https://www.theguardian.com/world/2022/oct/21/russia-ukraine-war-latest-what-we-know-on-day-240-of-the-invasion" - apiUrl: - type: string - format: uri - description: URL of the API content - example: "https://content.guardianapis.com/world/2022/oct/21/russia-ukraine-war-latest-what-we-know-on-day-240-of-the-invasion" - isHosted: - type: boolean - description: Whether the content is hosted - example: false - pillarId: - type: string - description: ID of the pillar - example: "pillar/news" - pillarName: - type: string - description: Name of the pillar - example: "News" - fields: - $ref: '#/components/schemas/ContentFields' - tags: - type: array - items: - $ref: '#/components/schemas/Tag' - elements: - type: array - items: - $ref: '#/components/schemas/Element' - references: - type: array - items: - $ref: '#/components/schemas/Reference' - blocks: - $ref: '#/components/schemas/ContentBlocks' - required: - - id - - type - - webTitle - - webUrl - - apiUrl - - ContentFields: - type: object - description: Additional fields for content (when requested via show-fields) - properties: - trailText: - type: string - description: Trail text HTML - headline: - type: string - description: Headline HTML - showInRelatedContent: - type: string - description: Whether content can appear in related content (boolean as string) - body: - type: string - description: Full body content HTML - lastModified: - type: string - format: date-time - description: Last modification date - hasStoryPackage: - type: string - description: Whether content has related story package (boolean as string) - score: - type: string - description: Relevance score (float as string) - standfirst: - type: string - description: Standfirst HTML - shortUrl: - type: string - format: uri - description: Short URL - thumbnail: - type: string - format: uri - description: Thumbnail image URL - wordcount: - type: string - description: Word count (integer as string) - commentable: - type: string - description: Whether comments are allowed (boolean as string) - isPremoderated: - type: string - description: Whether comments are premoderated (boolean as string) - allowUgc: - type: string - description: Whether user generated content is allowed (boolean as string) - byline: - type: string - description: Byline HTML - publication: - type: string - description: Publication name - internalPageCode: - type: string - description: Internal page code - productionOffice: - type: string - description: Production office - shouldHideAdverts: - type: string - description: Whether to hide adverts (boolean as string) - liveBloggingNow: - type: string - description: Whether currently live blogging (boolean as string) - commentCloseDate: - type: string - format: date-time - description: Date when comments were closed - starRating: - type: string - description: Star rating (integer as string) - - ContentSearchResponse: - allOf: - - $ref: '#/components/schemas/BaseResponse' - - type: object - properties: - startIndex: - type: integer - description: Starting index of results - example: 1 - pageSize: - type: integer - description: Number of items per page - example: 10 - currentPage: - type: integer - description: Current page number - example: 1 - pages: - type: integer - description: Total number of pages - example: 1 - orderBy: - type: string - description: Sort order used - example: "newest" - results: - type: array - items: - $ref: '#/components/schemas/ContentItem' - required: - - results - - SingleItemResponse: - allOf: - - $ref: '#/components/schemas/BaseResponse' - - type: object - properties: - content: - $ref: '#/components/schemas/ContentItem' - leadContent: - type: array - description: Key pieces of lead content for tags - items: - $ref: '#/components/schemas/ContentItem' - storyPackage: - type: array - description: Related content in same story - items: - $ref: '#/components/schemas/ContentItem' - editorsPicks: - type: array - description: Editor-chosen content - items: - $ref: '#/components/schemas/ContentItem' - mostViewed: - type: array - description: Most viewed content - items: - $ref: '#/components/schemas/ContentItem' - relatedContent: - type: array - description: Related content items - items: - $ref: '#/components/schemas/ContentItem' - - Tag: - type: object - properties: - id: - type: string - description: Tag identifier - example: "katine/football" - type: - type: string - description: Type of tag - enum: [keyword, series, contributor, tone, type, blog] - example: "keyword" - webTitle: - type: string - description: Web title of the tag - example: "Football" - webUrl: - type: string - format: uri - description: Web URL of the tag - example: "http://www.theguardian.com/katine/football" - apiUrl: - type: string - format: uri - description: API URL of the tag - example: "http://beta.content.guardianapis.com/katine/football" - sectionId: - type: string - description: Section ID of the tag - example: "katine" - sectionName: - type: string - description: Section name of the tag - example: "Katine" - references: - type: array - items: - $ref: '#/components/schemas/Reference' - required: - - id - - type - - webTitle - - TagsResponse: - allOf: - - $ref: '#/components/schemas/BaseResponse' - - type: object - properties: - startIndex: - type: integer - description: Starting index of results - example: 1 - pageSize: - type: integer - description: Number of items per page - example: 10 - currentPage: - type: integer - description: Current page number - example: 1 - pages: - type: integer - description: Total number of pages - example: 7 - results: - type: array - items: - $ref: '#/components/schemas/Tag' - required: - - results - - Section: - type: object - properties: - id: - type: string - description: Section identifier - example: "football" - webTitle: - type: string - description: Web title of the section - example: "Football" - webUrl: - type: string - format: uri - description: Web URL of the section - example: "https://www.theguardian.com/football" - apiUrl: - type: string - format: uri - description: API URL of the section - example: "https://content.guardianapis.com/football" - editions: - type: array - items: - $ref: '#/components/schemas/Edition' - required: - - id - - webTitle - - SectionsResponse: - allOf: - - $ref: '#/components/schemas/BaseResponse' - - type: object - properties: - results: - type: array - items: - $ref: '#/components/schemas/Section' - required: - - results - - Edition: - type: object - properties: - id: - type: string - description: Edition identifier - example: "au" - path: - type: string - description: Path of the edition - example: "au" - edition: - type: string - description: Edition name - example: "AU" - webTitle: - type: string - description: Web title of the edition - example: "new guardian australia front page" - webUrl: - type: string - format: uri - description: Web URL of the edition - example: "https://www.theguardian.com/au" - apiUrl: - type: string - format: uri - description: API URL of the edition - example: "https://content.guardianapis.com/au" - code: - type: string - description: Edition code - example: "default" - required: - - id - - edition - - EditionsResponse: - allOf: - - $ref: '#/components/schemas/BaseResponse' - - type: object - properties: - results: - type: array - items: - $ref: '#/components/schemas/Edition' - required: - - results - - Element: - type: object - description: Media element (image, video, audio) - properties: - id: - type: string - description: Element identifier - relation: - type: string - description: Relationship to content - type: - type: string - enum: [image, video, audio] - description: Type of element - assets: - type: array - items: - $ref: '#/components/schemas/Asset' - - Asset: - type: object - description: Media asset within an element - properties: - type: - type: string - description: Asset type - mimeType: - type: string - description: MIME type of the asset - file: - type: string - format: uri - description: URL of the asset file - typeData: - type: object - description: Additional type-specific data - - Reference: - type: object - description: Reference data (ISBN, IMDB, etc.) - properties: - type: - type: string - description: Type of reference - enum: [ - author, bisac-prefix, esa-cricket-match, esa-football-match, - esa-football-team, esa-football-tournament, isbn, imdb, musicbrainz, - musicbrainzgenre, opta-cricket-match, opta-football-match, - opta-football-team, opta-football-tournament, pa-football-competition, - pa-football-match, pa-football-team, r1-film, reuters-index-ric, - reuters-stock-ric, witness-assignment - ] - id: - type: string - description: Reference identifier - - ContentBlocks: - type: object - description: Content blocks for articles and live blogs - properties: - main: - $ref: '#/components/schemas/Block' - body: - type: array - items: - $ref: '#/components/schemas/Block' - - Block: - type: object - description: Individual content block - properties: - id: - type: string - description: Block identifier - bodyHtml: - type: string - description: HTML content of the block - bodyTextSummary: - type: string - description: Text summary of the block - title: - type: string - description: Block title - attributes: - type: object - description: Block attributes - published: - type: boolean - description: Whether the block is published - createdDate: - type: string - format: date-time - description: Block creation date - firstPublishedDate: - type: string - format: date-time - description: First publication date - publishedDate: - type: string - format: date-time - description: Publication date - lastModifiedDate: - type: string - format: date-time - description: Last modification date - contributors: - type: array - items: - type: string - description: Block contributors - elements: - type: array - items: - $ref: '#/components/schemas/Element' - -tags: - - name: Content - description: Search and retrieve Guardian content - - name: Tags - description: Manage content tags and categories - - name: Sections - description: Browse content sections - - name: Editions - description: Access regional editions - - name: Single Item - description: Retrieve individual items by path \ No newline at end of file