Skip to content

Commit

Permalink
Improve inline expression; improve validation; refactor readme
Browse files Browse the repository at this point in the history
  • Loading branch information
Artem-Romanenia committed Feb 23, 2024
1 parent 8dd2e7b commit 736cf2b
Show file tree
Hide file tree
Showing 12 changed files with 1,079 additions and 431 deletions.
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "o2o"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
authors = ["Artem Romanenia <artem.romanenia@gmail.com>"]
categories = ["rust-patterns"]
Expand All @@ -10,8 +10,8 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/Artem-Romanenia/o2o"

[dependencies]
o2o-impl = { version = "0.3.0", path = "o2o-impl" }
o2o-macros = { version = "0.3.0", path = "o2o-macros" }
o2o-impl = { version = "0.3.1", path = "o2o-impl" }
o2o-macros = { version = "0.3.1", path = "o2o-macros" }

[workspace]
members = ["o2o-impl", "o2o-macros", "o2o-tests"]
225 changes: 145 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,98 @@
================================
[<img alt="github.com" src="https://github.com/Artem-Romanenia/o2o/workflows/Build/badge.svg" height="25">](https://github.com/Artem-Romanenia/o2o/)
[<img alt="crates.io" src="https://img.shields.io/crates/v/o2o.svg?style=for-the-badge&color=2f4d28&labelColor=f9f7ec&logo=rust&logoColor=black" height="25">](https://crates.io/crates/o2o)
[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-o2o-444444?style=for-the-badge&labelColor=aaaaaa&logo=docs.rs" height="25">](https://docs.rs/o2o)

## Quick pitch<!-- omit from toc -->

``` rust ignore
impl From<Person> for PersonDto {
fn from(value: Person) -> PersonDto {
PersonDto {
id: value.id,
name: value.name,
age: value.age,
}
}
}
```

Writing code like above is not the most exciting or emotionally rewarding part of working with Rust. If you're Ok with letting procedural macro write it for you, welcome to the rest of this page.

## Basic Example<!-- omit from toc -->

``` rust
use o2o::o2o;

struct Person {
id: u32,
name: String,
age: u8
}

#[derive(o2o)]
#[from_owned(Person)] // This tells o2o to generate 'From<Person> for PersonDto' implementation
#[owned_into(Person)] // This generates 'Into<Person> for PersonDto'
struct PersonDto {
id: u32,
name: String,
age: u8
}

// Applying #[derive(o2o)] on PersonDto allows you to do this:

let person = Person { id: 123, name: "John".into(), age: 42 };
let dto = PersonDto::from(person);

assert_eq!(dto.id, 123); assert_eq!(dto.name, "John"); assert_eq!(dto.age, 42);

// and this:

let dto = PersonDto { id: 321, name: "Jack".into(), age: 23 };
let person: Person = dto.into();

assert_eq!(person.id, 321); assert_eq!(person.name, "Jack"); assert_eq!(person.age, 23);
```

And here's the code that `o2o` generates (from here on, generated code is produced by [rust-analyzer: Expand macro recursively](https://rust-analyzer.github.io/manual.html#expand-macro-recursively) command):
<details>
<summary>View generated code</summary>

``` rust ignore
impl std::convert::From<Person> for PersonDto {
fn from(value: Person) -> PersonDto {
PersonDto {
id: value.id,
name: value.name,
age: value.age,
}
}
}
impl std::convert::Into<Person> for PersonDto {
fn into(self) -> Person {
Person {
id: self.id,
name: self.name,
age: self.age,
}
}
}
```
</details>

## Content<!-- omit from toc -->

- [Quick pitch](#quick-pitch)
- [Brief description](#brief-description)
- [Traits and `o2o` *trait instructions*](#traits-and-o2o-trait-instructions)
- [The (not so big) Problem](#the-not-so-big-problem)
- [Examples](#examples)
- [Simplest Case](#simplest-case)
- [Different field name](#different-field-name)
- [Different field type](#different-field-type)
- [Nested structs](#nested-structs)
- [Nested collection](#nested-collection)
- [Assymetric fields (skipping and providing default values)](#assymetric-fields-skipping-and-providing-default-values)
- [Expressions](#expressions)
- [Expressions for struct level instructions](#expressions-for-struct-level-instructions)
- [Expressions for member level instructions](#expressions-for-member-level-instructions)
- [Expressions for struct instructions](#expressions-for-struct-instructions)
- [Expressions for member instructions](#expressions-for-member-instructions)
- [More examples](#more-examples)
- [Slightly complex example](#slightly-complex-example)
- [Flatened children](#flatened-children)
Expand All @@ -37,47 +114,43 @@
- [Contributions](#contributions)
- [License](#license)

## Quick pitch
## Traits and `o2o` *trait instructions*

``` rust ignore
impl From<Person> for PersonDto {
fn from(value: Person) -> PersonDto {
PersonDto {
id: value.id,
name: value.name,
age: value.age,
}
}
}
```
To let o2o know what traits you want implemented, you have to use type-level `o2o` *trait instructions* (i.e. proc macro attributes):

Writing code like above is not the most exciting or rewarding part of working with Rust. If you're Ok with letting procedural macro write it for you, welcome to the rest of this page.
``` rust
struct Entity { }

## Brief description
#[derive(o2o::o2o)]
#[from_owned(Entity)]
struct EntityDto { }
```

**o2o** procedural macro is able to generate implementation of 6 kinds of traits (currently for structs only):
o2o procedural macro is able to generate implementation of 6 kinds of traits:

``` rust ignore
// #[from_owned()]
// When applied to a struct B:

// #[from_owned(A)]
impl std::convert::From<A> for B { ... }

// #[from_ref()]
// #[from_ref(A)]
impl std::convert::From<&A> for B { ... }

// #[owned_into()]
// #[owned_into(A)]
impl std::convert::Into<A> for B { ... }

// #[ref_into()]
// #[ref_into(A)]
impl std::convert::Into<A> for &B { ... }

// #[owned_into_existing()]
// #[owned_into_existing(A)]
impl o2o::traits::IntoExisting<A> for B { ... }

// #[ref_into_existing()]
// #[ref_into_existing(A)]
impl o2o::traits::IntoExisting<A> for &B { ... }
```

It also has shortcuts to configure multiple trait implementations with fewer lines of code:
o2o also has shortcuts to configure multiple trait implementations with fewer lines of code:

| | #[map()] | #[from()] | #[into()] | #[map_owned()] | #[map_ref()] | #[into_existing()] |
| ---------------------------- | -------- | ---------- | --------- | ---------------| ------------ | -------------------|
Expand All @@ -88,65 +161,57 @@ It also has shortcuts to configure multiple trait implementations with fewer lin
| **#[owned_into_existing()]** |||||| ✔️ |
| **#[ref_into_existing()]** |||||| ✔️ |

With that, let's look at some examples.
E.g. following two bits of code are equivalent:

## Examples
``` rust
struct Entity { }

### Simplest Case
#[derive(o2o::o2o)]
#[map(Entity)]
struct EntityDto { }
```

``` rust
use o2o::o2o;
struct Entity { }

struct Person {
id: u32,
name: String,
age: u8
}

#[derive(o2o)]
#[map_owned(Person)]
struct PersonDto {
id: u32,
name: String,
age: u8
}
#[derive(o2o::o2o)]
#[from_owned(Entity)]
#[from_ref(Entity)]
#[owned_into(Entity)]
#[ref_into(Entity)]
struct EntityDto { }
```
From here on, generated code is produced by [rust-analyzer: Expand macro recursively](https://rust-analyzer.github.io/manual.html#expand-macro-recursively) command
<details>
<summary>View generated code</summary>

``` rust ignore
impl std::convert::From<Person> for PersonDto {
fn from(value: Person) -> PersonDto {
PersonDto {
id: value.id,
name: value.name,
age: value.age,
}
}
}
impl std::convert::Into<Person> for PersonDto {
fn into(self) -> Person {
Person {
id: self.id,
name: self.name,
age: self.age,
}
}
}
```
</details>
And before looking at some examples, let's talk about...

## The (not so big) Problem

This section may be useful for people which are not very familiar with Rust's procedural macros.

With the above code you should be able to do this:
Being procedural macro, o2o has knowledge only about the side of the mapping where `#[derive(o2o)]` is applied.

``` rust ignore
let person = Person { id: 123, name: "John".into(), age: 42 };
let dto: PersonDto = person.into();
// and this:
let dto = PersonDto { id: 123, name: "John".into(), age: 42 };
let person: Person = dto.into();
#[derive(o2o::o2o)]
#[map(Entity)]
struct EntityDto { }
```

In code above, o2o knows everything about `EntityDto`, but it doesn't know anything about `Entity`. It doens't know if it is a struct, doesn't know what fields it has, doesn't know if it is struct or a tuple, *it doesn't even know if it exists*.

So unlike mappers from languages like C#, Java, Go etc. that can use reflection to find out what they need to know, `o2o` can only assume things.

For the piece of code above, o2o will assume that:

* `Entity` exists *(duh!)*
* `Entity` is the same data type that `EntityDto` is (in this case a struct)
* `Entity` has exactly the same fields that `EntityDto` has

If o2o is wrong in any of its assumptions, you will have to tell it that.

So now let's see how you do it...

## Examples

### Different field name

``` rust
Expand Down Expand Up @@ -322,8 +387,8 @@ struct Child {
struct EntityDto {
some_int: i32,
// Here field name as well as type are different, so we pass in field name and tilde inline expression.
// Also, it doesn't hurt to use member level instruction #[map()],
// which is broader than struct level instruction #[map_owned]
// Also, it doesn't hurt to use member trait instruction #[map()],
// which is broader than trait instruction #[map_owned]
#[map(children, ~.iter().map(|p|p.into()).collect())]
children_vec: Vec<ChildDto>
}
Expand Down Expand Up @@ -430,7 +495,7 @@ use o2o::o2o;

#[derive(o2o)]
#[map_owned(PersonDto)]
#[ghost(zodiac_sign: {None})] // #[ghost()] struct level instruction accepts only braced closures.
#[ghost(zodiac_sign: {None})] // #[ghost()] struct instruction accepts only braced closures.
struct Person {
id: i32,
full_name: String,
Expand Down Expand Up @@ -473,7 +538,7 @@ enum ZodiacSign {}

## Expressions

### Expressions for struct level instructions
### Expressions for struct instructions

``` rust ignore
#[ghost(field: { None })]
Expand All @@ -493,7 +558,7 @@ enum ZodiacSign {}
// field: (|x: &Type| x.get_value())(&self),
```

### Expressions for member level instructions
### Expressions for member instructions

``` rust ignore
#[map({ None })]
Expand Down Expand Up @@ -550,7 +615,7 @@ struct EmployeeDto {
full_name: String,

#[from(|x| Box::new(x.subordinate_of.as_ref().into()))]
#[into(subordinate_of, |x| Box::new(x.reports_to.as_ref().into()))]
#[into(subordinate_of, { Box::new(@.reports_to.as_ref().into()) })]
reports_to: Box<EmployeeDto>,

#[map(~.iter().map(|p| Box::new(p.as_ref().into())).collect())]
Expand Down
2 changes: 1 addition & 1 deletion o2o-impl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "o2o-impl"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
authors = ["Artem Romanenia <artem.romanenia@gmail.com>"]
description = "Implementation of 'o2o' crate"
Expand Down
Loading

0 comments on commit 736cf2b

Please sign in to comment.