katafrakt
Hi, I’m contemplating writing an admin library, similar to Kaffy and LiveAdmin, but filling a few gaps found while using Kaffy. For that I figure I would leverage protocols instead of giant configs (LiveAdmin) or magic admin modules (Kaffy). But after some initial coding I see a few problems.
What I have right now is this:
defimpl Admin.Resource, for: App.Accounts.User do
def name(_), do: "User"
def slug(_), do: "user"
def form(user) do
import Admin.FormBuilder
new(user)
|> title(if user.id, do: "User #{user.id}", else: "New user")
|> field(:email)
|> field(:first_name)
|> field(:last_name)
|> build()
end
end
This is nice, but obviously the number of customizable things grows in time. Not only I want to have customizable form, but also records lists, their fields, custom actions, custom mass actions etc. On the other hand, I don’t want to force user to define all functions when they just may use defaults (for example, no need for custom slug or resource name).
So far I have two ideas and I want to know what you think:
1. Have a macro with default
defimpl Admin.Resource, for: App.Accounts.User do
# I don't want to customize slug and name
def form(user) do
# ...
end
use Admin.Resource.DefaultImplementations
end
This has a problem with either having the users remember to put it at the end of the module, so default implementation does not shadow custom implemementations - this is not POLS and what is usually done with use. Also, it generates warnings. Alternatively, there would have to be an option to what default implementations to use, for example:
defimpl Admin.Resource, for: App.Accounts.User do
use Admin.Resource.DefaultImplementations, except: [:form, :custom_actions]
end
Both are not really great in my opinion, I would especially hate generating many warnings for the user (so using import instead is not great too).
2. Use many small protocols
Alternatively, I could have a protocol for just about anything that is customizable and user would choose what to implement. So we will have Admin.Resource, Admin.ResourceTable.Columns, Admin.ResourceTable.Actions, Admin.ResourceTable.MassActions, Admin.ResourceTable.Query, Admin.ResourceTable.SearchQuery, Admin.Resource.FormCustomActions etc. Each one of these will probably will end up with just one function (call?). Seems sensible to some extent, but I feel that having to implement these many protocols might put people off.
Which solution appeals to you more? Or maybe there’s some alternative I missing? Or maybe using protocols for that wasn’t such a great idea…
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #elixirconf-us
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
al2o3cr
This sounds like the situation
defoverridableis intended for.zachallaun
Every time I’ve personally started off with a protocol for some generic piece of functionality, I’ve eventually switched to a behaviour. I believe they are both more explicit and more flexible, and lend themselves well to the
defoverridablestrategy that @al2o3cr mentioned.cognivore
You have stumbled upon on the many faces of the expression problem!
One doesn’t need to squint hard that the notion of your architecture to be truly scalable under development (what I mean here, strictly, is to guarantee that at no point in time you have added a new data type such as
App.Accounts.User, but forgot to implement some method such asformand, conversely, you have added some method such asformand didn’t match some of the data types such asApp.Accounts.User), is the same as the notion of adding a new function and a new type case at the same time.I think what the colleagues who have answered with
defoverridablesuggestion imply is something like the idea from the post-scriptum of this post, which is “just being able to scale code base along one function / type case axis is enough for scalable code bases that are also ergonomic”.That said, defoverridable is not a panacea. It allows you to add more types easily, but based on your question you seem to be more concerned about adding more functions. Furthermore, you venture into the dangerous waters of default implementations which, if unconstrainted, can quickly turn unusable. Ask yourself when have you last seen a useful default implementation that survives class inheritance?!
This is why, I would argue that what you actually need a hierarchy of protocols. One could say, you need traits! Now if you’re able to define your default implementaitons in terms of existing traits, these implementations shall trivially survive subtyping!
If you wonder where can you get hierarchical traits in Elixir,
type_classlibrary has got you covered!What’s more, is that you can be principled about trait methods that you define and add properties for the traits that shall be checked at compile time.
Finally, on ergonomics that you desire.
Consider you have:
The UX you want is
Sadly, the best type_class can do is:
I sketched out an implementation plan for shallow dependency instance definitions here.
In general, I would strongly suggest you having the safest architecture that enforces completeness of function implementations over data types over both developer’s user experience and trying to chase the expression problem. Whatever it means for your particular problem.
tfwright
@katafrakt there are a couple of suggestions in this thread you might want to check out: Overridable functions in protocol implementations? - #6 by Ajwah
That said, when I created LiveAdmin I did consider integrating directly with the Ecto schema modules themselves to handle config, but I ultimately decided against it because I wanted something as flexible and light-weight as possible. Personally I preferred to keep that noise out of the application business logic, but other users might prefer to have it in the contexts, or separate per-schema modules, or whatever. It’s certainly true that eventually this can lead to unwieldy configs. However, since the config is just a list, there is nothing to stop the user from leveraging whatever language features they want to specify their config implementations consistently for all the schemas, and furthermore, to pass the result via app level config, rather than per-schema configs in the router. If you wanted to use protocols to contain your config logic, you could do something like this:
and then the router would only need the list of schemas, which is also something you could generate dynamically.
Admittedly, not all config is currently so conducive to that approach and would need additional effort to generate, but I think it should be doable if desired. Maybe that is something I should take a look at formalizing a bit more.
katafrakt
Thanks for the replies!
@al2o3cr
defoverridablewill indeed help to avoid problems with first approach. I forgot about it.@zachallaun I see the appeal of using behaviours instead here. The only element that would be missing is that I would need some kind of a registry to map actual resource (
App.Accounts.User) to an implementation of the behaviour. But I guess protocol defined inAdmin.Resource.__using__might handle that (and the default implementation as well). It’s and interesting take, I have to think about it.@cognivore thanks for introducing me to a
type_classpackage, I’d have to read more about it to full understand it. Looks promising, however on the other hand I’d like to avoid dependencies if possible.Yes, it crossed my mind that traits is what I really need here.
@tfwright The post you liked resonates with me a lot. I also thought that
@fallback_to_any truewould actually fallback missing functions as well. I see how it’s not desirable now, but my first instinct was that it should.Regarding putting all the config in config or router - I still kind of think that router is not a right place for that, although I see the benefit of having to just config a library in one place, not in multiple. You example of using protocols makes it interestingly lean.
tfwright
I tend to agree on both counts, I definitely didn’t want config in my app code, and settled for keeping it in one place in my app code, the router, because the router is the closest to config already. You have your resources, you are essentially configuring how to expose them to the endpoint.
Now that I am thinking about it, I am wondering whether extending the router DSL would have been better, so you could do something like this:
Arguably just as much config in the router, but it would be more…“routerish” config
BartOtten
I like the effort and not to rain on your project but I do wonder: If only a few gaps, wouldn’t it be better to fix those in one of the mentioned libs? You save yourself a lot of time and converge (because of improving existing) instead of possibly diverge (3 admin libs divide attention) the community.
Just a thought
katafrakt
That’s a valid concern. I have actually contributed to Kaffy in the past. I thought of maintaining a fork with some bigger changes, but then I realized it would diverge a lot (for example, Kaffy does not use LiveView or Phoenix components at all) - and what’s the point of maintaining a fork if you cannot sync it with upstream? So, “few gaps” might have been an understatement.
I should probably re-evaluate LiveAdmin again, as it changed a lot since the last time I checked.
Anyway, I wouldn’t worry about diverging the community - I don’t have a history of successful open source projects. This would rather just be for my current company and one personal project I have.
BartOtten
That was yesterday, today is a new day!
Which made you valuable for the community already. Take credit
aesmail
I’m a little biased here, but with Kaffy, I can hit the ground running immediately with extremely minimum configurations. Having said that, I have long overdue plans to make Kaffy more “protocol”-based.
Protocols would provide basic functionality via adapters and can be customizable based on the datasource. The community could also write their own “adapters” which would change or add functionality to Kaffy.
I do support your point. However, I believe phoenix still doesn’t have a great admin library. It’s been a while since I was able to dedicate some time to work on Kaffy. I would be more than happy to add maintainers if anyone is interested.
To be honest, I still don’t have any plans to introduce LiveView to Kaffy. Personally, I think LiveView is still in its early stages and maintaining it as a dependency might be challenging in the meantime. Having said that, I’m not against LiveView per se.