-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Compile-time safe way to restrict API endpoint calls #2
Comments
So you have user generated endpoints? |
I have endpoints generated by the user of the library, another developer. The user of an app can't define any. |
Can you make the MoyaProvider generic and have it take an enum as a parameter? Then you can verify the endpoint definition take one of those enums and the request method take one of the enums. The user of the library would still need to associate each enum value with an endpoint... Not sure best way to do that. |
You could do two sort of things, if I am understanding correctly. Have an enum that defines every endpoint, or pieces of an endpoint, which may be super huge, something like: enum EndpointPiece:String {
case Base = "http://rdjpg.com/"
case TwoHundred = "200/"
case ThreeHundred = "300/"
....
} And then have methods to concatenate them to form an actual endpoint. This may give you the closest thing to type safety. Or you could have a protocol like so: protocol EndPoint {
typealias E
func endPoint(point:String) -> E
} And then only take objects conforming to the |
A more correct protocol: protocol EndPoint {
func endPoint() -> String
} |
struct Endpoint<T> {
let URL: T
}
struct MoyaProvider<T: RawRepresentable> {
let EndpointType: T.Type
let endpoints: [Endpoint<T>]
func request(URL: T) {
println(URL.toRaw())
}
}
enum RdJpg:String {
case ThreeHundredByTwoHundred = "http://rdjpg.com/300/200/"
}
enum Foo:String {
case Bar = "http://foo.com/bar"
case Baz = "http://foo.com/baz"
}
let rdJpgEndpoint = Endpoint(URL: RdJpg.ThreeHundredByTwoHundred)
let fooEndpoint = Endpoint(URL: Foo.Bar)
// Valid providers
let rdJpgProvider = MoyaProvider(EndpointType: RdJpg.self, endpoints: [rdJpgEndpoint])
let fooProvider = MoyaProvider(EndpointType: Foo.self, endpoints: [fooEndpoint])
// Invalid providers
//let rdJpgProvider = MoyaProvider(EndpointType: RdJpg.self, endpoints: [fooEndpoint])
//let fooProvider = MoyaProvider(EndpointType: Foo.self, endpoints: [rdJpgEndpoint])
// Valid requests
rdJpgProvider.request(RdJpg.ThreeHundredByTwoHundred)
fooProvider.request(Foo.Bar)
fooProvider.request(Foo.Baz)
// Invalid requests
//rdJpgProvider.request(Foo.Bar)
//rdJpgProvider.request(Foo.Baz)
//fooProvider.request(RdJpg.ThreeHundredByTwoHundred) |
You could get better type safety by making the protocol EndpointURL: RawRepresentable {
class func fromRaw(raw: String) -> Self?
}
enum RdJpg:String, EndpointURL {
case ThreeHundredByTwoHundred = "http://rdjpg.com/300/200/"
} |
Altogether now: protocol EndpointURL: RawRepresentable {
class func fromRaw(raw: String) -> Self?
}
struct Endpoint<T> {
let URL: T
}
struct MoyaProvider<T: EndpointURL> {
let EndpointType: T.Type
let endpoints: [Endpoint<T>]
func request(URL: T) {
println(URL.toRaw())
}
}
enum RdJpg:String, EndpointURL {
case ThreeHundredByTwoHundred = "http://rdjpg.com/300/200/"
}
enum Foo:String, EndpointURL {
case Bar = "http://foo.com/bar"
case Baz = "http://foo.com/baz"
}
let rdJpgEndpoint = Endpoint(URL: RdJpg.ThreeHundredByTwoHundred)
let fooEndpoint = Endpoint(URL: Foo.Bar)
// Valid providers
let rdJpgProvider = MoyaProvider(EndpointType: RdJpg.self, endpoints: [rdJpgEndpoint])
let fooProvider = MoyaProvider(EndpointType: Foo.self, endpoints: [fooEndpoint])
// Invalid providers
//let rdJpgProvider = MoyaProvider(EndpointType: RdJpg.self, endpoints: [fooEndpoint])
//let fooProvider = MoyaProvider(EndpointType: Foo.self, endpoints: [rdJpgEndpoint])
// Valid requests
rdJpgProvider.request(.ThreeHundredByTwoHundred)
fooProvider.request(.Bar)
fooProvider.request(.Baz)
// Invalid requests
//rdJpgProvider.request(Foo.Bar)
//rdJpgProvider.request(Foo.Baz)
//fooProvider.request(RdJpg.ThreeHundredByTwoHundred) |
Edited the above to reflect that type can be inferred for valid request parameters. |
Here is one possible solution: // Defined in Moya:
protocol StringConvertible {
func toString() -> String
}
class Endpoint<T: StringConvertible> {
let baseURL: String
let slug: T
init(baseURL: String, slug: T) {
self.baseURL = baseURL
self.slug = slug
}
func fullURLString() -> String {
return self.baseURL + self.slug.toString()
}
}
// So a user of Moya can write:
enum Slug: String, StringConvertible {
case DoTaskOne = "/do/task1"
case DoTaskTwo = "/do/task2"
// Unfortunately, the following method has to be
// defined by all Moya users
func toString() -> String {
return self.toRaw()
}
}
var endpoint = Endpoint<Slug>(baseURL: "http://example.com", slug: .DoTaskOne)
println(endpoint.fullURLString()) |
Hey, looks like it's also possible to get rid of the toString() requirement:
and then:
Personally, I like the StringConvertible way better, because the API contract is clearer there, methinks. |
I really, realy dig these. We could even go further and abstract away the need for a URL string, which would decouple its 1-1 relationship with an endpoint. That way, you could provide a different URL depending on parameters, etc. I'll put together a pull request. |
Cool. Take a look at #6 and let me know what you think. |
Remove string percent encoding in NetworkLoggerPlugin #2
# This is the 1st commit message: Merge pull request Moya#1335 from devxoul/map-decodable Add support for mapping response to Decodable object # The commit message Moya#2 will be skipped: # feat: add support for JSON Encodable requests. # The commit message Moya#3 will be skipped: # chore: add changelog entry and added documentation regarding the new request
So this is a tough one that I need to figure out an abstraction for. The basic gist is as follows: You set up a
MoyaProvider
with a bunch of endpoints. Cool, right? Except as of this moment, you access the API like this:Not cool – let's avoid stringly-typedness in this library.
Is there a way to provide a compile-time check that the endpoint actually exists? Can we set up some sort of enum or token or something that you pass in when you create the endpoint, and then reuse that token later on when calling the API? It needs to be something accessible by both whoever sets up the API at app launch, likely the app delegate, and anyone else who wants to access it (view controllers, network layers, whoever).
It's tricky since I want a clear separation of this library and the app code. Since each app has a different API, this abstraction – whatever it ends up being – needs to be something defined in app code, but is used by the library.
So I need a thing that someone using this library creates, uses to register the endpoint, then uses somewhere else in order to access the API. Any thoughts?
The text was updated successfully, but these errors were encountered: