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: add form support and better routing; implement TODO app example #7

Merged
merged 1 commit into from
Aug 19, 2024

Conversation

m4tx
Copy link
Member

@m4tx m4tx commented Aug 13, 2024

Quite a big PR, but at this stage of the project it's difficult to make any significant changes while also separating it into logical blocks.

This adds:

  • HTTP GET and POST Form support with easy-to-use derive macro
  • Much better routing, with support for path params, and ability to get a URL for given view name
  • Simple support for the Askama template engine
  • A simple TODO app example showcasing the features listed above

@m4tx m4tx requested review from seqre and Iipin August 13, 2024 22:37

async fn remove_todo(request: Request) -> Result<Response, Error> {
let todo_id = request.path_param("todo_id").expect("todo_id not found");
let todo_id = todo_id.parse::<usize>().expect("todo_id is not a number");
Copy link
Member Author

Choose a reason for hiding this comment

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

It would probably be nice to have an ergonomic API to get path parameters with type safety (similar to Form I guess), but this is definitely out of scope of this PR.

Copy link
Member

Choose a reason for hiding this comment

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

I think that the fn(request) -> response) was inspired by Django's approach, but I think I'd rather go with Rocket's /Axum's approach of variables as function arguments, I think those seem more convenient to use.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, to be clear and for comparison purposes:

  1. Django uses function parameters to pass URL params (docs)
  2. Rocket uses function parameters to pass URL params (docs)
  3. Axum seems to be using a special tuple type that is passed as a parameter to the function (docs)

So, seems like there are two main approaches, and I'm not quite sure which one would be better for us. I've added a task to the project for now, as this is out of scope of this PR, as I've mentioned: https://github.com/orgs/flareon-rs/projects/2/views/7?pane=issue&itemId=75286758

}
}

impl FormField for CharField {
Copy link
Member Author

Choose a reason for hiding this comment

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

This implementation will probably be moved into some macro in the next PRs as it's pretty generic, but an in-place implementation is fine as long as we only have one field type (which is the case for now).

Copy link

codecov bot commented Aug 13, 2024

Codecov Report

Attention: Patch coverage is 55.94059% with 356 lines in your changes missing coverage. Please review.

Files Patch % Lines
flareon/src/forms.rs 0.00% 102 Missing ⚠️
flareon/src/router.rs 0.00% 89 Missing ⚠️
examples/todo-list/src/main.rs 0.00% 57 Missing ⚠️
flareon/src/request.rs 0.00% 56 Missing ⚠️
flareon/src/lib.rs 0.00% 23 Missing ⚠️
flareon/src/router/path.rs 90.47% 20 Missing ⚠️
flareon-macros/src/form.rs 98.40% 4 Missing ⚠️
flareon/src/error.rs 0.00% 4 Missing ⚠️
examples/hello-world/src/main.rs 0.00% 1 Missing ⚠️
Flag Coverage Δ
rust 50.80% <55.94%> (+39.00%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
flareon-macros/src/lib.rs 100.00% <100.00%> (+100.00%) ⬆️
flareon-macros/tests/compile_tests.rs 100.00% <100.00%> (ø)
examples/hello-world/src/main.rs 0.00% <0.00%> (ø)
flareon-macros/src/form.rs 98.40% <98.40%> (ø)
flareon/src/error.rs 0.00% <0.00%> (ø)
flareon/src/router/path.rs 90.47% <90.47%> (ø)
flareon/src/lib.rs 0.00% <0.00%> (ø)
flareon/src/request.rs 0.00% <0.00%> (ø)
examples/todo-list/src/main.rs 0.00% <0.00%> (ø)
flareon/src/router.rs 0.00% <0.00%> (ø)
... and 1 more

@m4tx m4tx force-pushed the todo-app branch 2 times, most recently from f3c13a3 to 5ed7b02 Compare August 13, 2024 23:16
Copy link
Member

@seqre seqre left a comment

Choose a reason for hiding this comment

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

LGTM for now, few comments in the code

let listener = tokio::net::TcpListener::bind(address_str).await.unwrap();

let handler = |request: axum::extract::Request| async move {
pass_to_axum(&project, request)
pass_to_axum(&project, Request::new(request, project.clone()))
Copy link
Member

Choose a reason for hiding this comment

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

I don't think cloning project on each request is the way to go

Copy link
Member Author

Choose a reason for hiding this comment

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

pub mod prelude;
#[doc(hidden)]
pub mod private;
Copy link
Member

Choose a reason for hiding this comment

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

If private shouldn't that be pub(crate)?

Copy link
Member Author

@m4tx m4tx Aug 19, 2024

Choose a reason for hiding this comment

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

The answer is inside the module documentation (somewhat, maybe it needs clarification):

/// Re-exports of some of the Flareon dependencies that are used in the macros.
///
/// This is to avoid the need to add them as dependencies to the crate that uses
/// the macros.

The problem is that when you generate some code inside a proc macro, the code end up being "pasted" inside the consumer's crate, not our crate. In order to use things like async_trait there, either the user has to declare it as their dependency, or we can just create this sort of "public-hidden" module that re-exports stuff that the generated code is using.

Copy link
Member Author

Choose a reason for hiding this comment

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

By the way - this seems to be a common practice in many libraries, e.g. https://github.com/serde-rs/serde/blob/master/serde/src/lib.rs#L313-L316

self.inner.headers().get(axum::http::header::CONTENT_TYPE)
}

pub async fn form_data(&mut self) -> Result<Bytes, Error> {
Copy link
Member

Choose a reason for hiding this comment

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

I think the name is confusing, wouldn't data just be enough?

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps. But then won't it be confusing when we want to have a method that returns a raw request body? data() sounds very much like it could be just returning raw data, while this method specifically parses the URL/request body as a HTML form, hence the name.

</head>
<body>
<h1>TODO List</h1>
<form id="todo-form" action="{{ flareon::reverse_str!(request, "add-todo") }}" method="post">
Copy link
Member

Choose a reason for hiding this comment

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

Nit, but I think that flareon filters/actions should be in scope by default

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed, but I think sadly this will require forking askama library, or at least creating some wrapper proc macro. Forking askama to make it more convenient to use with flareon is on our roadmap, and I think it's out of scope for this PR, so I'd keep it as is for now.

Comment on lines +77 to +81
Route::with_handler_and_name(
"/todos/:todo_id/remove",
Arc::new(Box::new(remove_todo)),
"remove-todo",
),
Copy link
Member

Choose a reason for hiding this comment

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

I assume that /remove is only because we don't have support for DELETE yet, right?

Copy link
Member Author

@m4tx m4tx Aug 19, 2024

Choose a reason for hiding this comment

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

Typically in web development, unless developing a REST service (or some other service that's not directly web-browser-facing), you only ever use GET and POST, really. I think one of the reasons is that it's defined as idempotent, and in case of this TODO app, it's clearly not (successive requests to /todos/0/delete will eventually remove all of the TODOs). There seem to be some very good historical reasons as well, but I'm not sure how important these are today.


async fn remove_todo(request: Request) -> Result<Response, Error> {
let todo_id = request.path_param("todo_id").expect("todo_id not found");
let todo_id = todo_id.parse::<usize>().expect("todo_id is not a number");
Copy link
Member

Choose a reason for hiding this comment

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

I think that the fn(request) -> response) was inspired by Django's approach, but I think I'd rather go with Rocket's /Axum's approach of variables as function arguments, I think those seem more convenient to use.

@m4tx m4tx merged commit 7867952 into master Aug 19, 2024
18 checks passed
@m4tx m4tx deleted the todo-app branch August 19, 2024 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants