Closed reygoch closed 5 years ago
Why not "define" (derive) the link making functions right in the module where you define the API type (but not necessarily the implementation) ? Note that you can do this "simultaneously" with the newly added support for record-based APIs: https://haskell-servant.readthedocs.io/en/release-0.14/cookbook/generic/Generic.html.
@alpmestan It doesn't help. I have my Model.hs
where I've defined data types my API is returning, than I have API.hs
where I've defined my API. I can use allFieldLinks
to create a record with all the links in the API.hs
but than what? I usually have some ToJSON or ToHTML instance in my Model.hs
file and If I want to use safe links in my ToHTML
instance for my template I can't because I'd have to import API.hs
into the Model.hs
and API.hs
is already importing Model.hs
and I get an import cycle.
The only solutions I can see is to either define both my model and api in the same file ( along with all the required ToHtml instances ) which will in turn become a very large and unmanageable file, or use orphan instances to solve the cycle problem. None of those solutions seem appropriate for a project that isn't a simple toy example.
I hope I'm missing something here because safe links would be a really nice feature to have but at the moment it doesn't seem usable at all outside of small contrived examples.
Oh, there's a rather simple solution to your problem here I think. It's the first trick we go to when we've got that kind of circular dependency problem: just separate the core types into a dedicated .Type
module. In your case:
Model.Type
API
, which can freely refer to the model types by importing Model.Type
Model
, which can import both Model.Type
and API
and export Model
along with the problematic and non-problematic instances.Would that work for you?
@alpmestan if I define instances for types in the Model.Type.hs
in Model.hs
doesn't that cause orphaned instances since the instance is not made in the same file as data type or class?
@reygoch It does, but if you import Model
everywhere (except in API
, which has to be imported by Model
), and don't export Model.Type
outside of the package, they're not "all that orphan", so to speak. It's not like you're writing instances for a type that's from another package.
Is the loop like:
data Routes route = Routes
{ someRoute :: "foo" :> Get '[HTML, JSON] Thingie
, otherRoute :: ...
}
data Thingie = Thingie
instance ToHtml Thingie where
toHtml = ul_ $ do
li_ $ a [ routeHref_ someRoute ] "Thingie"
li_ $ a [ routeHref_ otherRoute ] "Something else"
then there are no clean non-orphan approach. There is a loop.
One "trick" is to make
data Routes' a route = Routes
{ someRoute :: "foo" :> Get '[HTML, JSON] a
, ...
}
-- now there aren't loop:
data Thingie = Thingie
type Routes = Routes' Thingie
instance ToHTML Thingie where
toHtml = ul_ $ do
li_ $ a [ routeHref_ someRoute ] "Thingie"
li_ $ a [ routeHref_ otherRoute ] "Something else"
This becomes painful is there are many entities, not only single Thingie
@phadej yes, I guess only option are orphan instances then.
Another way is to use newtype HtmlOf a = HtmlOf (Html ())
for pages, with htmlOf :: ToHtml a => a -> SomeHtml a
or something like that.
@phadej this might work, but I get a feeling a loop is hiding in this approach as well, and you loose ability to return multiple formats from a single endpoint like '[HTML,JSON]
...
@reygoch you don't, lazyness to the rescue: BundleOf a = BundleOf Aeson.Value Aeson.Encoding (Html ()) ...
I usually split my API into several sub APIs / modules which I think is a normal thing to do, so I end up with e.g.
RootAPI
,UsersAPI
,BlogAPI
etc...UsersAPI
andBlogAPI
are "children" ofRootAPI
and if I want to use links in any of those sub APIs I have to importRootAPI
so that I'm able to generate a link which leads to e.g. all users and sinceRootAPI
already importsUsersAPI
I get circular dependency and I can't think of any good way to avoid that dependency.Only solution so far is that I just use plain
Text
for my links, or keep my whole API definition and implementation in one massive module (which is a no go for me).So far it seems like the links feature is pretty much useless for anything serious. Am I missing something here?