-
Notifications
You must be signed in to change notification settings - Fork 27.5k
[WIP] feat($httpUrlParams): introduce new service abstracting params serialization #11238
[WIP] feat($httpUrlParams): introduce new service abstracting params serialization #11238
Conversation
…ization Closes angular#7429 Closes angular#9224
var HttpUrlParams = {}; | ||
|
||
HttpUrlParams.serialize = function(params, mode) { | ||
return paramSerializers[lowercase(mode) || provider.defaultMode](params); |
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.
Should we throw if a mode is not available? I think we should.
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.
If we decide to throw, it would be better to throw a more meaningful error (instead of undefined is not a function
).
I would also consider logging the incident and falling back to the default mode.
BTW, you are using lowercase
here, but not when registering. They should be consistent (and probably documented).
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.
We should ditch the lowercase
altogether. People should be able to specify the correct capitalization.
This is a great concept to be able to specify this My first thought is does this really need to be a separate service? It seems like it would quite easily be added to the |
@petebacondarwin you need a separate service due to #3311 |
function buildUrl(url, params, mode) { | ||
params = $httpUrlParams.serialize(params, mode); | ||
if (params) { | ||
url += ((url.indexOf('?') == -1) ? '?' : '&') + params; |
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.
While you are touching this, ===
would be nice :)
@petebacondarwin @Narretz @gkalpak thnx for the detailed review of the code - all the comments are very valid and for sure I'm going to incorporate those into the final version. But what I need mostly from you now is a OK / KO when it comes to concepts and APIs exposed to the users. Are there any other concerns apart from @petebacondarwin question about having this logic in a separate service? Once again, the need for a separate service was driven by those 2 issues:
If you think that the current approach / APIs are reasonable I would add tests and address all the comments so we can get it into 1.4. But if you've got any concerns regarding the approach it is time to speak up now :-) |
@pkozlowski-opensource - OK so here is my take on it. This should by no means be taken as the right way, it is just an opinion... I would steer away from using strings and lookups like this. It adds an additional opportunity for bugs to be disguised. Instead I would follow a similar design pattern as Http Interceptors, where a paramSerializer is just a service that implements an interface. Anyone is free to create their own version of this that will do the serialization in their own way: mod.factory('myParamSerializer', function() {
return function myParamSerializer(params) {
...
};
}); Out of the box we would provide two standard versions: Then you just pass the version you want to myMod.controller('MyCtrl', function($scope, $http) {
$http.get(myUrl, {
params: {...},
paramSerializer: '$jqueryParamSerializer'
}).then(...);
}); or you could set a default in the myMod.config(function($httpProvider) {
$httpProvider.defaultParamSerializer('$jqueryParamSerializer');
}); Following the Http Interceptor design, you could also define the serializer "inline" instead of as a service. |
@petebacondarwin I like what you are saying here and my initial proposal was kind of going in this direction. But I've changed this, mostly based on @caitp input, as I believe that:
@petebacondarwin @caitp or anyone interested - could we try to have a hangout on this topic somewhere early next week to clarify / agree upon the design? I would really love to ship this thing with 1.4! |
i can do a hangout, but my 2c is still that it should be kept as simple as possible. |
OK just to clarify... There could be a getter on the $http service that returns you the current default serialised for use in tests. My idea would allow the name of the serializer to be passed instead of an instance avoiding the need to inject it |
I hope is not too late for debate... |
OK, so we start to have different proposals from @petebacondarwin, @caitp and @jmendiara now and if we want to land something in 1.4 we need to chose now :-) To facilitate the decision process here is once again a list of problems we are trying to tackle with this change:
Now, with my proposal here is how one would tackle each use-case. For (1) and (2): angular.module('app', [], function($HttpUrlParamsProvider) {
$HttpUrlParamsProvider.defaultMode = 'jquery';
}).controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}})
}); would result in For (3) people could do: angular.module('app', []).controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}, paramsMode: 'jquery'})
}); To tackle (4) one would do: angular.module('app', [], function($HttpUrlParamsProvider) {
$HttpUrlParamsProvider.registerSerializer('mycustom', function(params) {
return //whatever crazy serialization people want to have
});
$HttpUrlParamsProvider.defaultMode = 'mycustom';
}).controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}})
}); and finally, one could inject $httpBackend.expectGET('http://host.com?' + $httpUrlParams.serialize( {foo: ['bar', 'baz']})); as well as for the per-request basis: $httpBackend.expectGET('http://host.com?' + $httpUrlParams.serialize( {foo: ['bar', 'baz']}, 'mycustom')); Now, what I really dislike here is relaying on string-based names that are really easy to get wrong... At the same time I would appreciate solutions where (4) and (5) are properly handled. From what I understand @caitp is proposing: angular.module('app', []).controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}, serialiseAsJQuery=true})
}); which is easy to use and tackles (1) as well as (2) but doesn't help with the rest, as far as I can see. @petebacondarwin @jmendiara how the code would look like with your respective proposals? |
My suggestion is not that much different except it removes the need for this registration service. We would provide two basic serializers (as angular services) out of the box:
The Custom serializers would simply be angular services, see Below are the demonstrations of how this would work for each use case. 1/2) Specify the serializer at the application level: angular.module('app', [])
.config(function($httpProvider) {
$httpProvider.defaultParamSerializer('$jqueryParamSerializer');
})
.controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}});
}); 3) Specify the serializer at the request level: angular.module('app', [])
.controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}, paramSerializer: '$jqueryParamSerializer'});
}); or more explicitly .controller('MyCtrl', function($http, $jqueryParamSerializer) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}, paramSerializer: $jqueryParamSerializer});
}); 4) Provide a completely custom serializer: angular.module('app', [])
.factory('myCustomParamSerializer', function() {
return function myCustomParamSerializer(params) {
return // Whatever crazy serialization people want to have
};
})
.config(function($httpProvider) {
$httpProvider.defaultParamSerializer('myCustomParamSerializer');
})
.controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}})
}); 5) Enable serialization during testing: it('should respond to a request with default serialization', inject(function($httpBackend, $http) {
$httpBackend.expectGET('http://host.com?' + $http.defaultParamSerializer({foo: ['bar', 'baz']}));
})); it('should respond to a request with a specific serialization', inject(function($httpBackend, myCustomParamSerializer) {
$httpBackend.expectGET('http://host.com?' + myCustomParamSerializer({foo: ['bar', 'baz']}));
})); |
@petebacondarwin oh, I see, you would expose a custom serializer on the @jmendiara how is your proposal different from what we are discussing here? |
@pkozlowski-opensource It's more like @petebacondarwin approach, but clearly separates param serialization from URL Building (which includes requestParameters concatenation with My WIP #11386 didn't ship jquery support in order to keep core small and simple (should core ship other fws functionalities? read @caitp comments ) and let users to implement this on their own apps (as an external dependency) or maybe provide an My approach registers an optional core provider that helps on urlBuilding, but is not needed at all, just sugar to developers to hack easily into the functionality. Although not in the WIP the jquery support can also be implemented AssumingTaking @petebacondarwin snippets as starting point... 1/2) Specify the serializer at the application level: angular.module('app', [])
.config(function($httpProvider) {
$httpProvider.defaultURLBuilder('$jqueryURLBuilder');
//OR $httpProvider.defaults.buildUrl = '$jqueryURLBuilder';
// the last one is just like specifying the default Cache for request, keeping the same paradigm
})
.controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}});
}); 3) Specify the serializer at the request level: angular.module('app', [])
.controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}, buildUrl: '$jqueryURLBuilder'});
}); or more explicitly .controller('MyCtrl', function($http, $jqueryURLBuilder) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}, buildUrl: $jqueryURLBuilder});
}); 4) Provide a completely custom serializer: //petebacondarwin use case is supported
angular.module('app', [])
.factory('myCustomUrlBuilder', function() {
return function myCustomUrlBuilder(url, params) {
return // Whatever crazy serialization people want to have
};
})
.config(function($httpProvider) {
$httpProvider.defaultURLBuilder('myCustomUrlBuilder');
})
.controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {foo: ['bar', 'baz']}})
}); or using my optional-to-implement-in-the-core angular.module('app')
//Create a function that says how an object should be converted to a `List of {key, value}` strings
.factory('arraySerializer', function() {
return function arraySerializer(params, addKeyValue) {
angular.forEach(params, function(value, key) {
if(angular.isArray(value)) {
angular.forEach(value, function(arrValue) {
addKeyValue(key+ '[]', String(arrValue));
});
} else {
addKeyValue(key, String(value));
}
});
}
})
//We rehuse the common way of building URLs
.factory('arrayStandardBuildUrl', function($httpUrlBuilderFactory, arraySerializer) {
return $httpUrlBuilderFactory(arraySerializer);
})
//we rehuse the arraySerialization, but making other kind URLs
// semicolonURLs returns a function(url, params) that returns urls concatenated with ;
// see https://github.com/jmendiara/angular.js/blob/http_params_serialization/src/ng/http.js#L1230
.factory('arrayCustomBuildUrl', function(arraySerializer, semicolonURLs) {
return semicolonURLs(arraySerializer);
})
.controller('MyCtrl', function($http) {
$http.get('http://host.com', {params: {id: 5, foo: ['bar', 'baz']},
buildUrl: 'arrayStandardBuildUrl'
}); // GET http://host.com?id=5&foo[]=bar&foo[]=baz
$http.get('http://host.com', {params: {id: 5, foo: ['bar', 'baz']},
buildUrl: 'arrayCustomBuildUrl'
}); // GET http://host.com?id=5;foo[]=bar;foo[]=baz
}) 5) Enable serialization during testing: it('should respond to a request with default serialization', inject(function($httpBackend, $http) {
var url = 'http://host.com', params = {foo: ['bar', 'baz']};
$httpBackend.expectGET($http.defaultURLBuilder(url, params));
$http.get(url, {params: params});
}));
it('should respond to a request with a specific serialization', inject(function($httpBackend, $http, myCustomUrlBuilder) {
var url = 'http://host.com', params = {foo: ['bar', 'baz']};
$httpBackend.expectGET(myCustomUrlBuilder(url, params));
$http.get(url, {params: params, buildUrl: myCustomUrlBuilder });
})); As you can see Why complete URL building and not only param serialization??
What i dislike most of my approach What i like most of my approach |
I really like the idea of using the |
I can see the benefit in providing a way to plugin in to the whole HTTP url building process but I think that this In the meantime, I wonder if we could solve the given use cases without resorting to completely opening up the url building:
|
I would very much like to avoid going into cache-related issues as part of what we are doing here... IMO the current cache implementation for $http leaves much to be desired, especially in terms of what people can control (ex.: when things are cached, under which key etc.). In short: cache has its own set of issues that we should tackle separately, IMO. |
I agree that it would be best if we could keep this simple. I believe if we attach the generated URL, as returned from the private function This would allow a request interceptor to have access to this URL but also have the opportunity to completely rebuild this URL in any way they want, which is effectively the requirement of @jmendiara. What do you think? |
I don't think I feel comfortable with using |
That's fine. It doesn't relate directly to this PR anyway and could always be added later if it was wanted. I think the response headers would also already contain this info, right? |
Probably. In any case I don't want to go into cache-related problems just when we should be shipping this one :-) For now I'm leaning towards @petebacondarwin proposal - if there are no objections I can re-submit my PR with this idea implemented. |
I think Pete's proposal is the best with regards to consistency with the rest of Angular. |
oauth1 signature needs params + url + method If we have I'm talking now about introducing the ability to Small code change, no new services/providers in core, fully customization by user and more use cases covered. BTW, @pkozlowski-opensource whichever decission you take, tell me if you need help with anything |
Suppressed by #11461 |
@jmendiara if you got a minute to review #11461, this would be much appreciated! |
@petebacondarwin @caitp here is another stab at having configurable strategies for $http request params serialization. This is still WIP (missing tests and doc updates) but wanted to check this one with you before putting more work into this approach.
Here is the basic idea:
$httpUrlParams
, that is responsible for serializing $http's request paramsserialize = function(params, mode)
wheremode
is a flag / key that people can use to trigger different serialization strategies (ex. per domain)traditional
) and register mode handlers (a function)$http
calls can trigger different modes by passingparamsMode
in the config object, ex.:It would be totally awesome if we could ship it with 1.4, so any input would be much appreciated.