joshuawright11 / papyrus Goto Github PK
View Code? Open in Web Editor NEWA type-safe HTTP client for Swift.
License: MIT License
A type-safe HTTP client for Swift.
License: MIT License
When creating an api with the following:
@GET("/some/endpoint/:param")
func getSomething(param: Path<Param>) async throws -> Something
Where Param is an enum with a raw value of String
enum Param: String, Codable, CaseIterable {
case something
case somethingElse = "something-else"
}
The following error occurs when building: Ambiguous use of 'addParameter(_:value:)'
It will be cool and convenient if Papyrus supports requests streaming
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?
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.
Will need Xcode 15 support via GitHub actions & code coverage is broken in the current beta anyways.
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:
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);nil
(which is a very horrible approach);@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!
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.
Curl's are very helpful to investigate failing requests in postman
@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)
}
Initializing a Provider
with a baseURL
without a trailing /
will produce a request with a malformed URL.
@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
Xcode Cloud complains that "Target 'Papyrus' must be enabled before it can be used." and gives up. Haven't seen this issue with other Swift packages. Any fixes?
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'
}
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
For those who don't want an Alamofire dependency
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!
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]
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.
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
I've just updated to Papyrus v0.6.5
and get the following error:
Looks like because of this change:
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?
There's not an easy way to tap into download progress, a useful feature for large downloads or uploads.
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*/
}
}
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 */)
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.
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:
static
in front of method declarations in the protocolCalling
@GET("/cask/:name.json")
func cask(name: String) async throws -> Cask
with name: "iterm2"
tries to request /cask/:name.json?name=iterm2
instead of /cask/iterm2.json
, as expected.
Am I misunderstanding the parameter syntax?
Hello! I found that if you use @POST
with a Query<String>
argument the query is treated as a field instead of a query.
Here is a repro case, as well as tests for it: malonehedges@799ccbc#diff-59796aae9da7e4376b94b04d63b3a1e8525f0cae95540e20693b310ce047c80dR144-R190
I'd be happy to help out with this but I haven't done any Swift Macros yet so I'm not sure where to start just yet. Will try to take a look tonight.
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.
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.
I'm working on a Swift project that has a server side, and client side. I'd like to unify the definition of endpoints across both builds.
Something like this would be very useful:
let routesBuilder = ...
let provider = Provider(baseURL: "https://api.example.com/")
let users: Users = UsersAPI(provider: provider)
routesBuilder.addRoutes(from: users)
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..
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.