Repository: Seddryck/Packata
Pull request #175: feat: basic definition of metrics
Author: login: Seddryck
Labels: name: new-feature
Created: 2025-05-09T17:14:44Z | Merged: 2025-05-09T20:00:42Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced support for defining metrics in data package schemas, including a new `Metric` class and a `Metrics` property on schemas.
  - Added example data packages in both JSON and YAML formats that include metric definitions.
- **Tests**
  - Added tests to validate deserialization of data packages with metrics in both JSON and YAML formats.
- **Documentation**
  - Improved XML documentation formatting for field properties.
- **Chores**
  - Updated project configuration to manage embedded resources and exclude certain files from compilation.
  - Refactored schema URL handling in a script for improved maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/package-delta-schemas.ps1 b/package-delta-schemas.ps1
index ea3b82d..8b62a3c 100644
--- a/package-delta-schemas.ps1
+++ b/package-delta-schemas.ps1
@@ -1,10 +1,11 @@
 $ErrorActionPreference = 'Stop'
 
+$baseUrl = "https://datapackage.org/profiles/2.0/"
 $schemas = @(
-    @{ Class = "Packata.Core.TableDialect";  Url = "https://datapackage.org/profiles/2.0/tabledialect.json"; Output = ".\.schemas\tabledialect.json"; Error = "Failed to generate TableDialect schema" },
-    @{ Class = "Packata.Core.Schema";        Url = "https://datapackage.org/profiles/2.0/tableschema.json";   Output = ".\.schemas\tableschema.json";   Error = "Failed to generate Table schema" },
-    @{ Class = "Packata.Core.Resource";      Url = "https://datapackage.org/profiles/2.0/dataresource.json";  Output = ".\.schemas\dataresource.json";  Error = "Failed to generate Resource schema" },
-    @{ Class = "Packata.Core.DataPackage";   Url = "https://datapackage.org/profiles/2.0/datapackage.json";   Output = ".\.schemas\datapackage.json";   Error = "Failed to generate DataPackage schema" }
+    @{ Class = "Packata.Core.TableDialect";  Value = "tabledialect.json"; Error = "Failed to generate TableDialect schema" },
+    @{ Class = "Packata.Core.Schema";        Value = "tableschema.json";  Error = "Failed to generate Table schema" },
+    @{ Class = "Packata.Core.Resource";      Value = "dataresource.json"; Error = "Failed to generate Resource schema" },
+    @{ Class = "Packata.Core.DataPackage";   Value = "datapackage.json";  Error = "Failed to generate DataPackage schema" }
 )
 
 $assemblyPath = ".\src\Packata.Core\bin\Release\net8.0\Packata.Core.dll"
@@ -18,7 +19,7 @@ $dir = ".\.schemas"
 foreach ($schema in $schemas) {
     try {
         Write-Host "Generating schema for $($schema.Class)..."
-        schemathief delta -a $assemblyPath -c $schema.Class -b $schema.Url -x "paths|profile" -o $schema.Output
+        schemathief delta -a $assemblyPath -c $schema.Class -b ($schema.Url + $schema.Value) -x "paths|profile" -o ($dir + $schema.Value)
     } catch {
         Write-Error $schema.Error
         exit 1
diff --git a/src/Packata.Core.Testing/Packata.Core.Testing.csproj b/src/Packata.Core.Testing/Packata.Core.Testing.csproj
index 6cead42..1970fe2 100644
--- a/src/Packata.Core.Testing/Packata.Core.Testing.csproj
+++ b/src/Packata.Core.Testing/Packata.Core.Testing.csproj
@@ -1,10 +1,19 @@
 ﻿<Project Sdk="Microsoft.NET.Sdk">
+  <ItemGroup>
+    <Compile Remove="PathHandling\**" />
+    <EmbeddedResource Remove="PathHandling\**" />
+    <None Remove="PathHandling\**" />
+  </ItemGroup>
   <ItemGroup>
     <None Remove="Serialization\Json\Resources\example.json" />
+    <None Remove="Serialization\Json\Resources\extension.json" />
+    <None Remove="Serialization\Yaml\Resources\extension.yaml" />
   </ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Serialization\Json\Resources\example.json" />
+    <EmbeddedResource Include="Serialization\Json\Resources\extension.json" />
     <EmbeddedResource Include="Serialization\Yaml\Resources\example.yaml" />
+    <EmbeddedResource Include="Serialization\Yaml\Resources\extension.yaml" />
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="Chrononuensis" Version="0.23.6" />
@@ -38,7 +47,4 @@
   <ItemGroup>
     <ProjectReference Include="..\..\src\Packata.Core\Packata.Core.csproj" />
   </ItemGroup>
-  <ItemGroup>
-    <Folder Include="PathHandling\" />
-  </ItemGroup>
 </Project>
diff --git a/src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs b/src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs
new file mode 100644
index 0000000..49bb50c
--- /dev/null
+++ b/src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs
@@ -0,0 +1,74 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
+using Newtonsoft.Json;
+using NUnit.Framework;
+using Packata.Core.Serialization;
+using SerJson = Packata.Core.Serialization.Json;
+using SerYaml = Packata.Core.Serialization.Yaml;
+using Packata.Core.Storage;
+
+namespace Packata.Core.Testing.Serialization;
+
+public class ExtensionSerializerTests
+{
+    private static Stream GetDataPackageProperties(string format)
+    {
+        var uformat = format.ToUpper()[0] + format.Substring(1);
+        var assembly = Assembly.GetExecutingAssembly();
+        var resourceName = $"{assembly.GetName().Name}.Serialization.{uformat}.Resources.extension.{format}";
+        var stream = assembly.GetManifestResourceStream(resourceName)
+            ?? throw new FileNotFoundException($"The embedded file {resourceName} doesn't exist.");
+        return stream;
+    }
+
+    private static IDataPackageSerializer GetSerializer(string format)
+        => format switch
+        {
+            "json" => new SerJson.DataPackageSerializer(),
+            "yaml" => new SerYaml.DataPackageSerializer(),
+            _ => throw new NotSupportedException($"The format '{format}' is not supported.")
+        };
+
+    public static IEnumerable<(Stream stream, IDataPackageSerializer serializer)> GetData()
+    {
+        yield return (GetDataPackageProperties("json"), GetSerializer("json"));
+        yield return (GetDataPackageProperties("yaml"), GetSerializer("yaml"));
+    }
+
+    [TestCaseSource(nameof(GetData))]
+    public void Deserialize_Package_Success((Stream Stream, IDataPackageSerializer Serializer) value)
+    {
+        using var streamReader = new StreamReader(value.Stream);
+        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
+        Assert.That(dataPackage, Is.Not.Null);
+        Assert.Multiple(() =>
+        {
+            Assert.That(dataPackage.Name, Is.EqualTo("extension_package"));
+        });
+    }
+
+    [TestCaseSource(nameof(GetData))]
+    public void Deserialize_Metrics_Success((Stream Stream, IDataPackageSerializer Serializer) value)
+    {
+        using var streamReader = new StreamReader(value.Stream);
+        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
+        Assert.That(dataPackage.Resources[0].Schema?.Metrics, Is.Not.Null);
+        var metrics = dataPackage.Resources[0].Schema?.Metrics;
+        Assert.Multiple(() =>
+        {
+            Assert.That(metrics, Has.Count.EqualTo(2));
+            Assert.That(metrics?[0].Name, Is.EqualTo("average_temperature"));
+            Assert.That(metrics?[0].Type, Is.EqualTo("numeric"));
+            Assert.That(metrics?[0].Title, Is.EqualTo("Average temperature"));
+            Assert.That(metrics?[0].Description, Is.EqualTo("Average temperature of the sensor"));
+            Assert.That(metrics?[0].Aggregation, Is.EqualTo("avg"));
+            Assert.That(metrics?[0].Expression, Is.EqualTo("temperature"));
+        });
+    }
+}
diff --git a/src/Packata.Core.Testing/Serialization/Json/Resources/extension.json b/src/Packata.Core.Testing/Serialization/Json/Resources/extension.json
new file mode 100644
index 0000000..60486be
--- /dev/null
+++ b/src/Packata.Core.Testing/Serialization/Json/Resources/extension.json
@@ -0,0 +1,69 @@
+﻿{
+    "$schema": "https://packata.dev/profiles/1.0/datapackage.json",
+    "name": "extension_package",
+    "id": "116f49c1-8603-463e-a908-68de98327266",
+    "version": "1.0",
+    "created": "2025-05-04T12:45:21Z",
+    "temporal": {
+        "start": "2020-01-01",
+        "end": "2021-01-01"
+    },
+    "resources": [
+        {
+            "name": "measures",
+            "path": "sensor_measures.csv",
+            "type": "table",
+            "$schema": "https://packata.dev/profiles/1.0/dataresource.json",
+            "title": "Sensors' temperatures & hygrometry",
+            "format": "csv",
+            "mediatype": "text/csv",
+            "encoding": "utf-8",
+            "schema": {
+                "fields": [
+                    {
+                        "name": "sensor_id",
+                        "type": "string"
+                    },
+                    {
+                        "name": "building",
+                        "type": "string"
+                    },
+                    {
+                        "name": "manufacturer",
+                        "type": "string"
+                    },
+                    {
+                        "name": "timestamp",
+                        "type": "datetime"
+                    },
+                    {
+                        "name": "temperature",
+                        "type": "numeric"
+                    },
+                    {
+                        "name": "hygrometry",
+                        "type": "numeric"
+                    }
+                ],
+                "metrics": [
+                    {
+                        "name": "average_temperature",
+                        "type": "numeric",
+                        "title": "Average temperature",
+                        "description": "Average temperature of the sensor",
+                        "aggregation": "avg",
+                        "expression": "temperature"
+                    },
+                    {
+                        "name": "max_temperature",
+                        "type": "numeric",
+                        "title": "Maximum temperature",
+                        "aggregation": "max",
+                        "expression": "temperature"
+                    }
+                ],
+                "$schema": "https://packata.dev/profiles/1.0/tableschema.json"
+            }
+        }
+    ]
+}
diff --git a/src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml b/src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml
new file mode 100644
index 0000000..b98f979
--- /dev/null
+++ b/src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml
@@ -0,0 +1,44 @@
+﻿"$schema": https://packata.dev/profiles/2.0/datapackage.json
+name: extension_package
+id: 116f49c1-8603-463e-a908-68de98327266
+version: 1.0
+created: 2025-05-04T12:45:21Z
+temporal:
+  start: "2020-01-01"
+  end: "2021-01-01"
+resources:
+- name: measures
+  path: sensor_measures.csv
+  type: table
+  "$schema": https://packata.dev/profiles/1.0/dataresource.json
+  title: "Sensors' temperatures & hygrometry"
+  format: csv
+  mediatype: text/csv
+  encoding: utf-8
+  schema:
+    fields:
+    - name: sensor_id
+      type: string
+    - name: building
+      type: string
+    - name: manufacturer
+      type: string
+    - name: timestamp
+      type: datetime
+    - name: temperature
+      type: numeric
+    - name: hygrometry
+      type: numeric
+    metrics:
+    - name: average_temperature
+      type: numeric
+      title: Average temperature
+      description: Average temperature of the sensor
+      aggregation: avg
+      expression: temperature
+    - name: max_temperature
+      type: numeric
+      title: Maximum temperature
+      aggregation: max
+      expression: temperature
+    "$schema": https://packata.dev/profiles/1.0/tableschema.json
diff --git a/src/Packata.Core/Field.cs b/src/Packata.Core/Field.cs
index 1f24353..36a7edc 100644
--- a/src/Packata.Core/Field.cs
+++ b/src/Packata.Core/Field.cs
@@ -13,7 +13,7 @@ public class Field
     public string? Name { get; set; }
 
     /// <summary>
-    /// The type keyword, which identifies the class inheriting from `Field` to deserialize",
+    /// The type keyword, which identifies the class inheriting from `Field` to deserialize
     /// </summary>
     public string? Type { get; set; }
 
@@ -23,12 +23,12 @@ public class Field
     public string? Format { get; set; }
 
     /// <summary>
-    /// "A human-readable title.
+    /// A human-readable title.
     /// </summary>
     public string? Title { get; set; }
 
     /// <summary>
-    /// "A text description. Markdown is encouraged.
+    /// A text description. Markdown is encouraged.
     /// </summary>
     public string? Description { get; set; }
 
diff --git a/src/Packata.Core/Metric.cs b/src/Packata.Core/Metric.cs
new file mode 100644
index 0000000..b506dfa
--- /dev/null
+++ b/src/Packata.Core/Metric.cs
@@ -0,0 +1,44 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Packata.Core;
+public class Metric
+{
+    /// <summary>
+    /// The name of the metric.
+    /// </summary>
+    public string? Name { get; set; }
+
+    /// <summary>
+    /// The type of the metric.
+    /// </summary>
+    public string? Type { get; set; }
+
+    /// <summary>
+    /// A human-readable title.
+    /// </summary>
+    public string? Title { get; set; }
+
+    /// <summary>
+    /// A text description. Markdown is encouraged.
+    /// </summary>
+    public string? Description { get; set; }
+
+    /// <summary>
+    /// The aggregation function to be used for the metric (e.g. SUM, MAX, COUNT).
+    /// </summary>
+    public string? Aggregation { get; set; }
+
+    /// <summary>
+    /// The expression to aggregate the metric. It can be a simple field name or a more complex expression.
+    /// </summary>
+    public string? Expression { get; set; }
+
+    /// <summary>
+    /// The dimension(s) to be used for the metric. It can be a single field name or an array of field names.
+    /// </summary>
+    public List<string>? Dimensions { get; set; }
+}
diff --git a/src/Packata.Core/Schema.cs b/src/Packata.Core/Schema.cs
index 802bf01..b4abfca 100644
--- a/src/Packata.Core/Schema.cs
+++ b/src/Packata.Core/Schema.cs
@@ -48,4 +48,6 @@ public class Schema
     ///     {"foreignKeys": [ { "fields": "state", "reference": { "fields": "id" } } ] }
     /// ]
     public List<ForeignKey>? ForeignKeys { get; set; } = null;
+
+    public List<Metric>? Metrics { get; set; }
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>src/Packata.Core/Metric.cs (1)</summary><blockquote>

`33-38`: **Consider adding validation for aggregation functions.**

The `Aggregation` property could benefit from validation constraints or enumeration of allowed values (SUM, MAX, COUNT, etc.) to ensure only supported aggregation functions are used.


You could implement this by using an enum instead of a string:

```diff
-    public string? Aggregation { get; set; }
+    public AggregationType? Aggregation { get; set; }
```

And define the enum:

```csharp
public enum AggregationType
{
    SUM,
    MAX,
    MIN,
    COUNT,
    AVG
    // Add other supported aggregation types
}
```

</blockquote></details>
<details>
<summary>src/Packata.Core/Schema.cs (1)</summary><blockquote>

`52-52`: **Consider initializing the Metrics property with an empty list.**

For consistency with the `Fields` property (line 13), consider initializing the `Metrics` property with an empty list instead of leaving it as null by default. This would provide a more consistent API experience.


```diff
-    public List<Metric>? Metrics { get; set; }
+    public List<Metric> Metrics { get; set; } = [];
```

</blockquote></details>
<details>
<summary>src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml (2)</summary><blockquote>

`5-5`: **Creation date is set in the future.**

The creation date is set to "2025-05-04T12:45:21Z", which is in the future. While this may be intentional for testing purposes, it's worth noting that this could cause issues if temporal validation is implemented.

---

`32-43`: **Good implementation of metrics array.**

The metrics array is well-structured with appropriate properties for name, type, title, description, aggregation, and expression. This aligns well with the feature goal of defining basic metrics.


However, I notice that the second metric `max_temperature` lacks a description property while the first one has it. Consider adding a description for consistency.

```diff
     - name: max_temperature
       type: numeric
       title: Maximum temperature
+      description: Maximum recorded temperature of the sensor
       aggregation: max
       expression: temperature
```

</blockquote></details>
<details>
<summary>src/Packata.Core.Testing/Serialization/Json/Resources/extension.json (1)</summary><blockquote>

`48-64`: **Well-structured metrics definition.**

The metrics array is properly defined with appropriate properties, matching the structure in the YAML file. Good job on keeping the definitions consistent between formats.


Similar to the YAML file, the second metric `max_temperature` is missing a description property while the first one has it. Consider adding a description for consistency.

```diff
                    {
                        "name": "max_temperature",
                        "type": "numeric",
                        "title": "Maximum temperature",
+                        "description": "Maximum recorded temperature of the sensor",
                        "aggregation": "max",
                        "expression": "temperature"
                    }
```

</blockquote></details>
<details>
<summary>src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs (1)</summary><blockquote>

`20-28`: **Resource stream retrieval could be more robust.**

The method to retrieve resource streams is well-structured, but could be improved for maintainability.

```diff
    private static Stream GetDataPackageProperties(string format)
    {
-        var uformat = format.ToUpper()[0] + format.Substring(1);
+        var uformat = char.ToUpper(format[0]) + format[1..];
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = $"{assembly.GetName().Name}.Serialization.{uformat}.Resources.extension.{format}";
-        var stream = assembly.GetManifestResourceStream(resourceName)
-            ?? throw new FileNotFoundException($"The embedded file {resourceName} doesn't exist.");
+        var stream = assembly.GetManifestResourceStream(resourceName);
+        if (stream == null)
+        {
+            // For debugging purposes, list available resources
+            var resources = assembly.GetManifestResourceNames();
+            var availableResources = string.Join(", ", resources);
+            throw new FileNotFoundException($"The embedded file {resourceName} doesn't exist. Available resources: {availableResources}");
+        }
        return stream;
    }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f75fe9ddeb2c82bfceb665b3174179d36ac46f0d and 6e36af73de7e22c0c355aeb139d4b359486e9c46.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `package-delta-schemas.ps1` (2 hunks)
* `src/Packata.Core.Testing/Packata.Core.Testing.csproj` (1 hunks)
* `src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs` (1 hunks)
* `src/Packata.Core.Testing/Serialization/Json/Resources/extension.json` (1 hunks)
* `src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml` (1 hunks)
* `src/Packata.Core/Field.cs` (2 hunks)
* `src/Packata.Core/Metric.cs` (1 hunks)
* `src/Packata.Core/Schema.cs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/Packata.Core/Schema.cs (1)</summary><blockquote>

<details>
<summary>src/Packata.Core/Metric.cs (1)</summary>

* `Metric` (8-44)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>src/Packata.Core/Metric.cs (1)</summary>

`1-44`: **New `Metric` class implementation looks good.**

The `Metric` class is well-structured with properly documented properties that capture all necessary aspects of metrics: name, type, title, description, aggregation function, expression, and dimensions.

</details>
<details>
<summary>src/Packata.Core/Field.cs (3)</summary>

`16-16`: **Documentation fix for Type property.**

Removed extraneous quotes from the documentation comment.

---

`26-26`: **Documentation fix for Title property.**

Removed extraneous quotes from the documentation comment.

---

`31-31`: **Documentation fix for Description property.**

Removed extraneous quotes from the documentation comment.

</details>
<details>
<summary>src/Packata.Core/Schema.cs (1)</summary>

`52-52`: **Added Metrics property to Schema class.**

The new nullable `Metrics` property allows schemas to include metric definitions, complementing the new `Metric` class.

</details>
<details>
<summary>src/Packata.Core.Testing/Packata.Core.Testing.csproj (2)</summary>

`2-6`: **Explicit exclusion of PathHandling directory.**

You're now explicitly excluding the PathHandling directory from compilation, embedded resources, and None item groups, which helps maintain a clean project structure.

---

`9-10`: **Added new test resources for metrics functionality.**

New embedded resources `extension.json` and `extension.yaml` have been added to support testing the new metrics functionality.




Also applies to: 14-16

</details>
<details>
<summary>package-delta-schemas.ps1 (1)</summary>

`3-8`: **Refactored URL handling with a centralized base URL.**

Good refactoring to extract the common base URL into a variable, which makes the code more maintainable.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T17:23:52Z

Review comments:
diff_hunk: @@ -18,7 +19,7 @@ $dir = ".\.schemas"
 foreach ($schema in $schemas) {
     try {
         Write-Host "Generating schema for $($schema.Class)..."
-        schemathief delta -a $assemblyPath -c $schema.Class -b $schema.Url -x "paths|profile" -o $schema.Output
+        schemathief delta -a $assemblyPath -c $schema.Class -b ($schema.Url + $schema.Value) -x "paths|profile" -o ($dir + $schema.Value)
path: package-delta-schemas.ps1
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Bug in schema generation command.**

The script tries to use `$schema.Url` in the command, but there is no 'Url' property defined in the schema entries.


```diff
-        schemathief delta -a $assemblyPath -c $schema.Class -b ($schema.Url + $schema.Value) -x "paths|profile" -o ($dir + $schema.Value)
+        schemathief delta -a $assemblyPath -c $schema.Class -b ($baseUrl + $schema.Value) -x "paths|profile" -o ($dir + $schema.Value)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        schemathief delta -a $assemblyPath -c $schema.Class -b ($baseUrl + $schema.Value) -x "paths|profile" -o ($dir + $schema.Value)
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T17:23:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+﻿"$schema": https://packata.dev/profiles/2.0/datapackage.json
path: src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Schema version inconsistency detected.**

The datapackage schema reference uses version 2.0 (`https://packata.dev/profiles/2.0/datapackage.json`), while the dataresource (line 13) and tableschema (line 44) both use version 1.0. This inconsistency should be resolved to maintain version compatibility.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for schema version inconsistencies in yaml files
echo "Checking schema versions in YAML files..."
find src -name "*.yaml" -exec grep -l "\$schema" {} \; | xargs cat | grep -o "profiles/[0-9]\.[0-9]"
```

Length of output: 340

---

🏁 Script executed:

```shell
#!/bin/bash
grep -R "profiles/1.0" -n src
```

Length of output: 1149

---



</details>

**Align schema version references in YAML extension**

There’s a mismatch in `extension.yaml` between the datapackage schema (2.0) and the resource/tableschema schemas (1.0). All schema URLs should use the same version.  

Files to update:  
- `src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml`  
  - Line 1: currently `"https://packata.dev/profiles/2.0/datapackage.json"`  
  - Line 13: `"https://packata.dev/profiles/1.0/dataresource.json"`  
  - Line 44: `"https://packata.dev/profiles/1.0/tableschema.json"`  
- For reference, JSON fixtures and code defaults use 1.0:  
  - `src/Packata.Core.Testing/Serialization/Json/Resources/extension.json` (all schemas 1.0)  
  - `Packata.Core/DataPackage.cs`, `Resource.Props.cs`, `TableDialect.cs` (default profiles 1.0)

Suggested fixes (pick one):  
- Downgrade the datapackage URL in `extension.yaml` to 1.0 to match the others.  
- Or, if you’re ready to adopt 2.0, bump the dataresource and tableschema URLs (and JSON fixtures/code defaults) to 2.0.  

Please update the URLs for consistent schema versions.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T17:23:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+﻿{
+    "$schema": "https://packata.dev/profiles/1.0/datapackage.json",
path: src/Packata.Core.Testing/Serialization/Json/Resources/extension.json
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Schema version inconsistency with YAML counterpart.**

The JSON file uses version 1.0 for the datapackage schema, while the YAML counterpart uses version 2.0. This inconsistency should be resolved to maintain consistency between the two formats.

---

🏁 Script executed:

```shell
#!/bin/bash
# Compare schema versions between JSON and YAML files
echo "JSON schema version:"
grep -o "profiles/[0-9]\.[0-9]/datapackage.json" src/Packata.Core.Testing/Serialization/Json/Resources/extension.json

echo "YAML schema version:"
grep -o "profiles/[0-9]\.[0-9]/datapackage.json" src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml
```

Length of output: 401

---



</details>

**Align JSON schema version with YAML (2.0)**

The JSON datapackage schema in `extension.json` is still on version 1.0, while the YAML counterpart uses 2.0. To keep both formats in sync, please update the JSON file:

- File: src/Packata.Core.Testing/Serialization/Json/Resources/extension.json  
  - Change line 2:
    ```diff
-   "$schema": "https://packata.dev/profiles/1.0/datapackage.json",
+   "$schema": "https://packata.dev/profiles/2.0/datapackage.json",
    ```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    "$schema": "https://packata.dev/profiles/2.0/datapackage.json",
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T17:23:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
+using Newtonsoft.Json;
+using NUnit.Framework;
+using Packata.Core.Serialization;
+using SerJson = Packata.Core.Serialization.Json;
+using SerYaml = Packata.Core.Serialization.Yaml;
+using Packata.Core.Storage;
+
+namespace Packata.Core.Testing.Serialization;
+
+public class ExtensionSerializerTests
+{
+    private static Stream GetDataPackageProperties(string format)
+    {
+        var uformat = format.ToUpper()[0] + format.Substring(1);
+        var assembly = Assembly.GetExecutingAssembly();
+        var resourceName = $"{assembly.GetName().Name}.Serialization.{uformat}.Resources.extension.{format}";
+        var stream = assembly.GetManifestResourceStream(resourceName)
+            ?? throw new FileNotFoundException($"The embedded file {resourceName} doesn't exist.");
+        return stream;
+    }
+
+    private static IDataPackageSerializer GetSerializer(string format)
+        => format switch
+        {
+            "json" => new SerJson.DataPackageSerializer(),
+            "yaml" => new SerYaml.DataPackageSerializer(),
+            _ => throw new NotSupportedException($"The format '{format}' is not supported.")
+        };
+
+    public static IEnumerable<(Stream stream, IDataPackageSerializer serializer)> GetData()
+    {
+        yield return (GetDataPackageProperties("json"), GetSerializer("json"));
+        yield return (GetDataPackageProperties("yaml"), GetSerializer("yaml"));
+    }
+
+    [TestCaseSource(nameof(GetData))]
+    public void Deserialize_Package_Success((Stream Stream, IDataPackageSerializer Serializer) value)
+    {
+        using var streamReader = new StreamReader(value.Stream);
+        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
+        Assert.That(dataPackage, Is.Not.Null);
+        Assert.Multiple(() =>
+        {
+            Assert.That(dataPackage.Name, Is.EqualTo("extension_package"));
+        });
+    }
+
+    [TestCaseSource(nameof(GetData))]
+    public void Deserialize_Metrics_Success((Stream Stream, IDataPackageSerializer Serializer) value)
+    {
+        using var streamReader = new StreamReader(value.Stream);
+        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
+        Assert.That(dataPackage.Resources[0].Schema?.Metrics, Is.Not.Null);
+        var metrics = dataPackage.Resources[0].Schema?.Metrics;
path: src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add null check before accessing metrics collection.**

While the code uses the null-conditional operator when retrieving the metrics, it doesn't ensure metrics is non-null before using it in assertions.

```diff
-        var metrics = dataPackage.Resources[0].Schema?.Metrics;
+        var metrics = dataPackage.Resources[0].Schema?.Metrics;
+        Assert.That(metrics, Is.Not.Null, "The metrics collection should not be null");
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        var metrics = dataPackage.Resources[0].Schema?.Metrics;
        Assert.That(metrics, Is.Not.Null, "The metrics collection should not be null");
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T17:23:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
+using Newtonsoft.Json;
+using NUnit.Framework;
+using Packata.Core.Serialization;
+using SerJson = Packata.Core.Serialization.Json;
+using SerYaml = Packata.Core.Serialization.Yaml;
+using Packata.Core.Storage;
+
+namespace Packata.Core.Testing.Serialization;
+
+public class ExtensionSerializerTests
+{
+    private static Stream GetDataPackageProperties(string format)
+    {
+        var uformat = format.ToUpper()[0] + format.Substring(1);
+        var assembly = Assembly.GetExecutingAssembly();
+        var resourceName = $"{assembly.GetName().Name}.Serialization.{uformat}.Resources.extension.{format}";
+        var stream = assembly.GetManifestResourceStream(resourceName)
+            ?? throw new FileNotFoundException($"The embedded file {resourceName} doesn't exist.");
+        return stream;
+    }
+
+    private static IDataPackageSerializer GetSerializer(string format)
+        => format switch
+        {
+            "json" => new SerJson.DataPackageSerializer(),
+            "yaml" => new SerYaml.DataPackageSerializer(),
+            _ => throw new NotSupportedException($"The format '{format}' is not supported.")
+        };
+
+    public static IEnumerable<(Stream stream, IDataPackageSerializer serializer)> GetData()
+    {
+        yield return (GetDataPackageProperties("json"), GetSerializer("json"));
+        yield return (GetDataPackageProperties("yaml"), GetSerializer("yaml"));
+    }
+
+    [TestCaseSource(nameof(GetData))]
+    public void Deserialize_Package_Success((Stream Stream, IDataPackageSerializer Serializer) value)
+    {
+        using var streamReader = new StreamReader(value.Stream);
+        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
+        Assert.That(dataPackage, Is.Not.Null);
+        Assert.Multiple(() =>
+        {
+            Assert.That(dataPackage.Name, Is.EqualTo("extension_package"));
+        });
+    }
+
+    [TestCaseSource(nameof(GetData))]
+    public void Deserialize_Metrics_Success((Stream Stream, IDataPackageSerializer Serializer) value)
+    {
+        using var streamReader = new StreamReader(value.Stream);
+        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
+        Assert.That(dataPackage.Resources[0].Schema?.Metrics, Is.Not.Null);
+        var metrics = dataPackage.Resources[0].Schema?.Metrics;
+        Assert.Multiple(() =>
+        {
+            Assert.That(metrics, Has.Count.EqualTo(2));
+            Assert.That(metrics?[0].Name, Is.EqualTo("average_temperature"));
+            Assert.That(metrics?[0].Type, Is.EqualTo("numeric"));
+            Assert.That(metrics?[0].Title, Is.EqualTo("Average temperature"));
+            Assert.That(metrics?[0].Description, Is.EqualTo("Average temperature of the sensor"));
+            Assert.That(metrics?[0].Aggregation, Is.EqualTo("avg"));
+            Assert.That(metrics?[0].Expression, Is.EqualTo("temperature"));
+        });
+    }
path: src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Enhance metric testing coverage.**

The test verifies the first metric's properties thoroughly but doesn't check all properties of the second metric.

```diff
    [TestCaseSource(nameof(GetData))]
    public void Deserialize_Metrics_Success((Stream Stream, IDataPackageSerializer Serializer) value)
    {
        using var streamReader = new StreamReader(value.Stream);
        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
        Assert.That(dataPackage.Resources[0].Schema?.Metrics, Is.Not.Null);
        var metrics = dataPackage.Resources[0].Schema?.Metrics;
        Assert.Multiple(() =>
        {
            Assert.That(metrics, Has.Count.EqualTo(2));
            
            // First metric checks
            Assert.That(metrics?[0].Name, Is.EqualTo("average_temperature"));
            Assert.That(metrics?[0].Type, Is.EqualTo("numeric"));
            Assert.That(metrics?[0].Title, Is.EqualTo("Average temperature"));
            Assert.That(metrics?[0].Description, Is.EqualTo("Average temperature of the sensor"));
            Assert.That(metrics?[0].Aggregation, Is.EqualTo("avg"));
            Assert.That(metrics?[0].Expression, Is.EqualTo("temperature"));
+            
+            // Second metric checks
+            Assert.That(metrics?[1].Name, Is.EqualTo("max_temperature"));
+            Assert.That(metrics?[1].Type, Is.EqualTo("numeric"));
+            Assert.That(metrics?[1].Title, Is.EqualTo("Maximum temperature"));
+            Assert.That(metrics?[1].Aggregation, Is.EqualTo("max"));
+            Assert.That(metrics?[1].Expression, Is.EqualTo("temperature"));
        });
    }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    [TestCaseSource(nameof(GetData))]
    public void Deserialize_Metrics_Success((Stream Stream, IDataPackageSerializer Serializer) value)
    {
        using var streamReader = new StreamReader(value.Stream);
        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
        Assert.That(dataPackage.Resources[0].Schema?.Metrics, Is.Not.Null);
        var metrics = dataPackage.Resources[0].Schema?.Metrics;
        Assert.Multiple(() =>
        {
            Assert.That(metrics, Has.Count.EqualTo(2));
            
            // First metric checks
            Assert.That(metrics?[0].Name,        Is.EqualTo("average_temperature"));
            Assert.That(metrics?[0].Type,        Is.EqualTo("numeric"));
            Assert.That(metrics?[0].Title,       Is.EqualTo("Average temperature"));
            Assert.That(metrics?[0].Description, Is.EqualTo("Average temperature of the sensor"));
            Assert.That(metrics?[0].Aggregation, Is.EqualTo("avg"));
            Assert.That(metrics?[0].Expression,  Is.EqualTo("temperature"));

            // Second metric checks
            Assert.That(metrics?[1].Name,        Is.EqualTo("max_temperature"));
            Assert.That(metrics?[1].Type,        Is.EqualTo("numeric"));
            Assert.That(metrics?[1].Title,       Is.EqualTo("Maximum temperature"));
            Assert.That(metrics?[1].Aggregation, Is.EqualTo("max"));
            Assert.That(metrics?[1].Expression,  Is.EqualTo("temperature"));
        });
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T17:23:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-09T17:14:50Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a `Metric` class to the core model and extends the `Schema` class with a `Metrics` property. New test resources in JSON and YAML formats are added, along with tests to validate metric deserialization. Project configuration is updated to embed these new resources and exclude certain files. Minor documentation comment corrections and a script refactor are also included.

## Changes

| File(s)                                                                                                 | Change Summary |
|---------------------------------------------------------------------------------------------------------|---------------|
| `src/Packata.Core/Metric.cs`                                                                            | New `Metric` class introduced, encapsulating metric properties such as name, type, aggregation, and expression. |
| `src/Packata.Core/Schema.cs`                                                                            | Added nullable `Metrics` property of type `List<Metric>?` to the `Schema` class. |
| `src/Packata.Core/Field.cs`                                                                             | Fixed XML documentation comments by removing extraneous leading quotes. |
| `src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs`                                    | Added new test class for deserializing and validating metrics from embedded JSON/YAML resources. |
| `src/Packata.Core.Testing/Serialization/Json/Resources/extension.json`,<br>`src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml` | Added new data package resource files with metric definitions for testing, in both JSON and YAML formats. |
| `src/Packata.Core.Testing/Packata.Core.Testing.csproj`                                                  | Updated project file to embed new resources and exclude `PathHandling` directory from compilation. |
| `package-delta-schemas.ps1`                                                                             | Refactored schema URL handling to use a base URL and simplified schema array entries. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant TestRunner as Test Runner
    participant ExtensionSerializerTests as ExtensionSerializerTests
    participant ResourceStream as Embedded Resource Stream
    participant Serializer as DataPackageSerializer
    participant DataPackage as DataPackage

    TestRunner->>ExtensionSerializerTests: Run Deserialize_Package_Success/Deserialize_Metrics_Success
    ExtensionSerializerTests->>ResourceStream: Retrieve embedded JSON/YAML resource
    ExtensionSerializerTests->>Serializer: Instantiate serializer (JSON/YAML)
    ExtensionSerializerTests->>Serializer: Deserialize resource stream
    Serializer->>DataPackage: Produce DataPackage object
    ExtensionSerializerTests->>DataPackage: Assert package and metrics properties
```

## Possibly related PRs

- Seddryck/Packata#168: Introduced the original `package-delta-schemas.ps1` script, which is directly refactored in this update.

## Suggested labels

`new-feature`

## Poem

> In fields of code where metrics bloom,  
> A rabbit hops from room to room.  
> With schemas new and tests in tow,  
> JSON and YAML both now show  
> How measures, metrics, all align—  
> In Packata’s core, the numbers shine!  
> 🐇✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Seddryck/Packata&utm_content=175):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-09T17:17:00Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Seddryck_Packata&pullRequest=175) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=Seddryck_Packata&pullRequest=175&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Seddryck_Packata&pullRequest=175&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Seddryck_Packata&pullRequest=175&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Seddryck_Packata&pullRequest=175&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Seddryck_Packata&pullRequest=175&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Seddryck_Packata&pullRequest=175)


author_association: NONE
user: login: codecov-commenter
created_at: 2025-05-09T17:23:10Z
body: ## [Codecov](https://app.codecov.io/gh/Seddryck/Packata/pull/175?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) Report
Attention: Patch coverage is `87.50000%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 76.93%. Comparing base [(`f75fe9d`)](https://app.codecov.io/gh/Seddryck/Packata/commit/f75fe9ddeb2c82bfceb665b3174179d36ac46f0d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) to head [(`6e36af7`)](https://app.codecov.io/gh/Seddryck/Packata/commit/6e36af73de7e22c0c355aeb139d4b359486e9c46?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/Seddryck/Packata/pull/175?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) | Patch % | Lines |
|---|---|---|
| [src/Packata.Core/Metric.cs](https://app.codecov.io/gh/Seddryck/Packata/pull/175?src=pr&el=tree&filepath=src%2FPackata.Core%2FMetric.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier#diff-c3JjL1BhY2thdGEuQ29yZS9NZXRyaWMuY3M=) | 85.71% | [1 Missing :warning: ](https://app.codecov.io/gh/Seddryck/Packata/pull/175?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #175      +/-   ##
==========================================
+ Coverage   76.88%   76.93%   +0.04%     
==========================================
  Files         105      106       +1     
  Lines        1787     1795       +8     
  Branches      283      283              
==========================================
+ Hits         1374     1381       +7     
- Misses        318      319       +1     
  Partials       95       95              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Seddryck/Packata/pull/175?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE