Repository: andoriyu/uclicious (language: rust)

Issue:
Add FromStr support



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9ccdb79..3cded2c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -50,15 +50,3 @@ jobs:
         with:
           command: fmt
           args: --all -- --check
-  vale:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@master
-      - name: Vale
-        uses: errata-ai/vale-action@v1.0.4
-        env:
-          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
-        with:
-          styles: |
-            https://github.com/errata-ai/Microsoft/releases/latest/download/Microsoft.zip https://github.com/errata-ai/write-good/releases/latest/download/write-good.zip
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 21afaeb..4ebf681 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -6,7 +6,7 @@ jobs:
 - job: run_tests
   displayName: Run Unit Tests
   pool:
-    vmImage: ubuntu-16.04
+    vmImage: ubuntu-20.04
   strategy:
     matrix:
       stable:
@@ -33,14 +33,13 @@ jobs:
         sudo ln -s /home/vsts/.cargo/bin/cargo-suity /usr/local/bin/
       displayName: Install suity
     - script: |
-        CARGO=$(which cargo)
-        RUSTFLAGS="-C link-dead-code" $CARGO test --no-run
+        RUSTFLAGS="-C link-dead-code" cargo test --no-run
         mkdir test-results
-        sudo RUSTFLAGS="-C link-dead-code" $CARGO suity
+        RUSTFLAGS="-C link-dead-code" cargo suity
       displayName: Run Tests
     - script: |
         cargo install cargo-tarpaulin -f
-        sudo RUSTFLAGS="-C link-dead-code" $(which cargo) tarpaulin --out Xml
+        RUSTFLAGS="-C link-dead-code" cargo tarpaulin --out Xml
         bash <(curl -s https://codecov.io/bash)
       displayName: Run Coverage
       condition: and(succeeded(),eq(variables['rustup_toolchain'], 'stable'))
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..00bfd34
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,154 @@
+{
+  "nodes": {
+    "andoriyu": {
+      "inputs": {
+        "devshell": "devshell",
+        "flake-utils": "flake-utils",
+        "nixpkgs": [
+          "nixpkgs"
+        ],
+        "rust-overlay": [
+          "rust-overlay"
+        ]
+      },
+      "locked": {
+        "lastModified": 1635446728,
+        "narHash": "sha256-RV2oJoIWVnG9Qii99ByXT35CwJaQI2AznD5UYOduHmI=",
+        "owner": "andoriyu",
+        "repo": "flakes",
+        "rev": "9b3bf5f66f3d13930998b2b2ef67cc3a8c20d9fd",
+        "type": "github"
+      },
+      "original": {
+        "owner": "andoriyu",
+        "repo": "flakes",
+        "type": "github"
+      }
+    },
+    "devshell": {
+      "locked": {
+        "lastModified": 1634850494,
+        "narHash": "sha256-8bD1CgAJovKjb88I9F/Lxa2Q9VdZBEJYX5rUqHHbaP4=",
+        "owner": "numtide",
+        "repo": "devshell",
+        "rev": "d36e4ba27668f1620a5adebf5e0c724213a0f157",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "ref": "master",
+        "repo": "devshell",
+        "type": "github"
+      }
+    },
+    "devshell_2": {
+      "locked": {
+        "lastModified": 1634850494,
+        "narHash": "sha256-8bD1CgAJovKjb88I9F/Lxa2Q9VdZBEJYX5rUqHHbaP4=",
+        "owner": "numtide",
+        "repo": "devshell",
+        "rev": "d36e4ba27668f1620a5adebf5e0c724213a0f157",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "ref": "master",
+        "repo": "devshell",
+        "type": "github"
+      }
+    },
+    "flake-utils": {
+      "locked": {
+        "lastModified": 1634851050,
+        "narHash": "sha256-N83GlSGPJJdcqhUxSCS/WwW5pksYf3VP1M13cDRTSVA=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "c91f3de5adaf1de973b797ef7485e441a65b8935",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "flake-utils_2": {
+      "locked": {
+        "lastModified": 1634851050,
+        "narHash": "sha256-N83GlSGPJJdcqhUxSCS/WwW5pksYf3VP1M13cDRTSVA=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "c91f3de5adaf1de973b797ef7485e441a65b8935",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "flake-utils_3": {
+      "locked": {
+        "lastModified": 1623875721,
+        "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1635403963,
+        "narHash": "sha256-0actzfzBAXvvDJ/EvPSGbtCPXUwSObQrcq0RpsPWZgA=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "2deb07f3ac4eeb5de1c12c4ba2911a2eb1f6ed61",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "andoriyu": "andoriyu",
+        "devshell": "devshell_2",
+        "flake-utils": "flake-utils_2",
+        "nixpkgs": "nixpkgs",
+        "rust-overlay": "rust-overlay"
+      }
+    },
+    "rust-overlay": {
+      "inputs": {
+        "flake-utils": "flake-utils_3",
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1635387260,
+        "narHash": "sha256-8rOGoTZfF066m3OHWl/a1INOGwqeLlx8sSPDPBWjkrs=",
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "rev": "1347817f5d8e2cc42e8645015079916d68a884d9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..dd04696
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,51 @@
+{
+  description = "Minimal Rust Development Environment";
+  inputs = {
+    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+    flake-utils.url = "github:numtide/flake-utils";
+    rust-overlay = {
+      url = "github:oxalica/rust-overlay";
+      inputs = { nixpkgs.follows = "nixpkgs"; };
+    };
+    andoriyu = {
+      url = "github:andoriyu/flakes";
+      inputs = {
+        nixpkgs.follows = "nixpkgs";
+        rust-overlay.follows = "rust-overlay";
+      };
+    };
+    devshell.url = "github:numtide/devshell/master";
+  };
+  outputs =
+    { self, nixpkgs, rust-overlay, flake-utils, andoriyu, devshell, ... }:
+    flake-utils.lib.eachDefaultSystem (system:
+      let
+        cwd = builtins.toString ./.;
+        overlays = [ devshell.overlay rust-overlay.overlay andoriyu.overlay ];
+        pkgs = import nixpkgs { inherit system overlays; };
+        rust = pkgs.rust-bin.fromRustupToolchainFile "${cwd}/rust-toolchain.toml";
+      in with pkgs; {
+        devShell = clangStdenv.mkDerivation {
+        name = "rust";
+        nativeBuildInputs = [
+            clangStdenv
+            binutils
+            gnumake
+            cmake
+            openssl
+            openssl.dev
+            pkgconfig
+            rust
+            rust-analyzer
+            cargo-expand-nightly
+            cargo-release
+            git-cliff
+          ];
+          RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
+          OPENSSL_DIR = "${openssl.bin}/bin";
+          OPENSSL_LIB_DIR = "${openssl.out}/lib";
+          OPENSSL_INCLUDE_DIR = "${openssl.out.dev}/include";
+        };
+      });
+}
+
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..698a98c
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,4 @@
+[toolchain]
+channel = "stable"
+profile = "default"
+components = ["rust-src"]
diff --git a/uclicious_derive/src/bindings.rs b/uclicious_derive/src/bindings.rs
index 3a8d882..07506c8 100644
--- a/uclicious_derive/src/bindings.rs
+++ b/uclicious_derive/src/bindings.rs
@@ -54,6 +54,10 @@ pub fn from_object_trait() -> Path {
     syn::parse_str("::uclicious::FromObject").unwrap()
 }
 
+pub fn from_str_trait() -> Path {
+    syn::parse_str("::std::str::FromStr").unwrap()
+}
+
 /// Boxed error type
 pub fn boxed_error() -> Type {
     syn::parse_str("::std::boxed::Box<dyn ::std::error::Error>").unwrap()
diff --git a/uclicious_derive/src/initializer.rs b/uclicious_derive/src/initializer.rs
index 97bea31..9b43621 100644
--- a/uclicious_derive/src/initializer.rs
+++ b/uclicious_derive/src/initializer.rs
@@ -20,6 +20,7 @@ pub struct Initializer<'a> {
     pub from: Option<Path>,
     pub try_from: Option<Path>,
     pub map: Option<Path>,
+    pub from_str: bool,
 }
 
 impl<'a> ToTokens for Initializer<'a> {
@@ -55,21 +56,29 @@ impl<'a> Initializer<'a> {
         }
     }
     fn match_some(&'a self) -> MatchSome {
-        match (&self.validation, &self.from, &self.try_from, &self.map) {
-            (None, None, None, None) => MatchSome::Simple,
-            (Some(validation), None, None, None) => MatchSome::Validation(validation),
-            (None, Some(src_type), None, None) => MatchSome::From(src_type),
-            (None, None, Some(src_type), None) => MatchSome::TryFrom(src_type),
-            (Some(validation), Some(from), None, None) => {
+        match (
+            &self.validation,
+            &self.from,
+            &self.try_from,
+            &self.map,
+            &self.from_str,
+        ) {
+            (None, None, None, None, false) => MatchSome::Simple,
+            (Some(validation), None, None, None, false) => MatchSome::Validation(validation),
+            (None, Some(src_type), None, None, false) => MatchSome::From(src_type),
+            (None, None, Some(src_type), None, false) => MatchSome::TryFrom(src_type),
+            (Some(validation), Some(from), None, None, false) => {
                 MatchSome::FromValidation(from, validation)
             }
-            (Some(validation), None, Some(from), None) => {
+            (Some(validation), None, Some(from), None, false) => {
                 MatchSome::TryFromValidation(from, validation)
             }
-            (None, None, None, Some(map_func)) => MatchSome::Map(map_func),
-            (Some(validation), None, None, Some(map_func)) => {
+            (None, None, None, Some(map_func), false) => MatchSome::Map(map_func),
+            (Some(validation), None, None, Some(map_func), false) => {
                 MatchSome::MapValidation(map_func, validation)
             }
+            (None, None, None, None, true) => MatchSome::FromStr,
+            (Some(validation), None, None, None, true) => MatchSome::FromStrValidation(validation),
             _ => panic!(
                 "field {}: map, from and try_from are mutually exclusive",
                 self.field_ident
@@ -99,6 +108,8 @@ enum MatchSome<'a> {
     TryFromValidation(&'a Path, &'a Path),
     Map(&'a Path),
     MapValidation(&'a Path, &'a Path),
+    FromStr,
+    FromStrValidation(&'a Path),
 }
 
 impl<'a> ToTokens for MatchNone<'a> {
@@ -127,6 +138,8 @@ impl<'a> ToTokens for MatchSome<'a> {
         let into_trait = bindings::into_trait();
         let try_into_trait = bindings::try_into_trait();
         let object_error_ty = bindings::ucl_object_error();
+        let string_ty = bindings::string_ty();
+        let from_str_trait = bindings::from_str_trait();
         let quote = match self {
             MatchSome::Simple => quote!(#from_object::try_from(obj)?),
             MatchSome::Validation(path) => quote!(
@@ -160,6 +173,17 @@ impl<'a> ToTokens for MatchSome<'a> {
                 let v = #map_func(obj)?;
                 #validation(&lookup_path, &v).map(|_| v)?
             ),
+            MatchSome::FromStr => quote!(
+                let v: #string_ty = #from_object::try_from(obj)?;
+                #from_str_trait::from_str(&v)
+                        .map_err(|e| #object_error_ty::other(e))?
+            ),
+            MatchSome::FromStrValidation(validation) => quote!(
+                let v: #string_ty = #from_object::try_from(obj)?;
+                let v = #from_str_trait::from_str(&v)
+                        .map_err(|e| #object_error_ty::other(e))?;
+                #validation(&lookup_path, &v).map(|_| v)?
+            ),
         };
         tokens.append_all(quote);
     }
diff --git a/uclicious_derive/src/options.rs b/uclicious_derive/src/options.rs
index 6e1f704..c476a4d 100644
--- a/uclicious_derive/src/options.rs
+++ b/uclicious_derive/src/options.rs
@@ -220,6 +220,8 @@ pub struct Field {
     try_from: Option<Path>,
     #[darling(default)]
     map: Option<Path>,
+    #[darling(default)]
+    from_str: bool,
 }
 impl FlagVisibility for Field {
     fn public(&self) -> &Flag {
@@ -481,6 +483,7 @@ impl<'a> FieldWithDefaults<'a> {
             from: self.field.from.clone(),
             try_from: self.field.try_from.clone(),
             map: self.field.map.clone(),
+            from_str: self.field.from_str,
         }
     }
 }


Test patch:
diff --git a/tests/derive.rs b/tests/derive.rs
index e64a0be..1df2a28 100644
--- a/tests/derive.rs
+++ b/tests/derive.rs
@@ -1,3 +1,4 @@
+use std::net::{Ipv4Addr, SocketAddrV4};
 use std::ptr::slice_from_raw_parts;
 use uclicious::Uclicious;
 use uclicious::{variable_handlers, Priority, UclError, DEFAULT_DUPLICATE_STRATEGY};
@@ -97,6 +98,22 @@ fn include_chunk() {
     let test = Test::builder().unwrap().build().unwrap();
     assert_eq!("asd", test.key_one);
 }
+
+#[test]
+fn from_str() {
+    #[derive(Uclicious, Debug)]
+    #[ucl(include(chunk = r#"key_one = "0.0.0.0:8080""#))]
+    struct Test {
+        #[ucl(from_str)]
+        key_one: SocketAddrV4,
+    }
+
+    let socket = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 8080);
+
+    let test = Test::builder().unwrap().build().unwrap();
+    assert_eq!(socket, test.key_one);
+}
+
 #[test]
 fn include_chunk_with_macro() {
     #[derive(Uclicious, Debug)]