New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add custom typed properties to requests #6321
Add custom typed properties to requests #6321
Conversation
I need to fix up the document tests and maybe make minor changes to |
withProperties(properties.+(entry)) | ||
override def +(entry: TypedEntry[_], entries: TypedEntry[_]*): Repr = | ||
withProperties(properties.+(entry, entries: _*)) |
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.
Are properties that fundamental to the concept of a request header that they should be given the +
method on RequestHeader
? Why shouldn't the +
method be used to add request headers to the request header? If I saw:
val newRequestHeader = requestHeader + (Foo -> fooValue)
I think I would assume that this was adding a foo header to the request, which I think would be a fairly intuitive reading of that code, but it's wrong. So I don't think this is a good use of an operator, I think the method should have a name that makes it clear that a property is being added.
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 same goes for get
, apply
, contains
, if these methods exist at all, they should be for headers, not properties.
What's the plan for typed request headers? Maintain properties in addition to headers? Both would probably be implemented the same way, I wonder if the distinction is worthwhile. Another thing, is "properties" the best name? Properties has a meaning on the JVM, and generally refer to Strings. In contrast, "attributes" is not yet used in Play, or as far as I'm aware in a major way in any libraries that Play uses, and is familiar to anyone coming from the servlet API, since attributes is the analogous name for this feature there. |
|
What's the use case for this? On Tue, Jul 19, 2016 at 9:58 PM James Roper notifications@github.com
|
@jroper I agree on the naming. @nafg The point is to be able to store rich data on the request. The use case is essentially the same as tags but broader: instead of strings it's now possible to store any type, and access the values in a type-safe way. We currently could use this within Play for the CSRF filter, where we store the CSRF token and some other data on the request so it can be later used by CSRF.getToken and the template helpers. It can also be used as a general mechanism to allow filters to cache data on the request to be used later by actions. |
@jroper, thanks for the feedback. You may wish to look at my other (now closed) PR #6227 which went a lot further. That PR moved all the request vals into the map and supporting typed headers as properties. It also used normal method names instead of operators. I think that other PR was too complex because it had a very flexible system for adding properties to requests, including supporting interdependencies between properties, lazy properties, etc. For this PR I decided to make things a bit simpler. There's no reason we can't bring back some of those more complicated features later though; the surface API for this PR is intentionally very similar. I'll think about the other stuff, but for now I'll do some of the simpler things:
|
279901d
to
2aa1eda
Compare
} | ||
|
||
/** | ||
* Apply any filters to the given handler. | ||
*/ | ||
@deprecated("Use filterHandler(RequestHeader, Handler) instead", "2.6.0") | ||
protected def filterHandler(next: RequestHeader => Handler): (RequestHeader => Handler) = { |
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.
@jroper I deprecated this method because I couldn't see a good reason for requiring a function as an argument. Do you know the reason?
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.
Ping @jroper?
a37ff8f
to
c2ea972
Compare
// Avoid clash between method arg and RequestHeader field | ||
val remoteAddressArg = remoteAddress | ||
|
||
new RequestHeader { | ||
new RequestHeader with WithAttrMap[RequestHeader] { |
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 WithAttrMap
mixin is a private Play trait that provides a simple implementation of request attributes. Implementors just need to provide access to a TypedMap
to store the attributes in.
override val clientCertificateChain = _clientCertificateChain | ||
override val hasBody = _hasBody || super.hasBody | ||
} | ||
new RequestHeaderImpl( |
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.
Nothing special here, but notice we can reuse the RequestHeaderImpl
class.
This is ready for review now. When reviewed I'll make small fixes, squash commits, etc. Then hopefully we can merge it! |
/** | ||
* A TypedKey is a key that can be used to get and set values in a | ||
* [[TypedMap]] or any object with typed keys. Implementations of this | ||
* object should ensure they have stable and well-behaving equals |
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.
Perhaps it should also mention that equals
has to ensure there can't be a collision between different types, meaning for types A and B such that B is not <: A, a subclass of TypedKey[X] T[X] must obey that a T[A] must never equal a T[B]. The caveats mentioned apply to any map key, but we need this guarantee as well
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 think if their types are different then keys should never be equal. But if their types are different then they're really different keys, and different keys should never be equal. I'm not sure if this is obvious or not, I'll add a note if I can think of a good way of expressing it.
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.
What I mean is the current wording doesn't rule out, I don't know, say case class MyCustomTypedKey[A](msg: String, labels: String*) extends TypedKey[A]
-- equals and hashCode will be defined "correctly" since it's a case class, but it won't behave correctly in TypedMap.
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.
Do we really need to allow subclasses? The safest thing would be to make it sealed
. With reference equality the only way to break the map is by casting deliberately.
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.
Good point. Maybe subclassing is not needed. We could just make TypedKey
a final class with reference equality semantics. That would prevent any misuse.
0ece6a7
to
957bd97
Compare
Fixes made, commits squashed, rebased to latest master to fix issue with Now ready for merging when tests pass. |
957bd97
to
6b6d768
Compare
Hopefully tests pass now. The request rewriting changes that I'd made interfered with how ( |
RequestHeader and Request can now have custom attributes added to them. This lets plugins and users add their own data to requests. Adding custom attributes was sort of possible by using tags, but tag values could only be strings. The new API adds support for typed attributes. I have intentionally restricted the API to hide internal details of how attributes are implemented. This is because I want to make it possible to change the underlying implementation later if we want to support more complicated features. In particular, I've avoided exposing the underlying map that's used to store the attributes. This is because we might want to support lazy or dynamic attributes later, and these types of attributes might not use a map. Fixes playframework#5814. Other misc changes: * Changed Java Request's username property to be an use attribute. * Refactored Java's Request to implement it in Scala. Fixes playframework#6193. * Use `Handler.Stage` for request and handler rewriting.
6b6d768
to
319dee6
Compare
LGTM |
RequestHeader and Request can now have custom attributes added to them. This lets plugins and users add their own data to requests. Adding custom attributes was sort of possible by using tags, but tag values could only be strings. The new API adds support for typed attributes. I have intentionally restricted the API to hide internal details of how attributes are implemented. This is because I want to make it possible to change the underlying implementation later if we want to support more complicated features. In particular, I've avoided exposing the underlying map that's used to store the attributes. This is because we might want to support lazy or dynamic attributes later, and these types of attributes might not use a map. Fixes playframework#5814. Other misc changes: * Changed Java Request's username property to be an use attribute. * Refactored Java's Request to implement it in Scala. Fixes playframework#6193. * Use `Handler.Stage` for request and handler rewriting.
RequestHeader and Request can now have custom properties added to
them. This lets plugins and users add their own data to requests.
Adding custom properties was sort of possible by using tags, but tag
values could only be strings. The new API adds support for typed
properties.
Typed properties are accessed using methods similar to Maps. For
example, the
+
method adds a new property andapply
orget
canbe used to retrieve a property.
I have intentionally restricted the API to hide internal details of
how properties are implemented. This is because I want to make it
possible to change the underlying implementation later if we want to
support more complicated features. For example, I've avoided exposing
the underlying map that's used to store the properties. This is
because we might want to support lazy or dynamic properties later, and
these might not be implemented by storing them in a simple map.
Fixes #5814.
TODO: