Repository: candlecorp/wick (language: rust)

Issue:
Create Package for Types
**Is your feature request related to a problem? Please describe.**
We need the ability to allow types to be stored on the registry.

**Propose a solution**
Add "package" of "types" to be uploaded to the registry.

**Describe alternatives you've considered**
None

**Additional context**
None


Discussion / hints:

Fix patch:
diff --git a/Justfile b/Justfile
index 2383a6c7c..d93fc359f 100644
--- a/Justfile
+++ b/Justfile
@@ -83,7 +83,7 @@ integration-setup:
 	./etc/integration/mssql.sh up init
 	./etc/integration/registry.sh up init
 	./etc/integration/httpbin.sh up init
-	cargo run -p wick-cli -- reg push --debug ${DOCKER_REGISTRY}/test-component/baseline:0.1.0 ./crates/integration/test-baseline-component/component.yaml --insecure=${DOCKER_REGISTRY}
+	cargo run -p wick-cli -- reg push --debug ./crates/integration/test-baseline-component/component.yaml --insecure-oci=${DOCKER_REGISTRY}
 
 integration-teardown:
 	# docker rm -f simple_registry
diff --git a/crates/wick/wick-config/definitions/v1/manifest.apex b/crates/wick/wick-config/definitions/v1/manifest.apex
index b9a27b253..069f91620 100644
--- a/crates/wick/wick-config/definitions/v1/manifest.apex
+++ b/crates/wick/wick-config/definitions/v1/manifest.apex
@@ -267,14 +267,20 @@ type Url @tagged("wick/resource/url@v1") {
 
 "A type definition for a Wick Components and Operations"
 type TypesConfiguration @tagged("wick/types@v1") {
-  "The name of this component."
+  "The name of this type."
   name: string?
 
-  "Additional types to export and make available to the component."
+  "Associated metadata for this type."
+  metadata: Metadata?,
+
+  "Additional types to export and make available to the type."
   types: [TypeDefinition]
 
   "A list of operation signatures."
   operations: [OperationDefinition]
+
+  "Details about the package for this types."
+  package: PackageDefinition?,
 }
 
 "A configuration for a Wick Component"
diff --git a/crates/wick/wick-config/docs/v1.html b/crates/wick/wick-config/docs/v1.html
index 4ca26c38f..5a79f0d65 100644
--- a/crates/wick/wick-config/docs/v1.html
+++ b/crates/wick/wick-config/docs/v1.html
@@ -1165,13 +1165,27 @@ <h3 class=field-name>
   <a name="name">name</a>
 </h3>
 
-<span class="description field-description">The name of this component.</span>
+<span class="description field-description">The name of this type.</span>
 
 
 <ul class="field-notes">
 <li><span class="type field-note">Type: <span class=field-type>string<span class=optional>(Optional)</span></span></span></li>
 
 
+</ul>
+</div><div class=field>
+
+<h3 class=field-name>
+  <a name="metadata">metadata</a>
+</h3>
+
+<span class="description field-description">Associated metadata for this type.</span>
+
+
+<ul class="field-notes">
+<li><span class="type field-note">Type: <span class=field-type><a href="#Metadata">Metadata</a><span class=optional>(Optional)</span></span></span></li>
+
+
 </ul>
 </div><div class=field>
 
@@ -1179,7 +1193,7 @@ <h3 class=field-name>
   <a name="types">types</a>
 </h3>
 
-<span class="description field-description">Additional types to export and make available to the component.</span>
+<span class="description field-description">Additional types to export and make available to the type.</span>
 
 
 <ul class="field-notes">
@@ -1200,6 +1214,20 @@ <h3 class=field-name>
 <li><span class="type field-note">Type: <span class=field-type><a href="#OperationDefinition">OperationDefinition</a>[]</span></span></li>
 
 
+</ul>
+</div><div class=field>
+
+<h3 class=field-name>
+  <a name="package">package</a>
+</h3>
+
+<span class="description field-description">Details about the package for this types.</span>
+
+
+<ul class="field-notes">
+<li><span class="type field-note">Type: <span class=field-type><a href="#PackageDefinition">PackageDefinition</a><span class=optional>(Optional)</span></span></span></li>
+
+
 </ul>
 </div></div></div><div class="definition">
 <h2 class="definition-name type-name">
diff --git a/crates/wick/wick-config/json-schema/manifest.json b/crates/wick/wick-config/json-schema/manifest.json
index 40e97e733..0fc1a02d4 100644
--- a/crates/wick/wick-config/json-schema/manifest.json
+++ b/crates/wick/wick-config/json-schema/manifest.json
@@ -1142,12 +1142,17 @@
             ]
           },
           "name": {
-            "description": "The name of this component.",
+            "description": "The name of this type.",
             "required": false,
             "type": "string"
           },
+          "metadata": {
+            "description": "Associated metadata for this type.",
+            "required": false,
+            "$ref": "#/$defs/v1/Metadata"
+          },
           "types": {
-            "description": "Additional types to export and make available to the component.",
+            "description": "Additional types to export and make available to the type.",
             "type": "array",
             "items": {
               "$ref": "#/$defs/v1/TypeDefinition"
@@ -1159,6 +1164,11 @@
             "items": {
               "$ref": "#/$defs/v1/OperationDefinition"
             }
+          },
+          "package": {
+            "description": "Details about the package for this types.",
+            "required": false,
+            "$ref": "#/$defs/v1/PackageDefinition"
           }
         },
         "required": []
diff --git a/crates/wick/wick-config/json-schema/v1/manifest.json b/crates/wick/wick-config/json-schema/v1/manifest.json
index 2318cf145..d0d463df1 100644
--- a/crates/wick/wick-config/json-schema/v1/manifest.json
+++ b/crates/wick/wick-config/json-schema/v1/manifest.json
@@ -758,13 +758,19 @@
           "enum": ["wick/types@v1"]
         },
         "name": {
-          "description": "The name of this component.",
+          "description": "The name of this type.",
           "required": false,
 
           "type": "string"
         },
+        "metadata": {
+          "description": "Associated metadata for this type.",
+          "required": false,
+
+          "$ref": "#/$defs/v1/Metadata"
+        },
         "types": {
-          "description": "Additional types to export and make available to the component.",
+          "description": "Additional types to export and make available to the type.",
 
           "type": "array",
           "items": {
@@ -778,6 +784,12 @@
           "items": {
             "$ref": "#/$defs/v1/OperationDefinition"
           }
+        },
+        "package": {
+          "description": "Details about the package for this types.",
+          "required": false,
+
+          "$ref": "#/$defs/v1/PackageDefinition"
         }
       },
       "required": []
diff --git a/crates/wick/wick-config/src/config/types_config.rs b/crates/wick/wick-config/src/config/types_config.rs
index 3c21b9051..5384d7c15 100644
--- a/crates/wick/wick-config/src/config/types_config.rs
+++ b/crates/wick/wick-config/src/config/types_config.rs
@@ -1,23 +1,35 @@
 use std::collections::HashMap;
 use std::path::{Path, PathBuf};
 
-use asset_container::AssetManager;
+use asset_container::{AssetManager, Assets};
+use wick_asset_reference::AssetReference;
 use wick_interface_types::TypeDefinition;
 
+use super::common::package_definition::PackageConfig;
 use super::OperationSignature;
+use crate::config;
 
 #[derive(Debug, Clone, Builder, derive_asset_container::AssetManager, property::Property)]
 #[property(get(public), set(private), mut(disable))]
-#[asset(asset(crate::config::AssetReference))]
+#[asset(asset(AssetReference))]
 #[must_use]
 pub struct TypesConfiguration {
+  #[asset(skip)]
+  #[builder(setter(strip_option), default)]
+  pub(crate) name: Option<String>,
   #[asset(skip)]
   #[property(skip)]
   pub(crate) source: Option<PathBuf>,
   #[asset(skip)]
+  #[builder(default)]
+  #[property(skip)]
+  pub(crate) metadata: Option<config::Metadata>,
+  #[asset(skip)]
   pub(crate) types: Vec<TypeDefinition>,
   #[asset(skip)]
   pub(crate) operations: HashMap<String, OperationSignature>,
+  #[builder(default)]
+  pub(crate) package: Option<PackageConfig>,
 }
 
 impl TypesConfiguration {
@@ -45,6 +57,25 @@ impl TypesConfiguration {
     self.types.iter().find(|t| t.name() == name)
   }
 
+  /// Return the version of the application.
+  #[must_use]
+  pub fn version(&self) -> String {
+    self.metadata.clone().map(|m| m.version).unwrap_or_default()
+  }
+
+  /// Return the metadata of the component.
+  #[must_use]
+  pub fn metadata(&self) -> config::Metadata {
+    self.metadata.clone().unwrap()
+  }
+
+  /// Get the package files
+  #[must_use]
+  pub fn package_files(&self) -> Option<Assets<AssetReference>> {
+    // should return empty vec if package is None
+    self.package.as_ref().map(|p| p.assets())
+  }
+
   /// Set the source location of the configuration.
   pub fn set_source(&mut self, source: &Path) {
     let mut source = source.to_path_buf();
diff --git a/crates/wick/wick-config/src/v1.rs b/crates/wick/wick-config/src/v1.rs
index ad60a3d91..fc1f5adf0 100644
--- a/crates/wick/wick-config/src/v1.rs
+++ b/crates/wick/wick-config/src/v1.rs
@@ -585,12 +585,17 @@ pub(crate) struct Url {
 #[serde(deny_unknown_fields)]
 /// A type definition for a Wick Components and Operations
 pub(crate) struct TypesConfiguration {
-  /// The name of this component.
+  /// The name of this type.
 
   #[serde(default)]
   #[serde(skip_serializing_if = "Option::is_none")]
   pub(crate) name: Option<String>,
-  /// Additional types to export and make available to the component.
+  /// Associated metadata for this type.
+
+  #[serde(default)]
+  #[serde(skip_serializing_if = "Option::is_none")]
+  pub(crate) metadata: Option<Metadata>,
+  /// Additional types to export and make available to the type.
 
   #[serde(default)]
   #[serde(skip_serializing_if = "Vec::is_empty")]
@@ -600,6 +605,11 @@ pub(crate) struct TypesConfiguration {
   #[serde(default)]
   #[serde(skip_serializing_if = "Vec::is_empty")]
   pub(crate) operations: Vec<OperationDefinition>,
+  /// Details about the package for this types.
+
+  #[serde(default)]
+  #[serde(skip_serializing_if = "Option::is_none")]
+  pub(crate) package: Option<PackageDefinition>,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
diff --git a/crates/wick/wick-config/src/v1/conversions.rs b/crates/wick/wick-config/src/v1/conversions.rs
index 11e0e077a..467d0d3bb 100644
--- a/crates/wick/wick-config/src/v1/conversions.rs
+++ b/crates/wick/wick-config/src/v1/conversions.rs
@@ -75,6 +75,8 @@ impl TryFrom<v1::TypesConfiguration> for types_config::TypesConfiguration {
 
   fn try_from(value: v1::TypesConfiguration) -> std::result::Result<Self, Self::Error> {
     Ok(Self {
+      name: value.name,
+      metadata: value.metadata.try_map_into()?,
       types: value.types.try_map_into()?,
       operations: value
         .operations
@@ -82,6 +84,7 @@ impl TryFrom<v1::TypesConfiguration> for types_config::TypesConfiguration {
         .map(|op| Ok((op.name.clone(), op.try_into()?)))
         .collect::<Result<_>>()?,
       source: None,
+      package: value.package.try_map_into()?,
     })
   }
 }
diff --git a/crates/wick/wick-oci-utils/src/lib.rs b/crates/wick/wick-oci-utils/src/lib.rs
index 6231a4bd2..3095e5d0f 100644
--- a/crates/wick/wick-oci-utils/src/lib.rs
+++ b/crates/wick/wick-oci-utils/src/lib.rs
@@ -135,6 +135,8 @@ pub enum WickPackageKind {
   APPLICATION,
   /// A Wick component package.
   COMPONENT,
+  /// A Wick types package.
+  TYPES,
 }
 
 /// Retrieve a payload from an OCI url.
diff --git a/crates/wick/wick-package/src/package.rs b/crates/wick/wick-package/src/package.rs
index d0eff47fb..762245d48 100644
--- a/crates/wick/wick-package/src/package.rs
+++ b/crates/wick/wick-package/src/package.rs
@@ -130,7 +130,10 @@ impl WickPackage {
 
     let options = wick_config::FetchOptions::default();
     let config = WickConfiguration::fetch(path.to_string_lossy(), options).await?;
-    if !matches!(config, WickConfiguration::App(_) | WickConfiguration::Component(_)) {
+    if !matches!(
+      config,
+      WickConfiguration::App(_) | WickConfiguration::Component(_) | WickConfiguration::Types(_)
+    ) {
       return Err(Error::InvalidWickConfig(path.to_string_lossy().to_string()));
     }
     let full_path = tokio::fs::canonicalize(path)
@@ -168,6 +171,19 @@ impl WickPackage {
 
         (kind, name, version, annotations, parent_dir, media_type, registry)
       }
+      WickConfiguration::Types(config) => {
+        let name = config.name().cloned().ok_or(Error::NoName)?;
+        let version = config.version();
+        let annotations = metadata_to_annotations(&config.metadata());
+        let media_type = media_types::TYPES;
+        let kind = wick_oci_utils::WickPackageKind::TYPES;
+
+        extra_files = config.package_files().map_or_else(Vec::new, |files| files.to_owned());
+
+        let registry = config.package().and_then(|package| package.registry().cloned());
+
+        (kind, name, version, annotations, parent_dir, media_type, registry)
+      }
       _ => return Err(Error::InvalidWickConfig(path.to_string_lossy().to_string())),
     };
 


Test patch:
diff --git a/crates/wick/wick-package/tests/files/mytypes.wick b/crates/wick/wick-package/tests/files/mytypes.wick
new file mode 100644
index 000000000..626c18648
--- /dev/null
+++ b/crates/wick/wick-package/tests/files/mytypes.wick
@@ -0,0 +1,41 @@
+---
+name: mytypes
+metadata:
+  version: 0.1.0
+package:
+  registry:
+    registry: $DOCKER_REGISTRY
+    namespace: default
+kind: wick/types@v1
+types:
+  - name: HttpVersion
+    kind: wick/type/enum@v1
+    variants:
+      - name: HTTP_1_0
+        value: "1.0"
+      - name: HTTP_1_1
+        value: "1.1"
+      - name: HTTP_2_0
+        value: "2.0"
+  - name: StatusCode
+    kind: wick/type/enum@v1
+    variants:
+      - name: OK
+        value: "200"
+      - name: Created
+        value: "201"
+  - name: HttpResponse
+    kind: wick/type/struct@v1
+    fields:
+      - name: version
+        type: HttpVersion
+        description: HTTP version enum
+      - name: status
+        type: StatusCode
+        description: status code enum
+      - name: headers
+        type: "{string: string[]}"
+        description: All response headers. Supports duplicates.
+      - name: body
+        type: bytes
+        description: Response body in bytes. optional.
diff --git a/crates/wick/wick-package/tests/wick_package_types_integration.rs b/crates/wick/wick-package/tests/wick_package_types_integration.rs
new file mode 100644
index 000000000..df400b2f7
--- /dev/null
+++ b/crates/wick/wick-package/tests/wick_package_types_integration.rs
@@ -0,0 +1,100 @@
+mod integration_test {
+
+  use std::path::Path;
+  use std::time::{SystemTime, UNIX_EPOCH};
+
+  use anyhow::Result;
+  use walkdir::WalkDir;
+  use wick_oci_utils::package::PackageFile;
+  use wick_oci_utils::OciOptions;
+  use wick_package::WickPackage;
+
+  #[test_logger::test(tokio::test)]
+  async fn test_push_and_pull_wick_package() -> Result<()> {
+    let crate_dir = format!("{}/./tests/files", env!("CARGO_MANIFEST_DIR"));
+
+    let host = std::env::var("DOCKER_REGISTRY").unwrap();
+    let tempdir = std::env::temp_dir().join("test_push_and_pull_wick_package_types");
+    let _ = tokio::fs::remove_dir_all(&tempdir).await;
+    tokio::fs::create_dir_all(&tempdir).await.unwrap();
+
+    println!("Using tempdir: {:?}", tempdir);
+    let options = OciOptions::default()
+      .overwrite(true)
+      .base_dir(Some(tempdir.clone()))
+      .allow_insecure(vec![host.to_owned()]);
+
+    // Run the push operation
+    let package_path = Path::new("./tests/files/mytypes.wick");
+    println!("Package path: {:?}", package_path);
+    let mut package = WickPackage::from_path(package_path).await.unwrap();
+    let num_files = package.list_files().len();
+    assert_eq!(num_files, 1, "Mismatch in hard coded number of files");
+
+    // necessary to clone our WickPackage because push() consumes our contents and we
+    // want to test the original bytes post-push.
+    let expected = package.clone();
+    let test_timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+
+    let reference = expected
+      .tagged_reference(&test_timestamp.as_secs().to_string())
+      .unwrap();
+    let push_result = package.push(&reference, &options).await;
+    if push_result.is_err() {
+      panic!("Failed to push WickPackage: {:?}", push_result);
+    };
+    drop(package); // dropping it here to make sure tests use the clone `expected` instead.
+
+    // Run the pull operation
+    let pulled_package = WickPackage::pull(&reference, &options).await?;
+
+    // Check if the pulled package is the same as the pushed one
+    assert_eq!(
+      expected.list_files().len(),
+      pulled_package.list_files().len(),
+      "Mismatch in number of files between packages"
+    );
+    let mut root_dir = pulled_package.path().to_owned();
+    root_dir.pop();
+    println!("Counting files in pulled directory: {}", root_dir.display());
+    let num_files = WalkDir::new(root_dir).into_iter().count();
+
+    assert_eq!(num_files, 3, "Mismatch in number of files in the pulled package");
+
+    let pushed_files: Vec<&PackageFile> = expected.list_files();
+    let pulled_files: Vec<&PackageFile> = pulled_package.list_files();
+
+    // Sort both the pushed_files and pulled_files by path
+    let mut pushed_files_sorted = pushed_files.clone();
+    let mut pulled_files_sorted = pulled_files.clone();
+    pushed_files_sorted.sort_by_key(|file| file.path());
+    pulled_files_sorted.sort_by_key(|file| file.path());
+
+    for (pushed_file, pulled_file) in pushed_files_sorted.iter().zip(pulled_files_sorted.iter()) {
+      let pushed_file_path = pushed_file.path().to_str().unwrap().trim_start_matches(&crate_dir);
+      let pulled_file_path = pulled_file
+        .path()
+        .to_str()
+        .unwrap()
+        .trim_start_matches(tempdir.to_str().unwrap());
+      assert_eq!(pushed_file_path, pulled_file_path, "Mismatch in file paths");
+      //if pushed_file.path() ends with .tar.gz, don't compare hashes
+      if pushed_file.path().to_str().unwrap().ends_with(".tar.gz") {
+        continue;
+      }
+      println!("Comparing hashes for file: {:?}", pushed_file.path());
+      assert_eq!(pushed_file.hash(), pulled_file.hash(), "Mismatch in file hashes");
+      assert_eq!(
+        pushed_file.media_type(),
+        pulled_file.media_type(),
+        "Mismatch in file media types"
+      );
+      assert_eq!(
+        pushed_file.contents(),
+        pulled_file.contents(),
+        "Mismatch in file contents"
+      );
+    }
+    Ok(())
+  }
+}