Skip to content
Commits on Source (60)
......@@ -2,9 +2,9 @@
set -ex
# verify:check, verify:format and verify:lint
echo '+make verify:all'
make verify:all
# vet:check, vet:format and vet:lint
echo '+make vet:all'
make vet:all
echo '+make test:all'
make test:all
blacklisted-names = ["foo", "baz", "quux"]
disallowed-names = ["foo", "baz", "quux"]
type-complexity-threshold = 300 # default: 250
stages:
- verify
- vet
- test
- build
- pages
.vet-tools: &vet-tools
image: grauwoelfchen/rust-vet-tools:stable
image: registry.gitlab.com/grauwoelfchen/portolan/rust-vet-tools:stable
except:
- tags
check:
stage: verify
stage: vet
<<: *vet-tools
before_script:
- rustc --version
- cargo --version
- mkdir -p .git/hooks
script:
- make verify:check
- make vet:check
format:
stage: verify
stage: vet
<<: *vet-tools
before_script:
- rustc --version
......@@ -27,10 +28,10 @@ format:
- cargo fmt --version
- mkdir -p .git/hooks
script:
- make verify:format
- make vet:format
lint:
stage: verify
stage: vet
<<: *vet-tools
before_script:
- rustc --version
......@@ -38,9 +39,9 @@ lint:
- cargo clippy --version
- mkdir -p .git/hooks
script:
- make verify:lint
- make vet:lint
test:
test-doc:
stage: test
<<: *vet-tools
before_script:
......@@ -49,14 +50,24 @@ test:
- kcov --version
- mkdir -p .git/hooks
script:
# test & get covered
- KCOV_PATH=/usr/bin/kcov make coverage
- make test:doc
test-unit:
stage: test
<<: *vet-tools
before_script:
- rustc --version
- cargo --version
- kcov --version
- mkdir -p .git/hooks
script:
- make coverage:lib
after_script:
- cat target/coverage/index.json
- cat target/coverage/lib/index.js
build:
build-debug:
stage: build
image: grauwoelfchen/rust:stable
image: registry.gitlab.com/grauwoelfchen/portolan/rust:stable
before_script:
- rustc --version
- cargo --version
......@@ -66,11 +77,11 @@ build:
cache:
untracked: true
only:
- master
- trunk
release:
build-release:
stage: build
image: grauwoelfchen/rust:stable
image: registry.gitlab.com/grauwoelfchen/portolan/rust:stable
before_script:
- rustc --version
- cargo --version
......@@ -81,3 +92,24 @@ release:
untracked: true
only:
- tags
pages:
stage: pages
image: grauwoelfchen/rust:stable
before_script:
- rustc --version
- cargo --version
- rustdoc --version
script:
- make doc
- echo "<meta http-equiv=\"refresh\" content=\"0; url=adequate\">" \
> ./target/doc/index.html
- mkdir public
- cp -R ./target/doc/* public/
artifacts:
untracked: true
paths:
- public
only:
- tags
- trunk
{
"baseBranches": ["trunk"],
"enabledManagers": ["cargo"],
"branchPrefix": "dep-",
"draftPR": true,
"labels": ["dependency"],
"timezone": "UTC",
"schedule": [
"before 8:00pm"
]
}
#!/bin/sh
set -x
dir_name=$(dirname $(readlink -f $0))
job_name="${1}"
env_file="${ENV_FILE:-.env.ci}"
env_opts="";
if [ -z "${job_name}" ]; then
echo "Please specify job name in .gitlab-ci.yml"
exit 2
fi
if [ ! -f "${env_file}" ]; then
echo "Please create \`${env_file}\` (cp ${env_file}.sample ${env_file})"
exit 2
fi
while read line; do
env_opts+=" --env ${line}"
done < "${env_file}"
# ci-runner <job>
echo "${env_opts} ${job_name}" | \
xargs $dir_name/gitlab-runner exec docker \
--cache-dir /cache \
--docker-privileged \
--docker-volumes $dir_name/../tmp/_cache:/cache \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
#!/bin/bash
set -eu
output_dir="$(pwd)/target/coverage"
target_dir="$(pwd)/target/debug"
if [ -f "${output_dir}/index.json" ]; then
rm "${output_dir}/index.json"
fi
if [ -n "${2:-}" ]; then
kcov_cmd="${2}"
elif [ ! -z "${KCOV_PATH:-}" ]; then
kcov_cmd="${KCOV_PATH}"
else
kcov_dir="$(dirname $(readlink -f "${0}"))/kcov"
kcov_cmd="${kcov_dir}/bin/kcov"
fi
echo "$kcov_cmd"
prefix="${1}"
for file in $(ls $target_dir/$prefix* | grep -v '\.d$'); do
echo $file
$kcov_cmd --verify --include-path=$(pwd)/src \
"$output_dir" "$file"
grep -E "$(basename $file)" "$output_dir"/index.json | \
grep -oE 'covered":"([0-9]*\.[0-9]*|[0-9]*)"' | \
grep -oE '[0-9]*\.[0-9]*|[0-9]*'
done
#!/bin/sh
set -eu
bin_dir=$(dirname $(readlink -f "${0}"))
name="gitlab-runner"
platform="linux-amd64"
version="latest"
indent() {
sed -u 's/^/ /'
}
echo "Platform: ${platform}"
echo "Version: ${version}"
echo ""
echo "-----> Installing into: ${bin_dir}"
location_base="https://gitlab-runner-downloads.s3.amazonaws.com"
location="${location_base}/${version}/binaries/${name}-${platform}"
curl -sL $location -o $bin_dir/$name
chmod +x $_
echo "Done" | indent
#!/bin/bash
set -eu
# NOTE:
# if set KCOV_DISCARD_CACHE=true, then it will force installing kcov)
renew="${KCOV_DISCARD_CACHE:-false}"
kcov_dir="$(dirname $(readlink -f "${0}"))/kcov"
kcov_cmd="${KCOV_PATH:-${kcov_dir}/bin/kcov}"
kcov_url="https://github.com/SimonKagstrom/kcov/archive"
kcov_ver="v34"
if [[ -f "${kcov_cmd}" && "${renew}" != "true" ]]; then
echo "kcov already installed in ${kcov_cmd}"
else
rm -fr $kcov_dir
mkdir $kcov_dir
cd $kcov_dir
curl -sLO ${kcov_url}/${kcov_ver}.tar.gz
mkdir $kcov_ver
tar zxvf ${kcov_ver}.tar.gz -C $kcov_ver --strip-components=1
cd $kcov_ver
mkdir build
cmake \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/
make
make install DESTDIR=../
fi
Sat 26 Aug 2023 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Reduce the use of String and Option<String> in structs. So that
users can reuse built validator function without creating unnecessary string
objects again
* Create Validator and OptionalValidator types
* Remove public access to ValidatinResult type
Wed 14 Sep 2022 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Fix warnings by checks of the latest clippy (stable)
Sun 06 Jun 2021 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Change error messages for max/min validations (contain -> have)
* Add a new validation method "not_contain_if_given"
Sat 29 May 2021 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Add a new validation method "contains_if_given"
Sat 22 May 2021 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Add a new validation method "contains_if_present"
Thu 20 May 2021 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Add a new validation method "contains"
Wed 19 May 2021 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* New release v0.1.2
* Fix bugs in PartialEq for Error, Feedback and Message struct
* Split modules into separated files (error, feedback and message)
* Add .renovaterc.json (dependencies check by renovate-runner)
Sun 02 May 2021 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Add docs (pages) for adequate.yasha.rs
* Update base image for CI (use them from registry.gitlab.com)
* Remove .tool
Wed 06 Jan 2021 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Change the default branch as `trunk`
Fri 21 Nov 2020 Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>
* New release v0.1.1
* Add a new length validation methods "{max,min}_if_present"
......
......@@ -2,7 +2,7 @@
# It is not intended for manual editing.
[[package]]
name = "adequate"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"cargo-husky",
"lazy_static",
......
......@@ -4,7 +4,7 @@ description = """\
A yet another validation library provides a macro inspired by \
Accord.\
"""
version = "0.1.1"
version = "0.1.2"
authors = ["Yasuhiro Яша Asaka <yasuhiro.asaka@grauwoelfchen.net>"]
edition = "2018"
homepage = "https://gitlab.com/grauwoelfchen/adequate"
......@@ -16,14 +16,22 @@ repository = "https://gitlab.com/grauwoelfchen/adequate"
license = "Apache-2.0"
exclude = [
".cargo-huskey/*",
".cargo-husky",
".cache",
"Makefile",
"/.env",
"/.env.ci.sample",
"/.clippy.toml",
"/.rustfmt.toml",
"rust-toolchain",
".env",
".env.ci.sample",
".gitignore",
".clippy.toml",
".rustfmt.toml",
".gitlab-ci.yml",
".renovaterc.json",
]
[badges]
gitlab = { repository = "grauwoelfchen/adequate", branch = "trunk" }
[dependencies]
lazy_static = "1.4"
strfmt = "0.1.6"
......
This diff is collapsed.
# verify
verify\:check: ## Verify code syntax [alias: check]
PACKAGE = adequate
# vet
vet\:check: # vet code syntax [synonym: check]
@cargo check --all --verbose
.PHONY: verify\:check
.PHONY: vet\:check
check: | verify\:check
check: vet\:check
.PHONY: check
verify\:format: ## Verify format without changes [alias: verify:fmt, format, fmt]
vet\:format: # vet format without changes [synonym: vet:fmt, format, fmt]
@cargo fmt --all -- --check
.PHONY: verify\:format
.PHONY: vet\:format
format: | verify\:format
format: vet\:format
.PHONY: format
fmt: | verify\:format
fmt: vet\:format
.PHONY: fmt
verify\:lint: ## Verify coding style using clippy [alias: lint]
vet\:lint: # vet coding style using clippy [synonym: lint]
@cargo clippy --all-targets
.PHONY: verify\:lint
.PHONY: vet\:lint
lint: | verify\:lint
lint: vet\:lint
.PHONY: lint
verify\:all: | verify\:check verify\:format verify\:lint ## Check code using all verify:xxx targets [alias: verify]
.PHONY: verify\:all
vet\:all: vet\:check vet\:format vet\:lint # Check code using all vet targets
.PHONY: vet\:all
verify: | verify\:all
.PHONY: verify
vet: vet\:check # Alias for vet:check
.PHONY: vet
# test
test\:all: ## Run all unit tests [alias: test]
test\:doc: # Run only doc tests
@cargo test --doc
.PHONY: test\:doc
test\:lib: # Run unit tests
@cargo test --tests
.PHONY: test\:all
.PHONY: test\:lib
test: | test\:all
test: test\:lib # Alias for test:lib
.PHONY: test
test\:all: test\:doc test\:lib # Run all tests
.PHONY: test\:all
# coverage
coverage: ## Generate coverage report of tests [alias: cov]
@cargo test --lib adequate --no-run
@./.tool/setup-kcov
./.tool/get-covered libadequate
coverage\:lib: # Generate a coverage report of tests for library [synonym: cov:lib]
@set -uo pipefail; \
dir="$$(pwd)"; \
target_dir="$${dir}/target/coverage/lib"; \
cargo test --lib --no-run --target-dir=$${target_dir}; \
result=($${target_dir}/index.js*); \
if [ -f $${result}[0] ]; then \
rm "$${target_dir}/index.js*"; \
fi; \
file=($$target_dir/debug/deps/$(PACKAGE)-*); \
kcov --verify --include-path=$$dir/src $$target_dir $${file[0]}; \
grep 'index.html' $$target_dir/index.js* | \
grep --only-matching --extended-regexp \
'covered":"([0-9]*\.[0-9]*|[0-9]*)"' | sed -E 's/[a-z\:"]*//g'
.PHONY: coverage\:lib
cov\:lib: coverage\:lib
.PHONY: cov\:lib
coverage: coverage\:lib # Alias for coverage:lib [synonym: cov]
.PHONY: coverage
cov: | coverage
cov: coverage
.PHONY: cov
# documentation
document: # Generate documentation files [synonym: doc]
cargo doc --no-deps --package $(PACKAGE)
.PHONY: document
doc: document
.PHONY: doc
# }}}
# build
build\:debug: ## Build in debug mode [alias: build]
build\:debug: # Build in debug mode
cargo build
.PHONY: build\:debug
build: | build\:debug
build: build\:debug # Alias for build:debug
.PHONY: build
build\:release: ## Create release build
build\:release: # Create release build
cargo build --release
.PHONY: build\:release
# utility
clean: ## Clean up
clean: # Clean up
@cargo clean
.PHONY: clean
package: ## Create package
# NOTE:
# This depends on environment variables from .env.ci, and requires
# the gitlab-runner command.
runner-%: # Run a CI job on local (on Docker)
@set -uo pipefail; \
job=$(subst runner-,,$@); \
opt=""; \
while read line; do \
opt+=" --env $$(echo $$line | sed -E 's/^export //')"; \
done < .env.ci; \
gitlab-runner exec docker \
--executor docker \
--cache-dir /cache \
--docker-volumes $$(pwd)/.cache/gitlab-runner:/cache \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock \
$${opt} $${job}
.PHONY: runner
package: # Create package
@cargo package
.PHONY: package
help: ## Display this message
@grep -E '^[0-9a-z\:\\]+: ' $(MAKEFILE_LIST) | \
grep -E ' ## ' | \
sed -e 's/\(\s|\(\s[0-9a-z\:\\]*\)*\) / /' | \
tr -d \\\\ | \
awk 'BEGIN {FS = ": ## "}; \
{printf "\033[38;05;222m%-14s\033[0m %s\n", $$1, $$2}' | \
sort
publish: # Publish package
@cargo publish
.PHONY: publish
help: # Display this message
@grep --extended-regexp '^[0-9a-z\:\\\%]+: ' $(firstword $(MAKEFILE_LIST)) | \
grep --extended-regexp ' # ' | \
sed --expression='s/\([a-z0-9\-\:\ ]*\): \([a-z0-9\-\:\ ]*\) #/\1: #/g' | \
tr --delete \\\\ | \
awk 'BEGIN {FS = ": # "}; \
{printf "\033[38;05;222m%-14s\033[0m %s\n", $$1, $$2}' | \
sort
.PHONY: help
.DEFAULT_GOAL = test:all
default: verify\:check verify\:format verify\:lint test\:all
.DEFAULT_GOAL = test
default: test
# Adequate
[![pipeline](
https://gitlab.com/grauwoelfchen/adequate/badges/master/pipeline.svg)](
https://gitlab.com/grauwoelfchen/adequate/commits/master) [![coverage](
https://gitlab.com/grauwoelfchen/adequate/badges/master/coverage.svg)](
https://gitlab.com/grauwoelfchen/adequate/commits/master)
https://gitlab.com/grauwoelfchen/adequate/badges/trunk/pipeline.svg)](
https://gitlab.com/grauwoelfchen/adequate/commits/trunk) [![coverage](
https://gitlab.com/grauwoelfchen/adequate/badges/trunk/coverage.svg)](
https://gitlab.com/grauwoelfchen/adequate/commits/trunk) [![crate::adequate](
https://img.shields.io/crates/v/adequate?label=crates&style=flat)](
https://crates.io/crates/adequate) [![doc::adequate](
https://docs.rs/adequate/badge.svg)](https://docs.rs/crate/adequate)
A yet another validation library provides a macro inspired by [Accord](
https://github.com/ChrisBuchholz/accord).
## Repos
## Repositories
The source code is hosted in several repos, but developed mainly on [
GitLab.com](https://gitlab.com/grauwoelfchen/adequate).
This is mainly developed on [GitLab.com](
https://gitlab.com/grauwoelfchen/adequate), but the source code is hosted also
in several following repositories.
The merge/pull requests or issues on any repository are welcomed.
Any merge/pull requests or issues on any repository are welcomed.
* https://gitlab.com/grauwoelfchen/adequate
* https://github.com/grauwoelfchen/adequate
* https://git.sr.ht/~grauwoelfchen/adequate
```zsh
# the main branch is "trunk"
% git clone git@gitlab.com:grauwoelfchen/adequate.git
% git --no-pager branch -v
* trunk xxxxxxx XXX
```
## Installation
......@@ -36,18 +46,25 @@ See `src/validation` directory for validators.
use adequate::validation::length;
// inputs
let fullname = "Albrecht Dürer".to_string();
let username = "albrecht".to_string();
let v1 = "Albrecht Dürer";
let v2 = "albrecht";
let result = validate! {
"fullname" => fullname => [length::max(3)],
"username" => username => [length::within(3..9)]
"fullname" => v1 => [length::max(3)],
"username" => v2 => [length::within(3..9)],
};
assert!(result.is_err());
```
### Validations
###### Contain
* contains
* contains_if_present
* contains_if_given
* not_contain_if_given
###### Length
* max
......@@ -68,17 +85,20 @@ Check `make help`
## Development
### Verify
### vet
```zsh
# check code using all verify:xxx targets
% make verify:all
# check code using all vet:xxx targets
% make vet:all
```
### Test
```zsh
% make test
# or check the report by kcov
% make coverage
```
### CI
......@@ -88,14 +108,11 @@ See `.gitlab-ci.yml`.
```zsh
# install gitlab-runner into .tools
% .tool/setup-gitlab-runner
# prepare environment variables for CI via .env.ci
% cp .env.ci.sample .env
# e.g. test (see .gitlab-ci.yml)
% .tool/ci-runner test
% make runner-test
```
......@@ -106,15 +123,15 @@ file.
### Unreleased commits
[v0.1.0...master](
https://gitlab.com/grauwoelfchen/adequate/compare/v0.1.0...master)
[v0.1.2...trunk](
https://gitlab.com/grauwoelfchen/adequate/compare/v0.1.1...trunk)
## License
```text
Adequate
Copyright 2020 Yasuhiro Яша Asaka
Copyright 2020-2023 Yasuhiro Яша Asaka
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
......
use crate::feedback::Feedback;
/// Error is an enum struct wraps multiple feedback.
#[derive(Clone, Debug)]
pub struct Error(pub Vec<Feedback>);
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::message::Message;
#[test]
fn test_eq() {
let a = Error(vec![Feedback {
field: "name",
messages: Vec::new(),
}]);
assert!(a.eq(&a));
let b = Error(vec![Feedback {
field: "name",
messages: Vec::new(),
}]);
assert!(a.eq(&b));
let c = Error(vec![Feedback {
field: "description",
messages: Vec::new(),
}]);
assert!(!a.eq(&c));
let d = Error(vec![Feedback {
field: "name",
messages: vec![Message {
text: "lorem ipsum {0}",
args: vec!["dolor sit amet".to_string()],
}],
}]);
assert!(!a.eq(&d));
}
}
use crate::message::Message;
/// Feedback struct contains target field name and multiple Message objects if
/// the context is negative (otherwise it will be an empty vector).
#[derive(Clone, Debug)]
pub struct Feedback {
pub field: &'static str,
pub messages: Vec<Message>,
}
impl Feedback {
pub fn is_negative(&self) -> bool {
!self.messages.is_empty()
}
}
impl PartialEq for Feedback {
fn eq(&self, other: &Self) -> bool {
if self.field != other.field {
return false;
}
self.messages == other.messages
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_is_negative() {
let f = Feedback {
field: "name",
messages: Vec::new(),
};
assert!(!f.is_negative());
let m = Message {
text: "lorem ipsum",
args: Vec::new(),
};
let f = Feedback {
field: "name",
messages: vec![m],
};
assert!(f.is_negative());
}
#[test]
fn test_eq() {
let a = Feedback {
field: "name",
messages: vec![Message {
text: "lorem ipsum",
args: Vec::new(),
}],
};
assert!(a.eq(&a));
let b = Feedback {
field: "name",
messages: vec![Message {
text: "lorem ipsum",
args: Vec::new(),
}],
};
assert!(a.eq(&b));
let c = Feedback {
field: "description",
messages: vec![Message {
text: "lorem ipsum",
args: Vec::new(),
}],
};
assert!(!a.eq(&c));
let d = Feedback {
field: "name",
messages: vec![Message {
text: "lorem ipsum {0}",
args: vec!["dolor sit amet".to_string()],
}],
};
assert!(!a.eq(&d));
}
}
......@@ -2,90 +2,14 @@
extern crate lazy_static;
extern crate strfmt;
use std::collections::HashMap;
use std::fmt;
mod error;
pub use error::Error;
use strfmt::strfmt;
mod feedback;
pub use feedback::Feedback;
/// Message struct holds validation error message and its arguments as
/// interpolation.
#[derive(Clone, Debug)]
pub struct Message {
pub text: String,
pub args: Vec<String>,
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut args = HashMap::new();
for (i, a) in self.args.iter().enumerate() {
args.insert(i.to_string(), a);
}
// panic if identifiers in template text won't match
let out = strfmt(&self.text, &args).expect("message format is invalid");
if !args.is_empty() && self.text == out {
panic!("message does not have expected number of identifiers");
}
write!(f, "{}", out)
}
}
impl PartialEq for Message {
fn eq(&self, other: &Self) -> bool {
if self.text != other.text {
return false;
}
for (i, a) in self.args.iter().enumerate() {
if a != &other.args[i] {
return false;
}
}
true
}
}
/// Feedback struct contains target field name and multiple Message objects if
/// the context is negative (otherwise it will be an empty vector).
#[derive(Clone, Debug)]
pub struct Feedback {
pub field: String,
pub messages: Vec<Message>,
}
impl Feedback {
pub fn is_negative(&self) -> bool {
!self.messages.is_empty()
}
}
impl PartialEq for Feedback {
fn eq(&self, other: &Self) -> bool {
if self.field != other.field {
return false;
}
for (i, m) in self.messages.iter().enumerate() {
if m != &other.messages[i] {
return false;
}
}
true
}
}
/// Error is an enum struct wraps multiple feedback.
#[derive(Clone, Debug)]
pub struct Error(pub Vec<Feedback>);
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
for (i, f) in self.0.iter().enumerate() {
if f != &other.0[i] {
return false;
}
}
true
}
}
mod message;
pub use message::Message;
pub mod validation;
......@@ -101,7 +25,7 @@ pub mod validation;
/// # use adequate::validation::length;
///
/// # fn main() {
/// let text = "lorem ipsum dolor sit amet".to_string();
/// let text = "lorem ipsum dolor sit amet";
///
/// let result = validate! {
/// "name" => text => [length::max(9)]
......@@ -111,11 +35,10 @@ pub mod validation;
/// let Error(out) = result.unwrap_err();
/// assert_eq!(out, vec![
/// Feedback {
/// field: "name".to_string(),
/// field: "name",
/// messages: vec![
/// Message {
/// text: "Must not contain more characters than {0}"
/// .to_string(),
/// text: "Must not have more characters than {0}",
/// args: vec!["9".to_string()]
/// }
/// ]
......@@ -132,9 +55,9 @@ pub mod validation;
#[macro_export]
macro_rules! validate {
( $( $n:expr => $v:expr => [ $( $c:expr ),* ] ),* ) => {{
let errors = vec![$(
let errors = [$(
Feedback {
field: $n.to_string(),
field: $n,
messages: [ $( $c(&$v) ),* ]
.iter()
.cloned()
......@@ -157,18 +80,18 @@ macro_rules! validate {
#[cfg(test)]
mod test {
use super::*;
use super::validation::ValidationResult;
use super::validation::Validator;
#[test]
fn test_message() {
let m = Message {
text: "lorem ipsum".to_string(),
text: "lorem ipsum",
args: Vec::new(),
};
assert_eq!(m.to_string(), "lorem ipsum");
let m = Message {
text: "lorem {0}".to_string(),
text: "lorem {0}",
args: vec!["ipsum".to_string()],
};
assert_eq!(m.to_string(), "lorem ipsum");
......@@ -178,7 +101,7 @@ mod test {
#[should_panic]
fn test_message_panic_with_non_numeric_tmpl_ident() {
let m = Message {
text: "lorem ipsum {}".to_string(),
text: "lorem ipsum {}",
args: vec!["dolor".to_string()],
};
m.to_string();
......@@ -188,7 +111,7 @@ mod test {
#[should_panic]
fn test_message_panic_with_missing_ident() {
let m = Message {
text: "lorem ipsum".to_string(),
text: "lorem ipsum",
args: vec!["dolor".to_string()],
};
m.to_string();
......@@ -198,7 +121,7 @@ mod test {
#[should_panic]
fn test_message_panic_with_missing_arg() {
let m = Message {
text: "lorem ipsum {0} {1}".to_string(),
text: "lorem ipsum {0} {1}",
args: vec!["dolor".to_string()],
};
m.to_string();
......@@ -207,7 +130,7 @@ mod test {
#[test]
fn test_feedback_with_positive_result() {
let f = Feedback {
field: "dummy".to_string(),
field: "dummy",
messages: vec![],
};
assert!(!f.is_negative());
......@@ -216,11 +139,11 @@ mod test {
#[test]
fn test_feedback_with_negative_result() {
let m = Message {
text: "lorem ipsum {0}".to_string(),
text: "lorem ipsum {0}",
args: vec!["dolor".to_string()],
};
let f = Feedback {
field: "dummy".to_string(),
field: "dummy",
messages: vec![m],
};
assert!(f.is_negative());
......@@ -229,10 +152,10 @@ mod test {
#[test]
fn test_failure() {
let dummy = "".to_string();
let validation = || -> Box<dyn Fn(&String) -> ValidationResult> {
Box::new(move |_: &String| {
let validation = || -> Box<Validator> {
Box::new(move |_: &str| {
Err(Message {
text: "Error".to_string(),
text: "Error",
args: vec![],
})
})
......@@ -247,9 +170,8 @@ mod test {
#[test]
fn test_success() {
let dummy = "".to_string();
let validation = || -> Box<dyn Fn(&String) -> ValidationResult> {
Box::new(move |_: &String| Ok(()))
};
let validation =
|| -> Box<Validator> { Box::new(move |_: &str| Ok(())) };
let result = validate! {
"input" => dummy => [validation()]
......
use std::collections::HashMap;
use std::fmt;
use strfmt::strfmt;
/// Message struct holds the validation error message and its arguments for
/// interpolation.
#[derive(Clone, Debug)]
pub struct Message {
pub text: &'static str,
pub args: Vec<String>,
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut args = HashMap::new();
for (i, a) in self.args.iter().enumerate() {
args.insert(i.to_string(), a);
}
// panic if identifiers in template text won't match
let out = strfmt(self.text, &args).expect("message format is invalid");
if !args.is_empty() && self.text == out {
panic!("message does not have expected number of identifiers");
}
write!(f, "{}", out)
}
}
impl PartialEq for Message {
fn eq(&self, other: &Self) -> bool {
if self.text != other.text {
return false;
}
self.args == other.args
}
}
// TODO: any idea for i18n?
lazy_static! {
pub static ref MESSAGES: [(&'static str, &'static str); 5] = [
("max", "Must not have more characters than {0}"),
("min", "Must not have less characters than {0}"),
("within", "Must be chars length within a range of {0}-{1}"),
("contains", "Must contain {0}"),
("not_contain", "Must not contain {0}"),
];
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_fmt() {
let m = Message {
text: "lorem ipsum {0}",
args: vec!["dolor sit amet".to_string()],
};
assert_eq!(format!("{}", m), "lorem ipsum dolor sit amet".to_string());
}
#[test]
fn test_eq() {
let a = Message {
text: "lorem ipsum {0}",
args: Vec::new(),
};
assert!(a.eq(&a));
let b = Message {
text: "lorem ipsum {0}",
args: Vec::new(),
};
assert!(a.eq(&b));
let c = Message {
text: "",
args: vec!["dolor sit amet".to_string()],
};
assert!(!a.eq(&c));
let d = Message {
text: "lorem ipsum {0}",
args: vec!["dolor sit amet".to_string()],
};
assert!(!a.eq(&d));
}
}
use crate::validation::{handle, OptionalValidator, Validator};
/// Returns a function that checks if the given &str contains another &str.
pub fn contains(part: &'static str) -> Box<Validator> {
Box::new(move |s: &str| {
handle(!s.contains(part), "contains", vec![part.to_string()])
})
}
/// Returns a function that checks if the given &str contains optional &str.
pub fn contains_if_given(part: Option<&'static str>) -> Box<Validator> {
let p = part.unwrap_or_default();
Box::new(move |s: &str| {
handle(
!p.is_empty() && !s.contains(p),
"contains",
vec![p.to_string()],
)
})
}
/// Returns a function that checks if the given string does not contain
/// optional &str.
pub fn not_contain_if_given(part: Option<&'static str>) -> Box<Validator> {
let p = part.unwrap_or_default();
Box::new(move |s: &str| {
handle(
!p.is_empty() && s.contains(p),
"not_contain",
vec![p.to_string()],
)
})
}
/// Returns a function that checks if the given string contains another string
/// when the one exists.
pub fn contains_if_present(part: &'static str) -> Box<OptionalValidator> {
Box::new(move |s: Option<&str>| match s {
Some(v) => contains(part)(v),
None => Ok(()),
})
}
#[cfg(test)]
mod test {
use super::*;
// contains
#[test]
fn test_contains_ok() {
let f = contains("lorem");
let result = f("lorem ipsum");
assert!(result.is_ok());
}
#[test]
fn test_contains_err() {
let f = contains("dolor sit amet");
let result = f("lorem ipsum");
assert!(result.is_err());
}
#[test]
fn test_contains_err_message() {
let f = contains("dolor sit amet");
let result = f("lorem ipsum");
assert_eq!(
result.map_err(|e| e.to_string()),
Err("Must contain dolor sit amet".to_string())
);
}
// contains_if_given
#[test]
fn test_contains_if_given_ok() {
let part = "lorem ipsum";
let f = contains_if_given(None);
let result = f(part);
assert!(result.is_ok());
let f = contains_if_given(Some(""));
let result = f(part);
assert!(result.is_ok());
let f = contains_if_given(Some("lorem"));
let result = f(part);
assert!(result.is_ok());
}
#[test]
fn test_contains_if_given_err() {
let f = contains_if_given(Some("dolor sit amet"));
let result = f("lorem ipsum");
assert!(result.is_err());
}
#[test]
fn test_contains_if_given_err_message() {
let f = contains_if_given(Some("dolor sit amet"));
let result = f("lorem ipsum");
assert_eq!(
result.map_err(|e| e.to_string()),
Err("Must contain dolor sit amet".to_string())
);
}
// not_contain_if_given
#[test]
fn test_not_contain_if_given_ok() {
let part = "lorem ipsum";
let f = not_contain_if_given(None);
let result = f(part);
assert!(result.is_ok());
let f = not_contain_if_given(Some(""));
let result = f(part);
assert!(result.is_ok());
let f = not_contain_if_given(Some("dolor sit amet"));
let result = f(part);
assert!(result.is_ok());
}
#[test]
fn test_not_contain_if_given_err() {
let part = "lorem ipsum";
let f = not_contain_if_given(Some("ipsum"));
let result = f(part);
assert!(result.is_err());
}
#[test]
fn test_not_contain_if_given_message() {
let f = not_contain_if_given(Some("dolor"));
let result = f("dolor sit amet");
assert_eq!(
result.map_err(|e| e.to_string()),
Err("Must not contain dolor".to_string())
);
}
// contains_if_present
#[test]
fn test_contains_if_present_ok() {
let f = contains_if_present("dolor sit amet");
let result = f(Some("lorem ipsum dolor sit amet"));
assert!(result.is_ok());
let result = f(None);
assert!(result.is_ok());
}
#[test]
fn test_contains_if_present_err() {
let f = contains_if_present("dolor sit amet");
let result = f(Some("lorem ipsum"));
assert!(result.is_err());
}
#[test]
fn test_contains_if_present_err_message() {
let f = contains_if_present("dolor sit amet");
let result = f(Some("lorem ipsum"));
assert_eq!(
result.map_err(|e| e.to_string()),
Err("Must contain dolor sit amet".to_string())
);
}
}