-
Notifications
You must be signed in to change notification settings - Fork 1
Conversation
url: `${data.url}/${url}` | ||
}; | ||
}, | ||
handleQueryParams: queryParams => getData => input => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the visitor to require the input
and do the fetching of query parameters from the input in place. This is an example of where the higher-kinded type is necessary (previously all our visitors where monomorphic), but will also be an essential change when we add the CaptureParam
constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see...
Before:
We were building an intermediary data structure (call it ClientDataIntermediary
) that at the end we would traverse with our input to end up with the desired data. For example, during the middleware composition phase, we would build up a set of queryparam keys, of which at the end we would finally traverse with the client input to build the final a dictionary of queryparam values.
After:
Rather than build up an intermediate data structure, we build up a function term of type I => ClientData
, of which at the end we pass it the relevant client input and get the appropriate client data. This choice is actually the choice we went with on the hub.
I can see pros and cons with going with either choice.
Why ClientDataIntermediary
The function type makes the visitors marginally more complicated, since we have to const data = getData(input)
all the time. Also, building up a data structure instead of a function type is closer to what happens with the server visitor, so we'll get some uniformity in that sense.
Why I => ClientData
:
You don't have to bifuricate your logic by 1) creating an intermediary data representation and then 2) specifying how to unpack that representation to your final representation. Instead, you build up a function type that gets you your final representation directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The encoding of HKT here adds the additional constraint that each middleware constructor is defined for all input object types I: {}
.
Before this PR, we could've declare a middleware constructor that was only defined for a subset of object types, i.e.
class HypotheticalMiddleware<I: { foo: string }> extends Middleware<I, I> { ... }
What do you think? Is it premature "ungeneralization" to want to be able to write middleware constructors that are defined on only a subset of object types?
url: `${data.url}/${url}` | ||
}; | ||
}, | ||
handleQueryParams: queryParams => getData => input => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see...
Before:
We were building an intermediary data structure (call it ClientDataIntermediary
) that at the end we would traverse with our input to end up with the desired data. For example, during the middleware composition phase, we would build up a set of queryparam keys, of which at the end we would finally traverse with the client input to build the final a dictionary of queryparam values.
After:
Rather than build up an intermediate data structure, we build up a function term of type I => ClientData
, of which at the end we pass it the relevant client input and get the appropriate client data. This choice is actually the choice we went with on the hub.
I can see pros and cons with going with either choice.
Why ClientDataIntermediary
The function type makes the visitors marginally more complicated, since we have to const data = getData(input)
all the time. Also, building up a data structure instead of a function type is closer to what happens with the server visitor, so we'll get some uniformity in that sense.
Why I => ClientData
:
You don't have to bifuricate your logic by 1) creating an intermediary data representation and then 2) specifying how to unpack that representation to your final representation. Instead, you build up a function type that gets you your final representation directly.
Here are my thoughts on these: wrt. the (BTW -- I think we might need to revisit the name middleware. It does translate to middleware on the server, but not on the client necessarily. What do you think of "endpoint combinators"? Or something like that.) wrt. the client visitor result type -- the change was mostly motivated because I was trying to do capture params as an example where the HKT encoding was needed, and I made the switch there, and I thought it's a switch worth doing anyway. For capture params, needing the input to generate the data is much cleaner, I think: in the alternative, the intermediary structure would need to have the Since the visitor result is mostly contained within the module that does the wrapping of a package (here, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool!
src/client.js
Outdated
endpoint: SafeAPI.Endpoint<I, O>, | ||
input: I | ||
): ClientData<I> { | ||
const clientData: any = endpoint.visit(clientDataVisitor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's see if we can get rid of this any
. Maybe flow is telling us something here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah, I think this is just Flow not implementing the conversion rule fully. (IIRC this is the one where I need to view a negative $Call<(X) => F X, I>
as F<I>
, and that only works positively in Flow.) So the direction is that we need to tell Flow something instead! 😄
src/index.js
Outdated
} | ||
|
||
export type Visitor<DataF: Function> = {| | ||
handleFragment: ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we somehow extract these from the middleware types? Something like
handleFragment: $ExtractVisitorMethod<typeof fragment>
src/index.js
Outdated
|
||
type F = <A>(A) => Array<A> | ||
val x: $Call<F, string> | ||
(x: Array<string>) is not valid and will need an intermediate cast to any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this actually true? I got your example to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh.. well, I take the above back! It might actually be an issue.
With regards to the Client visitor, I agree that the function type makes it easier to implement and we should pick whatever is easier, since that's opaque to the library user anyways. Yeah, I think sticking to adding inputs might be a fair assumption for now. It forces our library to be more declarative. If we support arbitrary input transformations then the library might be too powerful and therefore less "analyzable". I'm fine renaming |
src/index.js
Outdated
} | ||
} | ||
|
||
export interface Endpoint<I: {}, O> { | ||
visit<DataF: Function>(visitor: Visitor<DataF>): $Call<DataF, {}>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that the endpoint type is correct. visit
seems to recurse down the stack at the term level, but $Call<DataF, I>
will not recurse.
I think this the reason why we need any
in the extractClient
and extractServer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nevermind I misunderstood DataF
, DataF
is the HKT that represents the target data structure we're mapping down to, like I => ClientData<I>
or ServerData
.
In that case, shouldn't it be $Call<DataF, I>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah -- I think I read the I: {}
wrong as I = {}
. This could be the issue actually!
src/index.js
Outdated
handleQueryParams: <P: {}>( | ||
P | ||
) => <I: {}>($Call<DataF, I>) => $Call<DataF, $Merge<I, $ExtractTypes<P>>>, | ||
handleNil: () => $Call<DataF, {}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to init
28481f8
to
2acb5fa
Compare
…-api into sl-as/extensible-visitors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with making a ticket to reduce type signature duplication in index.js
. Otherwise this PR looks good to me!
This changes the base implementation of endpoints and middleware to allow for a generic visitor pattern, instead of hardcoding the specific
mapServerData
andmapClientData
methods we used to have. That way we can separate the definition of endpoints and middleware from the definitions of visitors/"interpreters" of those types, which are defined by pattern matching on all the various cases.As a result we could have the following package structure:
safe-api-base
is where all the base definitions of endpoints are done, which basically give a well-typed alternative to using the string type for URLs (and also specifying query parameters etc.)safe-api-fetch
andsafe-api-koa
, which define their own interpreters/visitors of the base types, giving well-typed versions of their basic methodsThis does introduce a bit of complexity, as it requires the use of higher-kinded types for typing the generic
visit
function correctly and maintaining all type information. (I have added an explanation in the code about how higher-kinded types are encoded in Flow. An example of such a type we have used already is$ObjMap<Keys, F>
--F
is a type-level function transforming the types of values of an object, and therefore has the higher kindType -> Type
.) I do think the extra complexity is well-warranted though; we have discussed with @sleexyz that being able to define new interpreters of the endpoint constructors is necessary in order to be able to add wrapper APIs for more libraries as independent NPM packages.Closes #21 .