Overview
Following up after Abstracting out adapters - Dependency Injection brought up a related problem. I’d be happy to put together a PR for this, but first we need to make some decisions. Specifically:
- Do we also want to include implementation requirements/recommendations?
- Where should this live?
- What should the interfaces actually consist of? How specific do we want to get?
I’m probably a bit biased, but it feels to me doing what PHP does with its PSRs and Java with its JSRs could be a worthy investment. Especially while the language is young. It probably wouldn’t be too hard to use those as points of inspiration given some of them are based on existing RFCs, if applicable, in order to port them to Crystal; if that’s the route we want to go.
The next sections are kinda me just thinking out loud.
API
I read thru PSR-7 again, and I think we could probably simplify it quite a bit as compared to my 1:1 port I came up with in my last post. Mainly due to the fact that we already have some pretty standardized APIs around URI
and IO
types. We also don’t parse files automatically, nor expose superglobal variables like $_POST
to access data. Tho defining standardized ways to represent/expose the data is prob still worth doing?
The other major difference is that their types are immutable. I kinda see why they did this, but also don’t think it’s worth changing to. Especially given that would be a large breaking change, but this could be a future conversation for 2.0.
Location
The most obvious choice is to include these interfaces in the stdlib within the HTTP
(or HTTP::Request
even) namespace. This keeps the specification and interface close to the code, but does have a few drawbacks. Specifically updates to the spec couldn’t really be made/published without releasing a new Crystal version. It also makes it harder to introduce specifications that do not directly related to a stdlib feature, such as caching or what have you.
Having a monorepo that includes all of the specifications could be sufficient. Tho it would have to be determined how to manage breaking changes in only specific specifications. A many-repo approach could be a good middle ground, possibly in a dedicated organization. Another consideration is how to make these available to the stdlib in a way that they could be updated without releasing a new Crystal version.
Options
I think providing interfaces along with some implementation suggestions would be a good and not much extra effort. So ultimately it comes down to where to put them. There are a few possible paths forward, each with their own pros and cons:
- Keep everything within the stdlib, close to related types if possible
- Pro: Removes need to vendor in an external repo with the stdlib
- Con: Changes to the interfaces/docs will need to wait on a Crystal release
- Pro: All projects inherently have access to the interfaces
- Pro: Easiest to implement
- Con: Unclear on how to handle breaking API changes
- Have to go with Crystal version? Use some flag to opt into what version you want? Have two versions with an alias that’ll change in the future?
- Keep everything within the stdlib, but in a dedicated
CSR
module
- Those mentioned before
- Pro: Easier to include interfaces not directly related to stdlib w/o polluting top level
- Pro: Would be easier to break out into its own thing if ever desired
- Have an external monorepo that contains all the interfaces/docs
- Con: How to expose them to the stdlib?
- Con: How to handle breaking API changes
- A branch per PSR? Something like what we’d do if they were in the stdlib?
- Pro: Can be released/updated independently from Crystal itself
- Have dedicated repos for each, managed via a monorepo
- Those mentioned before
- Pro: CSRs can be released/versioned independently of each other
- Con: More overhead required to manage the extra repos and possibly org
Proposal
After thinking about it for a while, I’m leaning towards having them bundled with the stdlib, in a dedicated module, that isn’t required by default outside of the things using the interfaces. I think this provides the best balance between ease of use, implementation, and ease of maintenance. I looked at all the PSR packages and after their initial 1.0
release, they aren’t touched too often. Thus it’s likely our APIs will be similar, making it being tied to a Crystal release a non-issue. Changes/updates to the documentation will probably be more common, but that would work the same as improvements the stdlib documentation.
Proposed HTTP API
module MessageInterface
abstract def version : String
abstract def version=(version : String) : String
# Just going to leverage existing headers type for this
# But, then might make sense to have a dedicated headers interface too
abstract def headers : HTTP::Headers
abstract def headers=(headers : HTTP::Headers)
abstract def body : IO?
abstract def body=(body : String)
abstract def body=(body : Bytes)
abstract def body=(body : IO)
abstract def body=(body : Nil)
end
module RequestInterface
include MessageInterface
abstract def request_target : String
abstract def request_target=(target : String) : String
abstract def method : String
abstract def method=(method : String)
abstract def uri : URI
abstract def uri=(uri : URI)
end
module ServerRequestInterface
include RequestInterface
abstract def cookies : HTTP::Cookies
abstract def cookies=(cookies : HTTP::Cookies)
abstract def query_params : URI::Params
abstract def query_params=(query_params : URI::Params)
# Optional
#
# Better than favoring monkey patching stuff into specific types.
# Could use some sort of generic container to make it work, while still being type safe.
abstract def attributes : _
abstract def get_attribute(name : String) : _
abstract def remove_attribute(name : String)
abstract def attributes=(attributes : _)
end
module ResponseInterface
include MessageInterface
abstract def status : HTTP::Status
abstract def status=(status : HTTP::Status)
end
It’s still a bit unclear on what to do when/if there is a breaking API change in a CSR. Mainly given they’re technically not tied to the Crystal’s semver versioning. Tho I also think we have options, and can figure it out later if ever needed.
Another thing that might be helpful, is also have a dedicated repo that could be use for extra CSRs, but before they’re “promoted” to the stdlib? But that’s also something we can hash out later.