Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Rust codegen for Constraints #582

Merged
merged 12 commits into from
Sep 26, 2024
Merged
22 changes: 15 additions & 7 deletions TestModels/Constraints/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ CORES=2

ENABLE_EXTERN_PROCESSING=1

TRANSPILE_TESTS_IN_RUST=1

include ../SharedMakefile.mk

PROJECT_SERVICES := \
Expand All @@ -19,13 +21,19 @@ SMITHY_DEPS=dafny-dependencies/Model/traits.smithy
# This project has no dependencies
# DEPENDENT-MODELS:=

# First, export DAFNY_VERSION=4.2
# Three separate items, because 'make polymorph_code_gen' doesn't quite work
polymorph:
npm i --no-save prettier@3 [email protected]
make polymorph_dafny
make polymorph_java
make polymorph_dotnet
clean: _clean
rm -rf $(LIBRARY_ROOT)/runtimes/java/src/main/dafny-generated
rm -rf $(LIBRARY_ROOT)/runtimes/java/src/main/smithy-generated
rm -rf $(LIBRARY_ROOT)/runtimes/java/src/test/dafny-generated

# Patch out tests that Rust codegen doesn't support
transpile_rust: | transpile_implementation_rust transpile_dependencies_rust remove_unsupported_rust_tests

remove_unsupported_rust_tests:
$(MAKE) _sed_file \
SED_FILE_PATH=$(LIBRARY_ROOT)/runtimes/rust/src/implementation_from_dafny.rs \
SED_BEFORE_STRING='let mut allowBadUtf8BytesFromDafny: bool = true' \
SED_AFTER_STRING='let mut allowBadUtf8BytesFromDafny: bool = false'

# Python

Expand Down
25 changes: 25 additions & 0 deletions TestModels/Constraints/runtimes/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "simple_constraints"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
wrapped-client = []

[dependencies]
aws-smithy-runtime = {version = "1.7.1", features=["client"]}
aws-smithy-runtime-api = {version = "1.7.2", features=["client"]}
aws-smithy-types = "1.2.4"
dafny_runtime = { path = "../../../dafny-dependencies/dafny_runtime_rust"}

[dependencies.tokio]
version = "1.26.0"
features = ["full"]

[dev-dependencies]
simple_constraints = { path = ".", features = ["wrapped-client"] }

[lib]
path = "src/implementation_from_dafny.rs"
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
extern crate simple_constraints;

/// Smoke tests for constraint validation when calling in from Rust code.
mod simple_constraints_test {
use simple_constraints::*;

fn client() -> Client {
let config = SimpleConstraintsConfig::builder().build().expect("config");
client::Client::from_conf(config).expect("client")
}

#[tokio::test]
async fn test_empty_input() {
let result = client().get_constraints().send().await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_short_string() {
let result = client().get_constraints().my_string("").send().await;
let error = result.err().expect("error");
assert!(matches!(
error,
simple_constraints::types::error::Error::ValidationError(..)
));
assert!(error.to_string().contains("my_string"));

use std::error::Error;
let source_message = error.source().expect("source").to_string();
assert!(source_message.contains("my_string"));
}

#[tokio::test]
async fn test_good_string() {
let result = client().get_constraints().my_string("good").send().await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_long_string() {
let result = client()
.get_constraints()
.my_string("too many characters")
.send()
.await;
let message = result.err().expect("error").to_string();
assert!(message.contains("my_string"));
}

#[tokio::test]
async fn test_small_int() {
let result = client().get_constraints().one_to_ten(0).send().await;
let message = result.err().expect("error").to_string();
assert!(message.contains("one_to_ten"));
}

#[tokio::test]
async fn test_good_int() {
let result = client().get_constraints().one_to_ten(5).send().await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_big_int() {
let result = client().get_constraints().one_to_ten(99).send().await;
let message = result.err().expect("error").to_string();
assert!(message.contains("one_to_ten"));
}
}
40 changes: 32 additions & 8 deletions TestModels/Constraints/test/WrappedSimpleConstraintsTest.dfy
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ module WrappedSimpleConstraintsTest {
TestGetConstraintWithGreaterThanOne(client);
TestGetConstraintWithUtf8Bytes(client);
TestGetConstraintWithListOfUtf8Bytes(client);

var allowBadUtf8BytesFromDafny := true;
if (allowBadUtf8BytesFromDafny) {
TestGetConstraintWithBadUtf8Bytes(client);
TestGetConstraintWithListOfBadUtf8Bytes(client);
}
}

method TestGetConstraintWithValidInputs(client: ISimpleConstraintsClient)
Expand Down Expand Up @@ -486,11 +492,6 @@ module WrappedSimpleConstraintsTest {
ret := client.GetConstraints(input := input);
expect ret.Failure?;

// good length, bad bytes
input := input.(MyUtf8Bytes := Some(ForceUtf8Bytes([255,255,255])));
ret := client.GetConstraints(input := input);
expect ret.Failure?;

var one : seq<uint8> := [0xf0, 0xa8, 0x89, 0x9f];
var two : seq<uint8> := [0xc2, 0xa3];
input := input.(MyUtf8Bytes := Some(ForceUtf8Bytes(one)));
Expand Down Expand Up @@ -527,13 +528,25 @@ module WrappedSimpleConstraintsTest {
// expect ret.Failure?;
}

// @length(min: 1, max: 10)
method TestGetConstraintWithBadUtf8Bytes(client: ISimpleConstraintsClient)
requires client.ValidState()
modifies client.Modifies
ensures client.ValidState()
{
// good length, bad bytes
var input := GetValidInput();
input := input.(MyUtf8Bytes := Some(ForceUtf8Bytes([255,255,255])));
var ret := client.GetConstraints(input := input);
expect ret.Failure?;
}

// @length(min: 1, max: 2)
method TestGetConstraintWithListOfUtf8Bytes(client: ISimpleConstraintsClient)
requires client.ValidState()
modifies client.Modifies
ensures client.ValidState()
{
var bad := ForceUtf8Bytes([255,255,255]);
var good := ForceUtf8Bytes([1,2,3]);

var input := GetValidInput();
Expand All @@ -552,9 +565,20 @@ module WrappedSimpleConstraintsTest {
input := input.(MyListOfUtf8Bytes := Some(ForceListOfUtf8Bytes([good, good, good])));
ret := client.GetConstraints(input := input);
expect ret.Failure?;
}

// @length(min: 1, max: 2)
method TestGetConstraintWithListOfBadUtf8Bytes(client: ISimpleConstraintsClient)
requires client.ValidState()
modifies client.Modifies
ensures client.ValidState()
{
var bad := ForceUtf8Bytes([255,255,255]);
var good := ForceUtf8Bytes([1,2,3]);

var input := GetValidInput();
input := input.(MyListOfUtf8Bytes := Some(ForceListOfUtf8Bytes([bad])));
ret := client.GetConstraints(input := input);
var ret := client.GetConstraints(input := input);
expect ret.Failure?;

input := input.(MyListOfUtf8Bytes := Some(ForceListOfUtf8Bytes([bad, good])));
Expand All @@ -565,4 +589,4 @@ module WrappedSimpleConstraintsTest {
ret := client.GetConstraints(input := input);
expect ret.Failure?;
}
}
}
27 changes: 27 additions & 0 deletions TestModels/Positional/runtimes/rust/src/conversions/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ pub fn to_dafny(
message: ::dafny_runtime::dafny_runtime_conversions::unicode_chars_false::string_to_dafny_string(&message),
list: ::dafny_runtime::dafny_runtime_conversions::vec_to_dafny_sequence(&list, |e| to_dafny(e.clone()))
},
crate::types::error::Error::ValidationError(inner) =>
crate::r#simple::positional::internaldafny::types::Error::Opaque {
obj: {
let rc = ::std::rc::Rc::new(inner) as ::std::rc::Rc<dyn ::std::any::Any>;
// safety: `rc` is new, ensuring it has refcount 1 and is uniquely owned.
// we should use `dafny_runtime_conversions::rc_struct_to_dafny_class` once it
// accepts unsized types (https://github.com/dafny-lang/dafny/pull/5769)
unsafe { ::dafny_runtime::Object::from_rc(rc) }
},
},
crate::types::error::Error::Opaque { obj } =>
crate::r#simple::positional::internaldafny::types::Error::Opaque {
obj: ::dafny_runtime::Object(obj.0)
Expand Down Expand Up @@ -68,5 +78,22 @@ pub fn from_dafny(
crate::types::error::Error::Opaque {
obj: obj.clone()
},
crate::r#simple::positional::internaldafny::types::Error::Opaque { obj } =>
{
use ::std::any::Any;
if ::dafny_runtime::is_object!(obj, crate::types::error::ValidationError) {
let typed = ::dafny_runtime::cast_object!(obj.clone(), crate::types::error::ValidationError);
crate::types::error::Error::ValidationError(
// safety: dafny_class_to_struct will increment ValidationError's Rc
unsafe {
::dafny_runtime::dafny_runtime_conversions::object::dafny_class_to_struct(typed)
}
)
} else {
crate::types::error::Error::Opaque {
obj: obj.clone()
}
}
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ impl GetName {
pub fn new() -> Self {
Self
}

pub(crate) async fn send(
simple_resource: &crate::types::simple_resource::SimpleResourceRef,
input: crate::operation::get_name::GetNameInput,
) -> ::std::result::Result<
crate::operation::get_name::GetNameOutput,
crate::types::error::Error,
> {

simple_resource.inner.borrow_mut().get_name(input)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ impl GetResource {
pub fn new() -> Self {
Self
}

pub(crate) async fn send(
client: &crate::client::Client,
input: crate::operation::get_resource::GetResourceInput,
) -> ::std::result::Result<
crate::operation::get_resource::GetResourceOutput,
crate::types::error::Error,
> {
if input.name.is_none() {
return ::std::result::Result::Err(::aws_smithy_types::error::operation::BuildError::missing_field(
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like going through a aws_smithy_types::error is adding complexity without adding value.
Couldn't we just make a wrap_validation_err out of a String and leave it as that?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's valuable: it makes it possible to programmatically handle validation errors, and I can see that being useful for middleware that handles translation of Smithy-modeled types, for example.

As long as displaying these errors gives a good user experience I say we keep them.

"name",
"name was not specified but it is required when building GetResourceInput",
)).map_err(crate::types::error::Error::wrap_validation_err);
}
let inner_input = crate::conversions::get_resource::_get_resource_input::to_dafny(input);
let inner_result =
::dafny_runtime::md!(client.dafny_client.clone()).GetResource(&inner_input);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ impl GetResourcePositional {
pub fn new() -> Self {
Self
}

pub(crate) async fn send(
client: &crate::client::Client,
input: crate::operation::get_resource_positional::GetResourcePositionalInput,
) -> ::std::result::Result<
crate::types::simple_resource::SimpleResourceRef,
crate::types::error::Error,
> {
if input.name.is_none() {
return ::std::result::Result::Err(::aws_smithy_types::error::operation::BuildError::missing_field(
"name",
"name was not specified but it is required when building GetResourcePositionalInput",
)).map_err(crate::types::error::Error::wrap_validation_err);
}
let inner_input = crate::standard_library_conversions::ostring_to_dafny(&input.name) .Extract();
let inner_result =
::dafny_runtime::md!(client.dafny_client.clone()).GetResourcePositional(&inner_input);
Expand Down
47 changes: 44 additions & 3 deletions TestModels/Positional/runtimes/rust/src/types/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ pub enum Error {
message: ::std::string::String,
},
CollectionOfErrors {
list: ::std::vec::Vec<Error>,
list: ::std::vec::Vec<Self>,
message: ::std::string::String,
},
ValidationError(ValidationError),
Opaque {
obj: ::dafny_runtime::Object<dyn ::std::any::Any>,
},
Expand All @@ -19,8 +20,48 @@ impl ::std::cmp::Eq for Error {}

impl ::std::fmt::Display for Error {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
write!(f, "{:?}", self)
match self {
Self::ValidationError(err) => ::std::fmt::Display::fmt(err, f),
_ => ::std::fmt::Debug::fmt(self, f),
}
}
}

impl ::std::error::Error for Error {}
impl ::std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::ValidationError(err) => Some(err),
_ => None,
}
}
}

impl Error {
pub fn wrap_validation_err<E>(err: E) -> Self
where
E: ::std::error::Error + 'static,
{
Self::ValidationError(ValidationError(::std::rc::Rc::new(err)))
}
}

#[derive(::std::clone::Clone, ::std::fmt::Debug)]
pub(crate) struct ValidationError(::std::rc::Rc<dyn ::std::error::Error>);

impl ::std::cmp::PartialEq for ValidationError {
fn eq(&self, other: &Self) -> bool {
::std::rc::Rc::<(dyn std::error::Error + 'static)>::ptr_eq(&self.0, &other.0)
}
}

impl ::std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
::std::fmt::Display::fmt(&self.0, f)
}
}

impl ::std::error::Error for ValidationError {
fn source(&self) -> ::std::option::Option<&(dyn ::std::error::Error + 'static)> {
::std::option::Option::Some(self.0.as_ref())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,10 @@ protected TokenTree fromDafny(
valueToRust = "(%s).unwrap()".formatted(valueToRust);
}
} else {
valueToRust = dafnyToRust.formatted(dafnyValue + ".as_ref()");
valueToRust =
dafnyToRust.formatted(
"::std::borrow::Borrow::borrow(%s)".formatted(dafnyValue)
);
alex-chew marked this conversation as resolved.
Show resolved Hide resolved
if (isRustOption) {
valueToRust = "Some(%s)".formatted(valueToRust);
}
Expand Down
Loading
Loading