Search code examples
rustfunctional-programmingdomain-driven-design

Rust and Domain-Driven Design to make certain errors impossible to write


I'm struggling with how I might implement something in Rust that is pretty straightforward in, say, TypeScript. Essentially a sum type of product types that enforces business rules at compile-time.

Suppose I have a struct Location that holds two properties, state and city, where state: StateEnum and city: String.

Generally, city and state can hold any StateEnum/String, respectively. So far, so good:

struct Location {
  state: StateEnum,
  city: String,
}

However, there are business rules that make it so certain values of state restrict the validity of certain city. Obviously you cannot have a valid Location { state: StateEnum::TX, city: "New York City }.

In OOP you'd make city private and have setCity check and throw an exception if the city wasn't legal for that state to enforce the business rule. A runtime exception.

In FP you have the ability to write your types so it's something like

type NYLocation = {
  state: NY
  city: 'Albany' | 'NYC' ...
}

type TexasLocation = { ... }

type Location = NYLocation | TexasLocation | ...

and your IDE would warn you that you cannot set TexasLocation.city to newCity: string without first using a type guard in your code to narrow the possible values from string to 'Houston' | 'Austin' | ....

Is there a way (I mean, obviously there has to be a way; is there an idiomatic way to make this compile-time in Rust instead of run-time like OOP has?


Solution

  • To a degree, yes. You can statically enforce a domain of discrete values with an enum, as you're already doing with states. You just have to extend that concept to cities.

    You could have an enum for each state's cities and have Location instead be an enum.

    enum NewYorkCities {
        Albany,
        NewYorkCity,
        // ...
    }
    
    enum WashingtonCities {
        Seattle,
        Olympia,
        // ...
    }
    
    enum Location {
        NewYork(NewYorkCities),
        Washington(WashingtonCities),
        // ...
    }
    

    This gives you the ability to prove that values not in the given domain cannot exist in the program.

    Finally, you just need a way to convert each enum to and from strings. These kinds of functions can be generated by macros in the strum crate.

    Obviously this requires recompilation to alter the acceptable domain, but you'd have the same problem with the TypeScript code anyway, so I assume that's an acceptable drawback.