Skip to content

Commit

Permalink
turned code samples into proper Rust tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bgbahoue committed Aug 14, 2017
1 parent 8058b4b commit 3e1a9e7
Showing 1 changed file with 166 additions and 0 deletions.
166 changes: 166 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,172 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! # Assayer
//! ### a•SEY•er, n.
//! 1. A detailed report of the findings in analysis of a substance
//! 2. An unfortunate person whose job it was to taste test royal food, in case of poison
//! 3. Static (compile-time) validation library for Rust
//!
//! ## Motivation
//! Frequently, programs need to process untrusted input. Before doing so, it is important to ensure that the values being
//! processed are, in fact, valid for the purpose for which they are intended.
//!
//! Values representing the hour of the day, the day of the month, a person's age, a URL, phone number, and so on.
//! Regardless of the underlying data type (integer, string, custom struct, etc.) some values are valid, and some are not.
//!
//! As a validation library, Assayer allows you to build efficient, highly cohesive code by concentrating your custom
//! validation logic to one place--your validator. You can create any number of validators for each of the different types
//! in your application requiring validation.
//!
//! ## How to Use
//! ### Quickstart
//! - Define your custom validator (eg. for non-empty strings) by implementing `ValidatorRef` and/or `ValidatorMutRef`
//! and/or `Validator`. `ValidatorRef` example:
//!
//! ```rust
//! # struct NonEmptyStringValidator;
//! impl assayer::ValidatorRef<String> for NonEmptyStringValidator {
//! fn validate_ref(v: &String) -> assayer::Result<&String> {
//! Ok(v)
//! .and_then(|v| match !v.is_empty() {
//! true => Ok(v),
//! false => Err(assayer::Error::ValueEmpty("Empty Value Error".to_string())),
//! })
//!
//! // Chain as many additional .and_then() clauses as you need to validate your input.
//! // Each validation clause in the .and_then() chain must return:
//! // * Ok(v) if the value passes that validation step or
//! // * Err(String) if the value fails that validation step.
//! // A clause in a Result::and_then() chain which returns Err() will terminate further validation processing
//! // and the returned Err() will be returned to the caller.
//!
//! //.and_then(|v| ...test #2...)
//! //...
//! //.and_then(|v| ...test #n...)
//! }
//! }
//! ```
//! To call the the validator, use either the function- or method-call syntax illustrated below:
//!
//! ```rust
//! # use assayer::MethodSyntaxValidatorRef;
//! # struct NonEmptyStringValidator;
//! # impl assayer::ValidatorRef<String> for NonEmptyStringValidator {
//! # fn validate_ref(v: &String) -> assayer::Result<&String> {
//! # Ok(v)
//! # .and_then(|v| match !v.is_empty() {
//! # true => Ok(v),
//! # false => Err(assayer::Error::ValueEmpty("Empty Value Error".to_string())),
//! # })
//! # }
//! # }
//! # fn main() {
//! let foo = String::new(); //This is an empty String
//! let bar = "I am not empty".to_string();
//!
//! // Function synthax
//! let function_call_syntax_result = String::validate_ref::<NonEmptyStringValidator>(&foo);
//!
//! // Method synthax
//! let method_call_syntax_result = (&bar).validate_ref::<NonEmptyStringValidator>();
//!
//! assert_eq!(function_call_syntax_result.err(), Some(assayer::Error::ValueEmpty("Empty Value Error".to_string())));
//! assert_eq!(method_call_syntax_result.ok(), Some(&bar));
//! # }
//! ```
//! ### FAQ
//! #### What are some of Assayer's design goals?
//! ##### • Static (compile-time) validator
//! As a static validator, the validation chain you create is able to fully benefit from the compiler optimizers. A dynamic
//! validator using, for example the builder pattern at run-time cannot realize these benefits, because the validation chain
//! does not exist as compile time.
//!
//! We thought about use-cases, and other than the case of deciding how to validate at run-time (not the intended purpose of
//! this library), we could not think of a drawback for static validators. If we come across one, we are open to augmenting
//! this implementation with a run-time variant.
//!
//! ##### • Lightweight, Zero Dependencies
//! Languages have long been heading down the path of pulling in other "modules" and building on top of them (the Javascript
//! community is particularly notable in this regard). Rust's Cargo and crate system brings easy dependencies to Rust,
//! which we fully welcome. At the same time, we are fans of beautiful, maintainable, predictable, small focuse code. When
//! less code can do more, we think that kind of efficiency gain is particularly elegant. So the library ended up being a
//! grand total of six trait definitions, plus three blanket impls (which is how `MethodSyntaxValidator<SomeType>` gets defined
//! automatically when you as the user implement only `Validator<SomeType>`), and no dependencies.
//! you as the user of the library )
//!
//! ##### • Facilitating High Cohesion, Single-Responsiblity, Don't Repeat Yourself Principles
//! A validator allows you to factor out all your checking into pre-conditions all in one place. When all the code in a
//! routine is focused on achieving the same goal, you get high cohesion, which as been shown to be very beneficial to the
//! reliability and maintainability of a given codebase.
//!
//! By focusing only validation code into a validator, the validator becomes responsible for just one thing: validation.
//! The single-responsibility principle helps to keep the code from growing into a "god object", which, if you've ever had
//! the pleasure of working on one, we're sure you'll agree you don't want to go back any time soon!
//!
//! #### What is the difference between function-call syntax and method-call syntax?
//! From a code perspective, none. (For the tests we've run under Rust v1.18.0, the compiler yields 100% identical assembly
//! instructions.) From a personal perspective, you may be more comfortable with one style or the other, or it may be
//! convenient to switch styles under different circumstances. The choice is yours.
//!
//! #### When should I implement `Validator` vs. `ValidatorRef` vs. `ValidatorMutRef`?
//! Implementing `Validator` means that the call will consume your value:
//!
//! ```rust
//! # use assayer::MethodSyntaxValidator;
//! # struct NonEmptyStringValidator;
//! impl assayer::Validator<String> for NonEmptyStringValidator {
//! fn validate(v: String) -> assayer::Result<String> {
//! Ok(v)
//! .and_then(|v| match !v.is_empty() {
//! true => Ok(v),
//! false => Err(assayer::Error::ValueEmpty("Empty Value Error".to_string())),
//! })
//! }
//! }
//!
//! fn main() {
//! let input = "Not an empty string".to_string();
//! let result = input.validate::<NonEmptyStringValidator>();
//! // assert_eq!(result.ok(), Some(input)); //Oops! input has been moved!
//! assert_eq!(result.ok(), Some("Not an empty string".to_string()));
//! }
//! ```
//! To remedy this, either use `input.clone().validate::<NonEmptyStringValidator>()` or implement `ValidateRef` and use
//! `(&input).validate_ref::<NonEmptyStringValidator>()`.
//!
//! The case for implementing `ValidatorMutRef` is somewhat rare and arguably a violation of the Single Responsibility
//! Principle. Whether you consider it to be or not, it can be used to 'fix up' an incoming value before it is used by your
//! post-validation code (you might want to pad or round the input value before it is used, for example). Here is how it is used:
//!
//! ```rust
//! # use assayer::MethodSyntaxValidatorMutRef;
//! # struct NonEmptyStringValidator;
//! impl assayer::ValidatorMutRef<String> for NonEmptyStringValidator {
//! fn validate_mut_ref(v: &mut String) -> assayer::Result<&mut String> {
//! Ok(v)
//! .and_then(|v| match !v.is_empty() {
//! true => { v.push_str("!"); Ok(v) },
//! false => Err(assayer::Error::ValueEmpty("Empty Value Error".to_string())),
//! })
//! }
//! }
//!
//! fn main() {
//! let mut input = "Not an empty string".to_string();
//! let mut expected_result_inner = "Not an empty string!".to_string();
//! let expected_result = Some(&mut expected_result_inner);
//!
//! let result = (&mut input).validate_mut_ref::<NonEmptyStringValidator>();
//! assert_eq!(result.ok(), expected_result);
//! }
//! ```
//!
//! Finally, if you simply want all three implementations to "just work" whenver you implement one, implement ValidateRef
//! as per your needs, and ValidateMutRef and Validate as follows:
//!
//! TODO: Provide blanket impls for ValidateMutRef & Validate, in terms of ValidateRef.


#![cfg_attr(feature="clippy", feature(plugin))]
#![cfg_attr(feature="clippy", plugin(clippy))]
#![allow(non_camel_case_types)]
Expand Down

5 comments on commit 3e1a9e7

@U007D
Copy link

@U007D U007D commented on 3e1a9e7 Sep 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, thank you for doing this.

I can say I don't really like it when there's a lot of explanatory text like this (I find it hard to find the real code). I'd much rather see it in a separate module somehow (not sure if that's possible--just stating a preference); also by having it here and in README.md, the docs are no longer DRY.

I do like the idea of the examples being compiled for validity, though.

LMK if you have any thoughts on the concerns...

@bgbahoue
Copy link
Author

@bgbahoue bgbahoue commented on 3e1a9e7 Sep 3, 2017 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@U007D
Copy link

@U007D U007D commented on 3e1a9e7 Sep 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: 1: I'm agreed that all our published crates should have proper documentation. It's not clear to me at all that the documentation needs to be in Rust format.

Can you tell me more about your experiences with crates with no docs other than the GitHub README.md? Why did those experiences suck?

FYI, I now wish I'd saved it--but here's a counter-data-point: a Rust community member (sarnold) was very complimentary to me about the docs on IRC yesterday. He found them easy to digest and complete. He even took time to highlight a number of points in the docs that he liked. No mention of issues with it not being Rust documentation. I was not soliciting documentation feedback.

Just so I'm clear, I'm not against it being Rust documentation per se. But I am against 1) duplication and 2) making the source code harder to read.

Re: 2: agreed--very little tractions
Re: 3(ii) too bad this doesn't resolve the DRY problem...
Re: 4: even though it's a temporary measure, I think we're years away from a true cargo solution--there are too many other fish to fry in the Rust ecosystem. I'll play with this a bit, and if it can reliably do the basics, I think it could be what we need.

Thank you for all the suggestions on how to improve this situation. Very cool. :)

@bgbahoue
Copy link
Author

@bgbahoue bgbahoue commented on 3e1a9e7 Sep 3, 2017 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@U007D
Copy link

@U007D U007D commented on 3e1a9e7 Sep 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would only quote what are in the 2 links I shared on the first point 1. Did you have a chance to go through them (esp the good practice document)

Yes. I've been though them both multiple times.

mainly because all the bad Readme I went through never contained the latest or more advanced features of a library

If I'm understanding correctly, what was actually frustrating for you was not that the docs were in a README.md, but that the docs were out of date. And you are (rightfully) concerned about maintaining synchronization in our codebases as well.

That is the point we should be focused on. (eg. what if we or someone else built a script that included the README.md as part of the docs compilation process? If that's the shortest path to ensuring a synchronized, valid, DRY API, then that should be on the table. I think the README.md is more a symptom, rather than a root cause.)

I was thinking on the other library work we are doing (ie shaku or the test library) and obviously it would be different for an application than it would be for a library.

Ah, since you brought this up in the Assayer publish PR, I've been under the impression you were referring to Assayer this whole time. Certainly, a more complex library or framework would want to revisit this and yes, I'd want us to do what was appropriate. I agree that documenting, say, Rocket in a README.md is probably not the wisest course of action... ;)

Please sign in to comment.