Skip to content
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

Closed
ashfurrow opened this issue Aug 16, 2014 · 13 comments · Fixed by #6
Closed

Compile-time safe way to restrict API endpoint calls #2

ashfurrow opened this issue Aug 16, 2014 · 13 comments · Fixed by #6

Comments

@ashfurrow
Copy link
Member

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:

let provider = MoyaProvider(endpoints: [
                    Endpoint(URL: "http://rdjpg.com/300/200/", sampleResponse: {
                        /* doesn't matter */
                    })
                ])

...

provider.request("http://rdjpg.com/300/200/", completion: { (object: AnyObject?) -> () in
    ...
})

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?

@tLewisII
Copy link

So you have user generated endpoints?

@ashfurrow
Copy link
Member Author

I have endpoints generated by the user of the library, another developer. The user of an app can't define any.

@michaelmcguire
Copy link

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.

@tLewisII
Copy link

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 EndPoint protocol, so endpoints would at least have to be constructed out of structs or objects, and not just defined as strings all over the place.

@tLewisII
Copy link

A more correct protocol:

protocol EndPoint {
    func endPoint() -> String
}

@paulyoung
Copy link

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)

@paulyoung
Copy link

You could get better type safety by making the enums conform to a protocol like this:

protocol EndpointURL: RawRepresentable {
  class func fromRaw(raw: String) -> Self?
}

enum RdJpg:String, EndpointURL {
    case ThreeHundredByTwoHundred = "http://rdjpg.com/300/200/"
}

@paulyoung
Copy link

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)

@paulyoung
Copy link

Edited the above to reflect that type can be inferred for valid request parameters.

@roop
Copy link

roop commented Aug 17, 2014

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())

@roop
Copy link

roop commented Aug 17, 2014

Hey, looks like it's also possible to get rid of the toString() requirement:

// Defined in Moya
protocol StringBackedEnum {
    func toRaw() -> String
}

and then:

// Code using Moya
enum Slug: String, StringBackedEnum {
    case DoTaskOne = "/do/task1"
    case DoTaskTwo = "/do/task2"
}

Personally, I like the StringConvertible way better, because the API contract is clearer there, methinks.

@ashfurrow
Copy link
Member Author

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.

@ashfurrow ashfurrow mentioned this issue Aug 17, 2014
@ashfurrow
Copy link
Member Author

Cool. Take a look at #6 and let me know what you think.

pedrovereza added a commit that referenced this issue Jun 30, 2017
Remove string percent encoding in NetworkLoggerPlugin #2
afonsograca pushed a commit to afonsograca/Moya that referenced this issue Oct 7, 2017
# 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants