papyrus icon indicating copy to clipboard operation
papyrus copied to clipboard

Improved Organisation of APIs

Open KaiTheRedNinja opened this issue 2 years ago • 2 comments

Issue:

When it comes to larger APIs, for example, the Google Classroom API, which I work with very often at glassroom, using Papyrus would result in a lot of messy top-level APIs.

How glassroom currently solves this is to declare all the protocols as enums, and all the functionality goes into extensions of those enums as static functions. This means that the definitions look like this:

// Definitions of all the APIs. They're implemented in other files.
public enum GlassRoomAPI {
    public enum GRCourses: GlassRoomAPIProtocol {
        public enum GRAliases: GlassRoomAPIProtocol {}
        public enum GRAnnouncements: GlassRoomAPIProtocol {}
        public enum GRCourseWork: GlassRoomAPIProtocol {
            public enum GRStudentSubmissions: GlassRoomAPIProtocol {}
        }
/* etc */

and calling functions looks more like this:

GlassRoomAPI.GRCourses.GRCourseWork.list(/* parameters here */) { result in
	/*completion here*/ 
}

However, since Papyrus uses protocols for the definitions and the resultant APIs are autogenerated, such organisation cannot be achieved.

@API
protocol GRCourses {
	/*methods here*/

	@API
	protocol GRAliases { // does not compile, as you cannot define other objects within a protocol
		/*methods here*/
    }
}

Suggested solution:

My suggestion is to have an option in @API and @Mock to extend a struct with the new functionality, instead of generating a whole new struct. This would allow for organisation by defining all the structs in a neat manner.

// empty implementations
struct GRCourses {
	struct GRAliases {}
}

@API(in: GRCourses.self)
protocol GRCoursesProtocol {
	/* methods here */
}

@API(in: GRAliases.self)
protocol GRAliasesProtocol {
	/* methods here */
}

/* 
// autogenerated:

extension GRCourses {
	/* implementations here */
}

extension GRAliases {
	/* implementations here */
}
*/

And you would call them this way:

let result = try await GRCourses.GRAliases().listAliases(/* parameters */)

KaiTheRedNinja avatar Jun 25 '23 04:06 KaiTheRedNinja

Thanks for the feedback @KaiTheRedNinja!

Great idea! I 100% agree it would be great to be able to better organize large API groups. Unfortunately macros are relatively nascent and so there are a few limitations:

  1. As you mentioned, nested protocols aren't allowed - there is some recent work being done here though.
  2. At the moment, macros aren't allowed to generate extensions. There's some movement towards removing this limitation but until then anything involving extensions on existing types isn't possible. Hopefully that will land soon, but before then I believe generating a new top-level type is the only viable solution.

Ideally, I think it would be great to completely hide the implementor of the protocol. This way there wouldn't be a bunch of concrete API types floating around and calling the API would look something like this:

@API
protocol GRAliases { ... }

let aliases: GRAliases = provider.create(GRAliases.self)
let result = try await aliases.listAliases(...)

Macros generating extensions would also unlock the in parameter as you recommended to further allow organization via nesting inside enums, structs, etc. Once it's available I think that would be a great addition.

joshuawright11 avatar Jun 27 '23 03:06 joshuawright11

With SE-404 organizing APIs inside a type is possible now...

public enum APIs {
    @API
    public protocol Foo {
        @GET("/bar")
        func bar() async throws
    }
}

let foo: APIs.Foo = APIs.FooAPI(provider: provider)

Unfortunately there's still no way for macros to extend types that they aren't annotating - but I think with this answer you should be able to get more organized outputs as long as there's a consistent suffix.

joshuawright11 avatar Jun 02 '24 14:06 joshuawright11