shahryarjb

shahryarjb

GuardedStruct - A macro allows to build Structs that provide you with a number of important options Validation, Sanitizing, Constructor

GuardedStruct macro allows to build Structs that provide you with a number of important options Validation, Sanitizing, Constructor

The creation of this macro will allow you to build Structs that provide you with a number of important options, including the following:

  1. Validation
  2. Sanitizing
  3. Constructor
  4. It provides the capacity to operate in a nested style simultaneously.

Run in Livebook

Suppose you are going to collect a number of pieces of information from the user, and before doing anything else, you are going to sanitize them. After that, you are going to validate each piece of data, and if there are no issues, you will either display it in a proper output or save it somewhere else. All of the characteristics that are associated with this macro revolve around cleaning and validating the data.

The features that we list below are individually based on a particular strategy and requirement, but thankfully, they may be combined and mixed in any way that you see fit.

My Blog post about GuardedStruct:

Nested Example

defmodule ConditionalFieldComplexTest do
  use GuardedStruct
  alias ConditionalFieldValidatorTestValidators, as: VAL

  guardedstruct do
    field(:provider, String.t())

    sub_field(:profile, struct()) do
      field(:name, String.t(), enforce: true)
      field(:family, String.t(), enforce: true)

      conditional_field(:address, any()) do
        field(:address, String.t(), hint: "address1", validator: {VAL, :is_string_data})

        sub_field(:address, struct(), hint: "address2", validator: {VAL, :is_map_data}) do
          field(:location, String.t(), enforce: true)
          field(:text_location, String.t(), enforce: true)
        end

        sub_field(:address, struct(), hint: "address3", validator: {VAL, :is_map_data}) do
          field(:location, String.t(), enforce: true, derive: "validate(string, location)")
          field(:text_location, String.t(), enforce: true)
          field(:email, String.t(), enforce: true)
        end
      end
    end

    conditional_field(:product, any()) do
      field(:product, String.t(), hint: "product1", validator: {VAL, :is_string_data})

      sub_field(:product, struct(), hint: "product2", validator: {VAL, :is_map_data}) do
        field(:name, String.t(), enforce: true)
        field(:price, integer(), enforce: true)

        sub_field(:information, struct()) do
          field(:creator, String.t(), enforce: true)
          field(:company, String.t(), enforce: true)

          conditional_field(:inventory, integer() | struct(), enforce: true) do
            field(:inventory, integer(),
              hint: "inventory1",
              validator: {VAL, :is_int_data},
              derive: "validate(integer, max_len=33)"
            )

            sub_field(:inventory, struct(), hint: "inventory2", validator: {VAL, :is_map_data}) do
              field(:count, integer(), enforce: true)
              field(:expiration, integer(), enforce: true)
            end
          end
        end
      end
    end
  end
end

Installing the library:

def deps do
  [
    {:guarded_struct, "~> 0.0.1"}
  ]
end

Links

Github: GitHub - mishka-group/guarded_struct: GuardedStruct macro allows to build Structs that provide you with a number of important options Validation, Sanitizing, Constructor
Hex: guarded_struct | Hex
LiveBook Document: guarded_struct/guidance/guarded-struct.livemd at master · mishka-group/guarded_struct · GitHub

Most Liked

dimitarvp

dimitarvp

This looks great, I’ll try it.

Some of the naming is IMO prone to changes, f.ex. :is_map_data should be just fine written as :is_map. Same for :is_string_data. Also not sure about enforce, why not christen it mandatory? :thinking:

Also the derive thing might need another format but I believe I can work with it as-is.

Thanks for making this.

dimitarvp

dimitarvp

Ah, thanks. I’d definitely go for something shorter and self-documenting.

I am aware but IMO Elixir blundered the name here. enforce is ambiguous and mandatory is much clearer.

You got me. :slight_smile: I commented before reviewing it, it was just a surface observation. Thanks for elaborating. :+1:

Definitely. I believe Elixir’s community really has to unite around one godlike parsing and validation / type+rule enforcement library. Yours looks like a solid candidate.

shahryarjb

shahryarjb

Hello,
I truly appreciate you sharing your feedback—it’s incredibly valuable to me.

Since the initial idea was inspired by another Elixir library and some of the points raised involve certain trade-offs, I’ll do my best to address them where possible. However, unfortunately, I can’t make structural changes at this stage. This library has been in use for about a year now, and rewriting macros would require significant time. Additionally, I am currently actively working on another open-source project.


In my opinion, the name of a macro should be completely transparent about its purpose and usage. This ensures that developers can immediately understand what the macro is and what it does. Additionally, the consumer might be a project manager with limited technical knowledge of Elixir, so clarity is crucial.

I don’t believe having three macros in the entire program is excessive. I agree that there is a learning curve involved, but it’s relatively short. To address this, I’ve made an effort to provide extensive LiveBook examples and detailed documentation to help users understand and utilize the macros effectively.

Thank you for this

The validator feature was the very first functionality I added to the macro. If nothing is explicitly provided, it automatically looks for and reads from the module itself, assuming relevant information is available. If you wish to use an external module, it should be provided as a tuple.

That said, I’d be delighted to support this feature alongside the current ones. I’d be even happier if someone with the time could submit a pull request for it—I think it would turn out to be a very clean and elegant addition!

The macro already supports this functionality. You can pass any type to it as needed.
Including the specific case you mentioned.

Currently, the t() type is generated directly within the module. As for documentation, I was aiming for a custom approach to write concise documentation for individual fields. However, this is something I plan to address more thoroughly in the future.
Regarding the callback, I plan to include it in the next version. I already have a task assigned for it.

I’m sorry, but I’m not a big fan of splitting files excessively. I believe the main macro module is already sufficiently divided into other modules. Further splitting would hinder rapid development, as developers would spend too much time searching for modules and functions.

I prefer structuring modules based on their responsibilities rather than merely reducing the number of lines.

I completely agree with many of the points you’ve mentioned. I’d love to add more features and create diverse implementations. However, managing the balance between compile-time and runtime has been quite time-consuming for me.

I’d really appreciate it if the community could contribute by submitting pull requests for the features they need or at least opening an issue before working on a pull request.

Personally, I’m heavily focused on the Mishka Chelekom project at the moment, so I might not be able to implement the features you’ve mentioned in the short term.


I hope my responses don’t come across as unwillingness to listen to and address feedback. On the contrary, I truly enjoy improving things based on constructive criticism. After all, projects only get better with input from their users.

I simply tried to explain the thoughts and considerations that have been on my mind.
Thank you in advance

Eiji

Eiji

Yeah, I know what you feel :slight_smile:


Oh, I so wish that everyone with “limited technical knowledge” would at least read documentation. I truly appreciate your consumers. :sweat_smile:


Ah, that makes more sense now. It’s technically possible to do same for capture special form, but I believe it would be a bit too magic solution for end users. There could be an option for a fallback validator module which defaults to current one, so the feature would be really well documented, but this is not really a high priority, so:

makes a perfect sense in this case. :+1:


Oh, in that case it may be worth to include it in the example code above. As always I try to write generic advices, so as long as I don’t mention something directly I’m commenting what’s visible now for all readers and not what’s deep in source code or so.


Thee are lots of lines like that, so putting some special cases instead of some fields which are different only by their names is definitely worth to consider. Please pay attention that you have mentioned any() and struct() several times.


Oh, I was not precise. Of course I mean both at the same time (something like Unix way of tiny apps doing very specific things). There could be (not read whole code) few helper modules you could define. One option is to split them based on features, so for example you may have few DSL macros and each one would be documented in in separate module, so you would have “private” modules like GuardedStruct.Field.

The other option is as you said divide by responsibilities, so there could be for example GuardedStruct.AST module. This for example allows contributors to “jump in” to specific feature or type and work only on meta-programming part or just improve documentation. This is especially important when documentation takes over 1k lines. Of course in smaller cases it’s not so important.


Your response looks good for me. Whatever others say I fully understand, so no worries. :+1:

mudasobwa

mudasobwa

Creator of Cure

That’s exactly why I suggested you to take a look at estructura, to maybe gain some ideas for more sophisticated coercers/validators and especially for stream data generation, which is a cornerstone if you want to make apps using this library ever testable.

Where Next?

Popular in Libraries Top

scohen
Lexical Lexical is a next-generation language server for the Elixir programming language. Features Context aware code completion As-you...
New
zoltanszogyenyi
Hey everyone :wave: Excited to join this forum - I am one of the founders and current project maintainers of a popular and open-source U...
New
kevinlang
Hey all, We have made an Ecto3 Adapter for SQLite3, ecto_sqlite3! We have successfully on-boarded the full suite of integration tests (...
New
seancribbs
Today I released a new dialyzer Mix task as the dialyzex package! At the time we started writing this task, the existing dialyzer integra...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. Features Unstyled Phoenix components. Storybook that can be added to...
New
arkgil
Hi all! I’m happy to announce that Telemetry v0.3.0 is out! This release marks the conversion from Elixir to Erlang so that all the libr...
New
New
benlime
LiveMotion enables high performance animations declared on the server and run on the client. As a follow up to my previous thread A libr...
New
Crowdhailer
Experimenting with this code. OK.try do user <- fetch_user(1) cart <- fetch_cart(1) order = checkout(cart, user) save_or...
New
mplatts
With HEEX released we decided to start a components library using Tailwind CSS - check it out here: Petal Components. We also have a boi...
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

Sub Categories:

We're in Beta

About us Mission Statement