diff --git a/src/AnthropicClient/Json/SourceConverter.cs b/src/AnthropicClient/Json/SourceConverter.cs index 642295d..f8dd163 100644 --- a/src/AnthropicClient/Json/SourceConverter.cs +++ b/src/AnthropicClient/Json/SourceConverter.cs @@ -23,8 +23,12 @@ class SourceConverter : JsonConverter private static Source DeserializeBase64Source(JsonElement root, JsonSerializerOptions options) { - var mediaType = root.GetProperty("media_type").GetString() ?? throw new JsonException("Missing 'media_type' property"); + var mediaType = root.TryGetProperty("media_type", out var mediaTypeElement) + ? mediaTypeElement.GetString() ?? throw new JsonException("Missing 'media_type' property") + : throw new JsonException("Missing 'media_type' property"); + var isImage = ImageType.IsValidImageType(mediaType); + return isImage ? JsonSerializer.Deserialize(root.GetRawText(), options)! : JsonSerializer.Deserialize(root.GetRawText(), options)!; diff --git a/src/AnthropicClient/Models/Citation.cs b/src/AnthropicClient/Models/Citation.cs index db42878..e502e5b 100644 --- a/src/AnthropicClient/Models/Citation.cs +++ b/src/AnthropicClient/Models/Citation.cs @@ -30,11 +30,6 @@ public abstract class Citation [JsonPropertyName("document_title")] public string DocumentTitle { get; init; } = string.Empty; - [JsonConstructor] - internal Citation() - { - } - /// /// Initializes a new instance of the class with a specified type. /// diff --git a/tests/AnthropicClient.Tests/Data/EventTestData.cs b/tests/AnthropicClient.Tests/Data/EventTestData.cs index 6fe6a87..ea08c7f 100644 --- a/tests/AnthropicClient.Tests/Data/EventTestData.cs +++ b/tests/AnthropicClient.Tests/Data/EventTestData.cs @@ -24,7 +24,13 @@ public class EventTestData : IEnumerable Usage = new Usage { InputTokens = 472, OutputTokens = 91 }, StopReason = "tool_use", Content = [ - new TextContent("Okay, let's check the weather for San Francisco, CA:"), + new TextContent("Okay, let's check the weather for San Francisco, CA:") + { + Citations = [ + new CharacterLocationCitation(), + new CharacterLocationCitation(), + ] + }, new ToolUseContent() { Id = "toolu_01T1x1fJ34qAmk2tNTrN7Up6", @@ -380,6 +386,46 @@ public class EventTestData : IEnumerable }, }; + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"citations_delta","citation": {"type":"char_location","start_char_index":0,"end_char_index":0, "cited_text":"","document_index":0,"document_title":""}}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 0, + Delta = new CitationDelta() + { + Citation = new CharacterLocationCitation() + } + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"citations_delta","citation":{"type":"char_location","start_char_index":0,"end_char_index":0,"cited_text":"","document_index":0,"document_title":""}}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 0, + Delta = new CitationDelta() + { + Citation = new CharacterLocationCitation() + }, + }, + }, + }; + yield return new object[] { """ diff --git a/tests/AnthropicClient.Tests/Unit/Json/SourceConverterTests.cs b/tests/AnthropicClient.Tests/Unit/Json/SourceConverterTests.cs new file mode 100644 index 0000000..f62395c --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Json/SourceConverterTests.cs @@ -0,0 +1,31 @@ +namespace AnthropicClient.Tests.Unit.Json; + +public class SourceConverterTests : SerializationTest +{ + [Fact] + public void JsonDeserialization_WhenTypeIsUnknown_ItThrowsException() + { + var json = @"{ ""type"": ""unknown"" }"; + + var action = () => Deserialize(json); + + action.Should().Throw(); + } + + [Fact] + public void JsonSerialization_WhenSourceIsNotKnown_ItShouldHaveExpectedShape() + { + var source = new TestSource(); + + var result = Serialize(source); + + JsonAssert.Equal(@"{ ""type"": ""test"" }", result); + } + + private class TestSource : Source + { + public TestSource() : base("test") + { + } + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/Base64SourceTests.cs b/tests/AnthropicClient.Tests/Unit/Models/Base64SourceTests.cs index 1a6972b..33761d7 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/Base64SourceTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/Base64SourceTests.cs @@ -1,7 +1,13 @@ namespace AnthropicClient.Tests.Unit.Models; -public class Base64SourceTests +public class Base64SourceTests : SerializationTest { + private readonly string _testJson = @"{ + ""type"": ""base64"", + ""media_type"": ""application/pdf"", + ""data"": ""base64data"" + }"; + [Fact] public void Constructor_WhenCalledWithValidArguments_ItShouldSetProperties() { @@ -36,4 +42,47 @@ public class Base64SourceTests action.Should().Throw(); } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var mediaType = "application/pdf"; + var data = "base64data"; + var source = new Base64Source(mediaType, data); + + var result = Serialize(source); + + JsonAssert.Equal(_testJson, result); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape() + { + var source = Deserialize(_testJson); + + var base64Source = source.As(); + base64Source!.Type.Should().Be("base64"); + base64Source.MediaType.Should().Be("application/pdf"); + base64Source.Data.Should().Be("base64data"); + } + + [Fact] + public void JsonDeserialization_WhenMediaTypeIsMissing_ItShouldThrowException() + { + var json = @"{ ""type"": ""base64"", ""data"": ""base64data"" }"; + + var action = () => Deserialize(json); + + action.Should().Throw(); + } + + [Fact] + public void JsonDeserialization_WhenMediaTypeIsNull_ItShouldThrowException() + { + var json = @"{ ""type"": ""base64"", ""media_type"": null, ""data"": ""base64data"" }"; + + var action = () => Deserialize(json); + + action.Should().Throw(); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/CustomSourceTests.cs b/tests/AnthropicClient.Tests/Unit/Models/CustomSourceTests.cs index 53d8379..fd7dc0b 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/CustomSourceTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/CustomSourceTests.cs @@ -1,7 +1,17 @@ namespace AnthropicClient.Tests.Unit.Models; -public class CustomSourceTests +public class CustomSourceTests : SerializationTest { + private readonly string _testJson = @"{ + ""type"": ""content"", + ""content"": [ + { + ""type"": ""text"", + ""text"": ""Sample text"" + } + ] + }"; + [Fact] public void Constructor_WhenCalled_ItShouldSetProperties() { @@ -32,4 +42,30 @@ public class CustomSourceTests act.Should().Throw(); } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var content = new List + { + new("Sample text") + }; + var source = new CustomSource(content); + + var result = Serialize(source); + + JsonAssert.Equal(_testJson, result); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape() + { + var source = Deserialize(_testJson); + + var customSource = source.As(); + customSource!.Type.Should().Be("content"); + customSource.Content.Should().HaveCount(1); + customSource.Content[0].Type.Should().Be("text"); + customSource.Content[0].Text.Should().Be("Sample text"); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/TextSourceTests.cs b/tests/AnthropicClient.Tests/Unit/Models/TextSourceTests.cs index eacdff3..dd9780c 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/TextSourceTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/TextSourceTests.cs @@ -1,7 +1,13 @@ namespace AnthropicClient.Tests.Unit.Models; -public class TextSourceTests +public class TextSourceTests : SerializationTest { + private readonly string _testJson = @"{ + ""type"": ""text"", + ""media_type"": ""text/plain"", + ""data"": ""data"" + }"; + [Fact] public void Constructor_WhenCalled_ItShouldSetProperties() { @@ -11,4 +17,25 @@ public class TextSourceTests result.MediaType.Should().Be("text/plain"); result.Data.Should().Be("data"); } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var source = new TextSource("data"); + + var result = Serialize(source); + + JsonAssert.Equal(_testJson, result); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape() + { + var source = Deserialize(_testJson); + + var textSource = source.As(); + textSource!.Type.Should().Be("text"); + textSource.MediaType.Should().Be("text/plain"); + textSource.Data.Should().Be("data"); + } } \ No newline at end of file