Coder Social home page Coder Social logo

papyrus's Introduction

๐Ÿ“œ Papyrus

Swift Version Latest Release License

Papyrus is a type-safe HTTP client for Swift.

It reduces your network boilerplate by turning turns your APIs into clean and concise Swift protocols.

It's Retrofit for Swift!

@API
@Authorization(.bearer("<my-auth-token>"))
protocol Users {
    @GET("/user")
    func getUser() async throws -> User

    @POST("/user")
    func createUser(email: String, password: String) async throws -> User

    @GET("/users/:username/todos")
    func getTodos(username: String) async throws -> [Todo]
}
let provider = Provider(baseURL: "https://api.example.com/")
let users: Users = UsersAPI(provider: provider)
let todos = try await users.getTodos(username: "joshuawright11")

Each endpoint of your API is represented as function on the protocol.

Annotations on the protocol, functions, and parameters help construct requests and decode responses.

Table of Contents

  1. Features
  2. Getting Started
  3. Requests
  4. Responses
  5. Advanced
  6. Testing
  7. Acknowledgements
  8. License

Features

  • Turn REST APIs into Swift Protocols
  • async/await or Callback APIs
  • JSON, URLForm and Multipart Encoding Support
  • Automatic Key Mapping
  • Sensible Parameter Defaults Based on HTTP Verb
  • Automatically Decode Responses with Codable
  • Custom Interceptors & Request Builders
  • Advanced Error Handling
  • Automatic Mocks for Testing
  • Powered by URLSession or Alamofire Out of the Box
  • Linux / Swift on Server Support Powered by async-http-client

Getting Started

Requirements

Supports iOS 13+ / macOS 10.15+.

Keep in mind that Papyrus uses macros which require Swift 5.9 / Xcode 15 to compile.

Installation

Install Papyrus using the Swift Package Manager, choosing a backing networking library from below.

URLSession

URLSession

Out of the box, Papyrus is powered by URLSession.

.package(url: "https://github.com/joshuawright11/papyrus.git", from: "0.6.0")
.product(name: "Papyrus", package: "papyrus")
Alamofire

Alamofire

If you'd prefer to use Alamofire, use the PapyrusAlamofire product.

.package(url: "https://github.com/joshuawright11/papyrus.git", from: "0.6.0")
.product(name: "PapyrusAlamofire", package: "papyrus")
AsyncHTTPClient (Linux)

AsyncHTTPClient (Linux)

If you're using Linux / Swift on Server, use the separate package PapyrusAsyncHTTPClient. It's driven by the swift-nio backed async-http-client.

.package(url: "https://github.com/joshuawright11/papyrus-async-http-client.git", from: "0.2.0")
.product(name: "PapyrusAsyncHTTPClient", package: "papyrus-async-http-client")

Requests

You'll represent each of your REST APIs with a protocol.

Individual endpoints are represented by a function on that protocol.

The function's parameters help Papyrus build the request and the return type indicates how to handle the response.

Method and Path

Set the request method and path as an attribute on the function. Available methods are GET, POST, PATCH, DELETE, PUT, OPTIONS, HEAD, TRACE, and CONNECT. Use @HTTP(_ path:method:) if you need a custom method.

@POST("/accounts/transfers")

Path Parameters

Parameters in the path, marked with a leading :, will be automatically replaced by matching parameters in the function.

@GET("/users/:username/repos/:id")
func getRepository(username: String, id: Int) async throws -> [Repository]

Query Parameters

Function parameters on a @GET, @HEAD, or @DELETE request are inferred to be a query.

@GET("/transactions") // GET /transactions?merchant=...
func getTransactions(merchant: String) async throws -> [Transaction]

If you need to add query paramters to requests of other HTTP Verbs, mark the parameter with Query<T>.

@POST("/cards") // POST /cards?username=...
func fetchCards(username: Query<String>) async throws -> [Card]

Static Query Parameters

Static queries can be set directly in the path string.

@GET("/transactions?merchant=Apple")

Headers

A variable request header can be set with the Header<T> type. It's key will be automatically mapped to Capital-Kebab-Case. e.g. Custom-Header in the following endpoint.

@GET("/accounts")
func getRepository(customHeader: Header<String>) async throws

Static Headers

You can set static headers on a request using @Headers at the function or protocol scope.

@Headers(["Cache-Control": "max-age=86400"])
@GET("/user")
func getUser() async throws -> User
@API
@Headers(["X-Client-Version": "1.2.3"])
protocol Users { ... }

Authorization Header

For convenience, the @Authorization attribute can be used to set a static "Authorization" header.

@Authorization(.basic(username: "joshuawright11", password: "P@ssw0rd"))
protocol Users {
    ...
}

Body

Function parameters on a request that isn't a @GET, @HEAD, or @DELETE are inferred to be a field in the body.

@POST("/todo")
func createTodo(name: String, isDone: Bool, tags: [String]) async throws

If you need to explicitly mark a parameter as a body field, use Field<T>.

@POST("/todo")
func createTodo(name: Field<String>, isDone: Field<Bool>, tags: Field<[String]>) async throws

Body<T>

Aternatively, the entire request body can be set using Body<T>. An endpoint can only have one Body<T> parameter and it is mutually exclusive with Field<T>.

struct Todo: Codable {
    let name: String
    let isDone: Bool
    let tags: [String]
}

@POST("/todo")
func createTodo(todo: Body<Todo>) async throws

Body Encoding

By default, all Body and Field parameters are encoded as application/json. You can encode with a custom JSONEncoder using the @JSON attribute.

extension JSONEncoder {
    static var iso8601: JSONEncoder {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return encoder
    }
}

@JSON(encoder: .iso8601)
@POST("/user")
func createUser(username: String, password: String) async throws
URLForm

You may encode body parameters as application/x-www-form-urlencoded using @URLForm.

@URLForm
@POST("/todo")
func createTodo(name: String, isDone: Bool, tags: [String]) async throws
Multipart

You can also encode body parameters as multipart/form-data using @Multipart. If you do, all body parameters must be of type Part.

@Multipart
@POST("/attachments")
func uploadAttachments(file1: Part, file2: Part) async throws
Global Encoding

You can attribute your protocol with an encoding attribute to encode all requests as such.

@API
@URLForm
protocol Todos {
    @POST("/todo")
    func createTodo(name: String, isDone: Bool, tags: [String]) async throws

    @PATCH("/todo/:id")
    func updateTodo(id: Int, name: String, isDone: Bool, tags: [String]) async throws
}
Custom Body Encoders

If you'd like to use a custom encoder, you may pass them as arguments to @JSON, @URLForm and @Multipart.

extension JSONEncoder {
    static var iso8601: JSONEncoder {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return encoder
    }
}

@JSON(encoder: .iso8601)
protocol Todos { ... }

Responses

The return type of your function tells Papyrus how to handle the endpoint response.

Decodable

If your function returns a type conforming to Decodable, Papyrus will automatically decode it from the response body using JSONDecoder.

@GET("/user")
func getUser() async throws -> User

Data

If you only need a response's raw body bytes, you can just return Data? or Data from your function.

@GET("/bytes")
func getBytes() async throws -> Data?

@GET("/image")
func getImage() async throws -> Data // this will throw an error if `GET /image` returns an empty body

Void

If you just want to confirm the response was successful and don't need to access the body, you may leave out the return type.

@DELETE("/logout")
func logout() async throws

Response

If you want the raw response data, e.g. to access headers, set the return type to Response.

@GET("/user")
func getUser() async throws -> Response

let res = try await users.getUser()
print("The response had headers \(res.headers)")

If you'd like to automatically decode a type AND access the Response, you may return a tuple with both.

@GET("/user")
func getUser() async throws -> (User, Response)

let (user, res) = try await users.getUser()
print("The response status code was: \(res.statusCode!)")

Error Handling

If any errors occur while making a request, a PapyrusError will be thrown. Use it to access any Request and Response associated with the error.

@GET("/user")
func getUser() async throws -> User

do {
    let user = try await users.getUser()
} catch {
    if let error = error as? PapyrusError {
        print("Error making request \(error.request): \(error.message). Response was: \(error.response)")
    }
}

Advanced

Parameter Labels

If you use two labels for a function parameter, the second one will be inferred as the relevant key.

@GET("/posts/:postId")
func getPost(id postId: Int) async throws -> Post

Key Mapping

Often, you'll want to encode request fields and decode response fields using something other than camelCase. Instead of setting a custom key for each individual attribute, you can use @KeyMapping at the function or protocol level.

Note that this affects Query, Body, and Field parameters on requests as well as decoding content from the Response.

@API
@KeyMapping(.snakeCase)
protocol Todos {
    ...
}

Access Control

When you use @API or @Mock, Papyrus will generate an implementation named <protocol>API or <protocol>Mock respectively. The access level will match the access level of the protocol.

Request Modifiers

If you'd like to manually run custom request build logic before executing any request on a provider, you may use the modifyRequests() function.

let provider = Provider(baseURL: "https://sandbox.plaid.com")
    .modifyRequests { (req: inout RequestBuilder) in
        req.addField("client_id", value: "<client-id>")
        req.addField("secret", value: "<secret>")
    }
let plaid: Plaid = PlaidAPI(provider: provider)

Interceptors

You may also inspect a Provider's raw Requests and Responses using intercept(). Make sure to call the second closure parameter if you want the request to continue.

let provider = Provider(baseURL: "http://localhost:3000")
    .intercept { req, next in
        let start = Date()
        let res = try await next(req)
        let elapsedTime = String(format: "%.2fs", Date().timeIntervalSince(start))
        // Got a 200 for GET /users after 0.45s
        print("Got a \(res.statusCode!) for \(req.method) \(req.url!.relativePath) after \(elapsedTime)")
        return res
    }

RequestModifer & Interceptor protocols

You can isolate request modifier and interceptor logic to a specific type for use across multiple Providers using the RequestModifer and Interceptor protocols. Pass them to a Provider's initializer.

struct MyRequestModifier: RequestModifier { ... }
struct MyInterceptor: Interceptor { ... }
let provider = Provider(baseURL: "http://localhost:3000", modifiers: [MyRequestModifier()], interceptors: [MyInterceptor()])

Callback APIs

Swift concurrency is the modern way of running asynchronous code in Swift.

If you haven't yet migrated to Swift concurrency and need access to a callback based API, you can pass an @escaping completion handler as the last argument in your endpoint functions.

The function must have no return type and the closure must have a single argument of type Result<T: Codable, Error>, Result<Void, Error>, or Response argument.

// equivalent to `func getUser() async throws -> User`
@GET("/user")
func getUser(callback: @escaping (Result<User, Error>) -> Void)

// equivalent to `func createUser(email: String, password: String) async throws`
@POST("/user")
func createUser(email: String, password: String, completion: @escaping (Result<Void, Error>) -> Void)

// equivalent to `func getResponse() async throws -> Response`
@GET("/response")
func getResponse(completion: @escaping (Response) -> Void)

Testing

Because APIs defined with Papyrus are protocols, they're simple to mock in tests; just implement the protocol.

If you use Path<T>, Header<T>, Field<T>, or Body<T> types, you don't need to include them in your protocol conformance. They are just typealiases used to hint Papyrus how to use the parameter.

@API
protocol GitHub {
    @GET("/users/:username/repos")
    func getRepositories(username: String) async throws -> [Repository]
}

struct GitHubMock: GitHub {
    func getRepositories(username: String) async throws -> [Repository] {
        return [
            Repository(name: "papyrus"),
            Repository(name: "alchemy"),
            Repository(name: "fusion"),
        ]
    }
}

You can then use your mock during tests when the protocol is required.

func testCounting() {
    let mock: GitHub = GitHubMock()
    let service = MyService(github: mock)
    let count = service.countRepositories(of: "joshuawright11")
    XCTAssertEqual(count, 3)
}

@Mock

For convenience, you can leverage macros to automatically generated mocks using @Mock. Like @API, this generates an implementation of your protocol.

The generated Mock type has mock functions to easily verify request parameters and mock responses.

@API  // Generates `GitHubAPI: GitHub`
@Mock // Generates `GitHubMock: GitHub`
protocol GitHub {
    @GET("/users/:username/repos")
    func getRepositories(username: String) async throws -> [Repository]
}

func testCounting() {
    let mock = GitHubMock()
    mock.mockGetRepositories { username in
        XCTAssertEqual(username, "joshuawright11")
        return [
            Repository(name: "papyrus"),
            Repository(name: "alchemy")
        ]
    }

    let service = MyService(github: mock)
    let count = service.countRepositories(of: "joshuawright11")
    XCTAssertEqual(count, 2)
}

Contribution

๐Ÿ‘‹ Thanks for checking out Papyrus!

If you'd like to contribute please file an issue, open a pull request or start a discussion.

Acknowledgements

Papyrus was heavily inspired by Retrofit.

License

Papyrus is released under an MIT license. See License.md for more information.

papyrus's People

Contributors

f0x1d avatar jayrhynas avatar joshuawright11 avatar kevin-kp avatar lutes1 avatar noahkamara avatar the-braveknight avatar tomrudnick avatar wouter01 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

papyrus's Issues

Request Retry

There should be a type similar to Interceptor that allows custom logic to be run after a failed request (such as retry, refresh auth token, etc).

Some useful default implementations (retry, retryAfterDelay, etc) should be available out of the box.

Support for default values in Request

Hi, first of all, thanks for writing such a nice macros.

There was one thing that I found disappointing.
The Swift syntax does not allow you to set default values for parameters in protocol functions.
If it's a normal protocol function, you can set the default value of the function parameter through an extension, but since it's a macro, it doesn't seem possible.

I don't know much about macros, but here's what I can think of.

@API
protocol Users {
  @GET("/users")
  func getList(size: Int = 20, cursor: String?) async throws -> [User]

  @POST("/user")
  func createUser(email: String, password: String, nickname: String? = nil) async throws
}

If it is possible to have an implementation that violates Swift syntax like the one above with just a macros, then the above seems to be the most convenient and reasonable.

@API
protocol Users {
  @GET("/users")
  func getList(@Default(20) size: Int, cursor: String?) async throws -> [User]

  @POST("/user")
  func createUser(email: String, password: String, @Default(nil) nickname: String?) async throws
}

Looking at the comments in the code and the post at, it seems that this is probably not possible at the moment, as there is no macros specification. If a spec is added in the future, this would be fine.

In addition to this, if there is a way to do this in the current implementation without a new one, I'd to hear about it!

Error when trying to set path parameter in front of static query

When trying to set a path parameter that sits in front of a static query like bar/:baz?query=1 papyrus fails to find the path parameter:

PapyrusError(
  message: "Tried to set path parameter `baz` but did not find `:baz` in path `bar/:baz?query=1`.", 
  request: nil, 
  response: nil
)

Test Case:

func testPathWithStaticQuery() {
        var req = RequestBuilder(baseURL: "foo/", method: "GET", path: "bar/:baz?query=1")
        req.addParameter("baz", value: "value")
        
        XCTAssertNoThrow(try req.fullURL())
        XCTAssertEqual(try req.fullURL().absoluteString, "foo/bar/value/?query=1")
    }

I'd be willing to work on this, since i need it for a project

Failed to resolve dependencies

Hi I have a problem with the latest version

Dependencies could not be resolved because package 'papyrus' is required using a stable-version but 'papyrus' depends on an unstable-version package 'swift-syntax' and 'core' depends on 'papyrus' 0.5.0..

Request Progress

There's not an easy way to tap into download progress, a useful feature for large downloads or uploads.

Multipart Form Data

Alamofire has built in support for this so the bulk of the work will be locking in how to define each "part" (with custom headers, name, and filename) as a function parameter.

Feature: Ability to decode error response

I'd love to be able to decode an error response, but from what I can see the only way to do this is to opt-out of any built-in response decoding and handle the Response myself entirely.

CI

Will need Xcode 15 support via GitHub actions & code coverage is broken in the current beta anyways.

Callback-based API

Some folks haven't upgraded to async yet so an option for a callback based API could be a useful addition.

The @API macro would just check to see if the function signature is async throws or if the last argument is a closure.

Allow returning `Data` as a response type

I tried writing "Data" as return type, but I got the response is not JSON as error. Is there any way to add a custom decoder or any other better way to do a binary file download request?

@JSON encoder argument

@POST("user/getDetails")
@JSON(decoder: JSONDecoder())
func getUserInfo() async throws -> UserInfo

which expends

func getUserInfo() async throws -> UserInfo {
    var req = builder(method: "POST", path: "/user/getDetails")
    req.requestEncoder = .json(JSONDecoder())
    req.responseDecoder = .json(JSONDecoder())
    let res = try await provider.request(req)
    return try res.decode(UserInfo.self, using: req.responseDecoder)
}

PapyrusAlamofire not working

Macro generated API struct tries to access Papyrus.Provider, but if we use PapyrusAlamofire, then we should access PapyrusAlamofire.Provider.

So, i just get:
image

Generated macro:
image

Unable to build project and apply custom JSON encoder and decoder

I've just updated to Papyrus v0.6.5 and get the following error:
image

Macro expansion:
image

Looks like because of this change:
image

BTW, i have JSON applied:

@JSON(
    encoder: Container.shared.jsonEncoder(),
    decoder: Container.shared.jsonDecoder()
)

But according to macro expansion this is not used anywhere or maybe i don't see something?

Automatically add default values for optional types

HI there!

It would be nice if Papyrus added default values when generating API implementations. Personally, I have a lot of functions that look like this:

    @POST("/sendMessage")
    func sendMessage(
        chatId: TelegramIdentifier,
        messageThreadId: Int?,
        text: String,
        parseMode: ParseMode?,
        entities: [MessageEntity]?,
        linkPreviewOptions: LinkPreviewOptions?,
        disableNotification: Bool?,
        protectContent: Bool?,
        replyParameters: ReplyParameters?,
        replyMarkup: ReplyMarkup?
    ) async throws -> TelegramResponse<Message>

Currently, I have to either:

  1. Pass nil to every single parameter, which doesn't work for the API I'm consuming (and I guess it wouldn't work for a lot of APIs either);
  2. Create several functions with the same path so I wouldn't have to use nil (which is a very horrible approach);
  3. Modify the @API macro so it adds default values for every parameter with an optional type.

I have a fork that implements the third option. However, I also had to modify the request builder so nil values aren't added to the request body, which may not be a very good idea if you have to explicitly set some value as nil.
If you have any other ideas, I'd be happy to implement them and open a PR. Maybe adding another macro, something like @AddDefaultValues?

Thanks!

Use enums with static functions instead of structs for API implementations

Since the autogenerated APIs only contain functions and due to their autogenerated nature cannot contain stored properties (they must be defined in the main body of the struct, which isn't accessible), it is better to have them be static methods in an enum to prevent unnescessary initialisation of a functionally empty struct.

Current usage. You need to initialise a struct to use the function, but the initialised struct is almost immediately deallocated from memory.

@API
protocol Test {
    func getSomething() async throws -> [MyType]
}

/*
// autogenerated struct:
struct TestAPI: Test {
	func getSomething() async throws -> [MyType] { /* Implementation */ }
}
*/

let testApi = TestAPI() // deallocated once the function/program exits
let result = try await testApi.getSomething()
// or
let result = try await TestAPI().getSomething()

Proposed usage. Since the methods are static functions, there is no unnecessary object being allocated.

@API
protocol Test {
    static func getSomething() async throws -> [MyType]
}

/*
// autogenerated static enum:
enum TestAPI: Test {
	static func getSomething() async throws -> [MyType] { /* Implementation */ }
}
*/

let result = try await MyAPI.getSomething()

Note that this would, however, be a breaking change. Developers would have to:

  1. Append a static in front of method declarations in the protocol
  2. Use the API enum directly instead of assigning an instance of the API to a variable and then calling it from the variable.

Request streaming

It will be cool and convenient if Papyrus supports requests streaming

Void return value support

Sometimes we just make API calls to report data without caring about the API response.

@POST("user/openApp")
func openApp() async throws -> Void

==>

func openApp() async throws -> Void {
    let req = builder(method: "POST", path: "user/openApp")
    let res = try await provider.request(req)
    return try res.decode(Void.self, using: req.responseDecoder)  // <- Error: Type 'Void' cannot conform to 'Decodable'

}

Request Cancellation

Right now there isn't a hook to a task to cancel in flight requests, whether async or closure based.

There needs to be some manner of doing so, likely abstracted to the backing networking library.

Multipart encoding not working

I have such api:

@API
@Mock
protocol Images {
    @Multipart
    @POST("/image/upload")
    func uploadImage(file: Part) async throws -> UploadResultResponse
}

and i use it this way

final class ImagesRepository: BaseRepository {
    
    @Injected(\.imagesApi) private var api
    
    func uploadImage(_ data: Data, mimeType: String) async throws -> String {
        let imagePart = Part(data: data, mimeType: mimeType)
        return try await api.uploadImage(file: imagePart).hash
    }
}

But when i try to run app i get crash, which says:

Can only encode `[String: Part]` with `MultipartEncoder`

I logged it out and it seems like instead of [String: Part] encoder gets [String: RequestBuilder.ContentValue]

Generalize for Server Usage with `async-http-client`

Would need to abstract the Alamofire specifics and add a separate PapyrusCore target. Then a separate library that provides a async-http-client based driver for use on server.

Investigate "providing" the protocol via that library as well.

@JSON has no effect

I have this macro:

extension JSONDecoder {
    static var testing: JSONDecoder {
        Container.shared.jsonDecoder()
    }
}
extension JSONEncoder {
    static var testing: JSONEncoder {
        Container.shared.jsonEncoder()
    }
}

@API
@Mock
@JSON(
    encoder: .testing,
    decoder: .testing
)
@KeyMapping(.snakeCase)
protocol Auth {
    @POST("/register")
    @JSON(
        encoder: .testing,
        decoder: .testing
    )
    func register(body: Body<RegisterRequestBody>) async throws -> TokensResponse
}

But in expansion i see:

struct AuthAPI: Auth {
    private let provider: PapyrusCore.Provider

    init(provider: PapyrusCore.Provider) {
        self.provider = provider
    }

    func register(body: Body<RegisterRequestBody>) async throws -> TokensResponse {
        var req = builder(method: "POST", path: "/register")
        req.requestEncoder = .json(JSONEncoder())
        req.responseDecoder = .json(JSONDecoder())
        req.setBody(body)
        let res = try await provider.request(req)
        try res.validate()
        return try res.decode(TokensResponse.self, using: req.responseDecoder)
    }

    private func builder(method: String, path: String) -> RequestBuilder {
        var req = provider.newBuilder(method: method, path: path)
        req.requestEncoder = .json(JSONEncoder())
        req.responseDecoder = .json(JSONDecoder())
        req.keyMapping = .snakeCase
        return req
    }
}

According to

req.requestEncoder = .json(JSONEncoder())
req.responseDecoder = .json(JSONDecoder())

in both places, It looks like @JSON has no effect on macro

Invalid request URL when baseURL is missing trailing `/`

Description

Initializing a Provider with a baseURL without a trailing / will produce a request with a malformed URL.

How to reproduce

@API
protocol Github {
    @GET("/users/:username/repos")
    func getRepositories(username: String) async throws -> Response
}

let provider = Provider(baseURL: "https://api.github.com")
let github = GithubAPI(provider: provider)
let response = try await github.getRepositories(username: "alchemy-swift")

The built request will have an URL with a missing /:

https://api.github.comusers/alchemy-swift/repos

Improved Organisation of APIs

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 */)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.