PF2e Encounter Builder is a Rust and Svelte web application that uses a Rust API and a Svelte front-end. The web application is designed to dynamically create encounters for a popular table top roll playing game called Pathfinder 2e. Using an HTML form on the front-end, users are able to configure their search constraints and then fetch()
sends a query data to the Rust API. The rust back-end calculates the encounter budget and other query parameters which is then used to query a PostgreSQL database and send it as JSON back to the client. Additionally, to get the dataset, I built a web scraper in Rust to obtain and build the SQL database from an open source document that I will cover in another blog post.
I had initially considered using TypeScript for this project because I knew that I wanted a type system. Calculating the budget requires enough calculations that I knew that using vanilla JavaScript would create major headaches and be prone to bugs. I ended up settling on Rust because I thought that it would be able to handle manipulating the data structures more efficiently and make the app feel snappier. I don't know what kind of performance gain, if any, this actually gives me and benchmarking it would require me to rewrite large portions of the API in JavaScript.
While my initial decision was driven by pure speculation about Rust vs. JavaScript performance, using Rust had other advantages that I did not anticipate.
This project helped me gain new insights into OOP. I had to solve the problem of how to effectively translate between the Rust, JavaScript, SQL and Json methods for working with objects and communicate between the JavaScript iteration on the client and the Rust iteration of the same concepts on the server. On the server I used structs to store and manipulate the data and enums to control the state of the objects based on data from the client side as well as query results from the PostgreSQL database. On the client-side I used a class based approach to represent the objects and setter methods for retrieving new data from the server. I used the state management tools built into Svelte in conjunction with an HTML form to manage state on the client side. The client-side required extra attention to validation in order to ensure that the server was obtaining valid data for instantiating the structs and safely querying the database.
A further insight I gained into OOP was the effect that using two programming languages and separate processes on the server had on coupling. While the concepts of a monster and an encounter needed to be represented to the client and on the server to query the database and generate an HTTP response for the client, the methods required for these tasks were entirely different. Using two processes promoted a greater degree of encapsulation and an extra layer of validation on the server-side where the data needed to adhere to Rust's rigid typing system. One drawback to this approach, how the two processes communicate, was mitigated to a large degree by the fact that the data was being translated into HTTP requests and responses regardless.
While in many cases it can make code more maintainable having it in a common language, if the back-end logic were written in JavaScript and existed in the same Sveltekit application, the temptation to couple would be huge. This project is small enough that would probably not be critical, but in large projects this separation of concerns could have OOP advantages.
There are a few minor disadvantages that I confronted. Switching between languages requires, or at least strongly encourages, a context switching in terms of naming conventions. This could create confusion over what variables mean as they are in a slightly different format, and requires some thinking about at what point you want to change the naming conventions when sending the data between the front-end and back-end. I don't think this is a major concern, but it's perhaps worth considering.
The other disadvantage I encountered was process management. Using Rust and JS together requires me to run and maintain two separate processes on the server. The overhead of the Rust process is negligible in terms of resources, but it separates the logging of the two processes. This can be viewed as a disadvantage or as an advantage as it keeps my front-end and back-end logs completely separate making identifying where problems are arising easier.
In order to test the app, I am using three different overall groups of tests. First, I am running smaller scale unit tests on functions, second an integration test that tested the back-end as a whole, but bypassing the network, and finally I wrote a separate test app with Node, Puppeteer and Mocha. The latter two tests ended up being the most helpful in finding bugs so far.
My strategy for the integration test was to begin and end with the two structs that implemented the serde json #[derive(Deserialize)]
and #[derive(Serialize)]
macros that deserialized the URL query string coming in from the client and then serialized the json response back to the client. The goal was the isolate just the back-end logic, ensure that it was correct so that I could rule that out when doing the end to end tests that tested the entire route from client, server, database, server and then back to the client. Any remaining issues then should lay with either the network or the front-end. This test aims to iterate through as many data combinations as possible and isolated some bugs that cropped up in edge cases.
Once I had isolated bugs with the server side integration test, I used puppeteer to enter similar data to what I had hard coded into the tests on the server side in the integration test. I then captured the json with puppeteer:
await page.waitForResponse(async response => { return (await response.text()).startsWith('{"id"'); });
{"id"
being the first part of the json response. Once I had the json I used the mocha library to test the data.
I ran into a rather frustrating issue with puppeteer that was a valuable lesson. I kept getting incorrect data back from the server when running the Node end to end tests, but after some time I realized that, after I loaded the page, the select input that I was trying to manipulate did not exist yet. It wasn't causing an error, but the form was not being submitted correctly. I used await page.waitForNetworkIdle();
in order to ensure that the page was fully loaded and then submitted the form data and the tests passed as expected.
Messing around with computers and coding since I was 8. Now getting paid to do what I love.