Super lightweight web framework in Swift based on SWSGI

Overview

Ambassador

Build Status CocoaPods Code Climate Issue Count GitHub license

Super lightweight web framework in Swift based on SWSGI

Features

  • Super lightweight
  • Easy to use, designed for UI automatic testing API mocking
  • Based on SWSGI, can run with HTTP server other than Embassy
  • Response handlers designed as middlewares, easy to composite
  • Async friendly

Example

Here's an example how to mock API endpoints with Ambassador and Embassy as the HTTP server.

import Embassy
import EnvoyAmbassador

let loop = try! SelectorEventLoop(selector: try! KqueueSelector())
let router = Router()
let server = DefaultHTTPServer(eventLoop: loop, port: 8080, app: router.app)

router["/api/v2/users"] = DelayResponse(JSONResponse(handler: { _ -> Any in
    return [
        ["id": "01", "name": "john"],
        ["id": "02", "name": "tom"]
    ]
}))

// Start HTTP server to listen on the port
try! server.start()

// Run event loop
loop.runForever()

Then you can visit http://[::1]:8080/api/v2/users in the browser, or use HTTP client to GET the URL and see

[
  {
    "id" : "01",
    "name" : "john"
  },
  {
    "id" : "02",
    "name" : "tom"
  }
]

Router

Router allows you to map different path to different WebApp. Like what you saw in the previous example, to route path /api/v2/users to our response handler, you simply set the desired path with the WebApp as the value

let router = Router()
router["/api/v2/users"] = DelayResponse(JSONResponse(handler: { _ -> Any in
    return [
        ["id": "01", "name": "john"],
        ["id": "02", "name": "tom"]
    ]
}))

and pass router.app as the SWSGI interface to the HTTP server. When the visit path is not found, router.notFoundResponse will be used, it simply returns 404. You can overwrite the notFoundResponse to customize the not found behavior.

You can also map URL with regular expression. For example, you can write this

let router = Router()
router["/api/v2/users/([0-9]+)"] = DelayResponse(JSONResponse(handler: { environ -> Any in
    let captures = environ["ambassador.router_captures"] as! [String]
    return ["id": captures[0], "name": "john"]
}))

Then all requests with URL matching /api/v2/users/([0-9]+) regular expression will be routed here. For all match groups, they will be passed into environment with key ambassador.router_captures as an array of string.

DataResponse

DataResponse is a helper for sending back data. For example, say if you want to make an endpoint to return status code 500, you can do

router["/api/v2/return-error"] = DataResponse(statusCode: 500, statusMessage: "server error")

Status is 200 OK, and content type is application/octet-stream by default, they all can be overwritten via init parameters. You can also provide custom headers and a handler for returning the data. For example:

router["/api/v2/xml"] = DataResponse(
    statusCode: 201,
    statusMessage: "created",
    contentType: "application/xml",
    headers: [("X-Foo-Bar", "My header")]
) { environ -> Data in
    return Data("<xml>who uses xml nowadays?</xml>".utf8)
}

If you prefer to send body back in async manner, you can also use another init that comes with extra sendData function as parameter

router["/api/v2/xml"] = DataResponse(
    statusCode: 201,
    statusMessage: "created",
    contentType: "application/xml",
    headers: [("X-Foo-Bar", "My header")]
) { (environ, sendData) in
    sendData(Data("<xml>who uses xml nowadays?</xml>".utf8))
}

Please notice, unlike sendBody for SWSGI, sendData only expects to be called once with the whole chunk of data.

JSONResponse

Almost identical to DataResponse, except it takes Any instead of bytes and dump the object as JSON format and response it for you. For example:

router["/api/v2/users"] = JSONResponse() { _ -> Any in
    return [
        ["id": "01", "name": "john"],
        ["id": "02", "name": "tom"]
    ]
}

DelayResponse

DelayResponse is a decorator response that delays given response for a while. In real-world, there will always be network latency, to simulte the latency, DelayResponse is very helpful. To delay a response, just do

router["/api/v2/users"] = DelayResponse(JSONResponse(handler: { _ -> Any in
    return [
        ["id": "01", "name": "john"],
        ["id": "02", "name": "tom"]
    ]
}))

By default, it delays the response randomly. You can modify it by passing delay parameter. Like, say if you want to delay it for 10 seconds, then here you do

router["/api/v2/users"] = DelayResponse(JSONResponse(handler: { _ -> Any in
    return [
        ["id": "01", "name": "john"],
        ["id": "02", "name": "tom"]
    ]
}), delay: .delay(10))

The available delay options are

  • .random(min: TimeInterval, max: TimeInterval) delay random, it's also the default one as .random(min: 0.1, max: 3)
  • .delay(seconds: TimeInterval) delay specific period of time
  • .never delay forever, the response will never be returned
  • .none no delay, i.e. the response will be returned immediately

DataReader

To read POST body or any other HTTP body from the request, you need to use swsgi.input function provided in the environ parameter of SWSGI. For example, you can do it like this

router["/api/v2/users"] = JSONResponse() { environ -> Any in
    let input = environ["swsgi.input"] as! SWSGIInput
    input { data in
        // handle the data stream here
    }
}

It's not too hard to do so, however, the data comes in as stream, like

  • "first chunk"
  • "second chunk"
  • ....
  • "" (empty data array indicates EOF)

In most cases, you won't like to handle the data stream manually. To wait all data received and process them at once, you can use DataReader. For instance

router["/api/v2/users"] = JSONResponse() { environ -> Any in
    let input = environ["swsgi.input"] as! SWSGIInput
    DataReader.read(input) { data in
        // handle the whole data here
    }
}

JSONReader

Like DataReader, besides reading the whole chunk of data, JSONReader also parses it as JSON format. Herer's how you do

router["/api/v2/users"] = JSONResponse() { environ -> Any in
    let input = environ["swsgi.input"] as! SWSGIInput
    JSONReader.read(input) { json in
        // handle the json object here
    }
}

URLParametersReader

URLParametersReader waits all data to be received and parses them all at once as URL encoding parameters, like foo=bar&eggs=spam. The parameters will be passed as an array key value pairs as (String, String).

router["/api/v2/users"] = JSONResponse() { environ -> Any in
    let input = environ["swsgi.input"] as! SWSGIInput
    URLParametersReader.read(input) { params in
        // handle the params object here
    }
}

You can also use URLParametersReader.parseURLParameters to parse the URL encoded parameter string if you want. Just do it like

let params = URLParametersReader.parseURLParameters("foo=bar&eggs=spam")

Install

CocoaPods

To install with CocoaPod, add Embassy to your Podfile:

pod 'EnvoyAmbassador', '~> 4.0'

Carthage

To install with Carthage, add Ambassador to your Cartfile:

github "envoy/Ambassador" ~> 4.0

Please notice that you should import Ambassador instead of EnvoyAmbassador. We use EnvoyAmbassador for Cocoapods simply because the name Ambassador was already taken.

Package Manager

To do this, add the repo to Package.swift, like this:

import PackageDescription

let package = Package(
    name: "AmbassadorExample",
    dependencies: [
        .package(url: "https://github.com/envoy/Ambassador.git",
                 from: "4.0.0"),
    ]
)
Comments
  • Hanging redirects

    Hanging redirects

    I noticed that stubbing requests with redirect responses makes server to "hang" with these stubs making it impossible to override this stub with another response. That does not happen for simple responses though. I was able to override it only after I deleted UI tests target from simulator.

    Here is how I stub request with redirect (the reset of the code is taken from the blog article):

    let response = DataResponse(statusCode: 301, statusMessage: "Moved Permanently", headers: [("Location", "http://www.example.org/")])
    router[DefaultRouter.fetchUsersPath] = DelayResponse(response)
    

    Even if I comment out these lines and run the test again I keep receiving redirect response.

    opened by ilyapuchka 6
  • Response body gets cut off

    Response body gets cut off

    When trying to get all the data from response body with DataReader it works fine when receiving small amount of data, but when trying to get a bigger body response, it doesn’t return the whole data, but only a part of it. I could fix it by increasing recvChunkSize to the size bigger that I was trying to read. But seems like the problem is in chunking itself, but I wasn’t able to debug it more deeply.

    Has anyone encountered this kind of problem?

    opened by bohdankoshyrets 5
  • App Lockup

    App Lockup

    Using the exact example code, after running the server..the app freezes, but the server is still accessible. Any ideas as to what may be going wrong? If I comment out loop.runForever() it doesn't happen..but the server doesn't work. using Xcode 9 on iOS 11 Swift 3.

    import Embassy
    import EnvoyAmbassador
    
    let loop = try! SelectorEventLoop(selector: try! KqueueSelector())
    let router = Router()
    let server = DefaultHTTPServer(eventLoop: loop, port: 8080, app: router.app)
    
    router["/api/v2/users"] = DelayResponse(JSONResponse(handler: { _ -> Any in
        return [
            ["id": "01", "name": "john"],
            ["id": "02", "name": "tom"]
        ]
    }))
    
    // Start HTTP server to listen on the port
    try! server.start()
    
    // Run event loop
    loop.runForever()```
    opened by skram 4
  • Swift 5 migration

    Swift 5 migration

    It seems there wasn't much to do to migrate this to Swift 5, but since this is the first time I propose a PR to a foreign repo I am not sure whether I perhaps forgot something.

    Besides the normal project file updates I would also propose this to increase the version of the pod to 4.1 as I am not sure it is fully backwards compatible. Thus I changed that in the podspec (together with specifying the swift version there instead of a .swift-version file).

    opened by GeroHerkenrath 3
  • Double sendBody in DataResponse

    Double sendBody in DataResponse

    Just curious, is this supposed to call sendBody with an empty Data? I don't see any comments around it why it would do that.

    https://github.com/envoy/Ambassador/blob/master/Ambassador/Responses/DataResponse.swift#L81

                if !data.isEmpty {
                    sendBody(data)
                }
    -->         sendBody(Data())
    
    opened by andyvanosdale 3
  • Reading request data

    Reading request data

    As readme says to read request data we can use DataRead.read(input) { data in ... } in request handler. This does not work for me as reading block is called asynchronously after request handler returns, so I can not use it to build data to send back. If I try to wait for this block without exiting handler connections is getting stuck.

    What I want to accomplish is to redirect all requests that are not stubbed to real server (because it's just insane to stub every request in the flow when you need to test one particular case on the screen that is deep inside navigation and app needs to make dozen of requests before reaching it). Here is my code:

    struct RedirectResponse: WebApp {
        
        // where to redirect
        let host: String
        let scheme: String
        
        func urlRequest(_ environ: [String: Any]) -> URLRequest {
            //reading environment variables and transforming header keys to their initial values....
            return request
        }
        
        public func app(_ environ: [String : Any], startResponse: @escaping ((String, [(String, String)]) -> Swift.Void), sendBody: @escaping ((Data) -> Swift.Void)) {
            var request = urlRequest(environ)
            let input = environ["swsgi.input"] as! SWSGIInput
            DataReader.read(input) { data in
           // this block is called only after method returns, which is too late as request was already fired.
           // Waiting for this block to be called before firing request keeps connection stuck and block is never called
                request.httpBody = data 
            }
            // makes request and wait for response in synchronous manner, using `group.wait()`
            let (data, statusCode, contentType, headers) = URLSession.shared.data(with: request)
            //send data from real server back
            DataResponse(statusCode: statusCode, statusMessage: "", contentType: contentType, headers: headers, handler: { (_, sendData) in
                sendData(data)
            }).app(environ, startResponse: startResponse, sendBody: sendBody)
        }
        
    }
    

    Then I set this response as notFoundResponse for the router.

    With that I receive such logs:

    2017-02-03 13:03:58 +0000 [info] - DefaultHTTPServer: New connection 87A6CC41-5E67-4585-944D-C409FDC9D16A from [::1]:58322
    2017-02-03 13:03:58 +0000 [info] - HTTPConnection: [87A6CC41-5E67-4585-944D-C409FDC9D16A] Header parsed, method=Optional("POST"), path="/auth/oauth2/client/access_token", version="HTTP/1.1", headers=[("Host", "localhost:8080"), ("Accept-Encoding", "gzip;q=1.0, compress;q=0.5"), ("mobile-app", "IOS-838"), ("Accept-Language", "en-US;q=1.0, en;q=0.9"), ("Content-Length", "177"), ("Content-Type", "application/json"), ("Accept", "*/*"), ("Connection", "keep-alive"), ("User-Agent", "Alamofire/4.2.0")]
    2017-02-03 13:03:58 +0000 [info] - HTTPConnection: [87A6CC41-5E67-4585-944D-C409FDC9D16A] Resume reading
    2017-02-03 13:03:58 +0000 [info] - HTTPConnection: [87A6CC41-5E67-4585-944D-C409FDC9D16A] Finish response
    2017-02-03 13:03:58 +0000 [info] - HTTPConnection: [87A6CC41-5E67-4585-944D-C409FDC9D16A] Connection closed, reason=byLocal
    

    Request fails as it does not contain required body data.

    When trying to wait for read data block:

    2017-02-03 13:01:35 +0000 [info] - DefaultHTTPServer: New connection 6DB9C3DF-02E3-49F5-8801-22EB0CE58AF5 from [::1]:58286
    2017-02-03 13:01:35 +0000 [info] - HTTPConnection: [6DB9C3DF-02E3-49F5-8801-22EB0CE58AF5] Header parsed, method=Optional("POST"), path="/auth/oauth2/client/access_token", version="HTTP/1.1", headers=[("Host", "localhost:8080"), ("Accept-Encoding", "gzip;q=1.0, compress;q=0.5"), ("mobile-app", "IOS-838"), ("Accept-Language", "en-US;q=1.0, en;q=0.9"), ("Content-Length", "177"), ("Content-Type", "application/json"), ("Accept", "*/*"), ("Connection", "keep-alive"), ("User-Agent", "Alamofire/4.2.0")]
    2017-02-03 13:01:35 +0000 [info] - HTTPConnection: [6DB9C3DF-02E3-49F5-8801-22EB0CE58AF5] Resume reading
    

    and then request fails with timeout.

    Here is the request that app makes:

    curl -i \
    	-X POST \
    	-H "mobile-app: IOS-838" \
    	-H "Content-Type: application/json" \
    	-H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \
    	-H "Accept-Language: en-US;q=1.0, en;q=0.9" \
    	-H "User-Agent: Alamofire/4.2.0" \
    	-d "{\"country\":\"us\",\"locale\":\"en-US\",\"grant_type\":\"client_credentials\",\"client_id\":\"...\",\"client_secret\":\"...\",\"scope\":\"public\"}" \
    	"http://localhost:8080/auth/oauth2/client/access_token"
    
    opened by ilyapuchka 3
  • Get query string from a request

    Get query string from a request

    I'm trying to get the query string arguments from an URL similar to: /v2/users?direction=past&limit=20. Is there a way to get those parameters and their values inside a router to form response based on those parameters? I did try to use URLParametersReader, but it returns an empty object every time.

    opened by bohdankoshyrets 2
  • Update podSpec to specify tvOS support

    Update podSpec to specify tvOS support

    Resolve validation error on pod repo push or install for tvOS apps.

    • ERROR | [tvOS] unknown: Encountered an unknown error (The platform of the target App (tvOS 11.0) is not compatible with EnvoyAmbassador (4.0.3), which does not support tvos.) during validation.
    opened by Joe-Spectrum 2
  • Possible to serve JSON files?

    Possible to serve JSON files?

    Ambassador is a great project - I've been using it for testing and I love it.

    Is it possible to serve JSON directly from a file, similar to how json-server works?

    opened by damianesteban 2
  • Swift PM and Linux Support

    Swift PM and Linux Support

    Hi,

    I'd like to participate in this project by adding support for Swift Package Manager and some fixes for Linux os.

    Please let me know when i missed something.

    Thanks

    opened by cpageler93 1
  • Update cocoapods version

    Update cocoapods version

    Aloha Envoy team!

    The cocoapods version is still a tiny bit behind, it doesn't include @fangpenlin's PR #28. Since my project uses cocoapods I still get a warning during builds (that's the only one, so kinda prominent).

    I had actually already forked the repo and wanted to fix the deprecated call to characters, I was really confused when master already had the latest Swift syntax. 😸

    Maybe one of you can quickly do a tag, version bump etc. and push to the cocoapods spec repo?

    Thanks a lot, your Embassy & Ambassador have really helped me a lot in testing (and since it's all swift I can keep my repo nice and clean!).

    opened by GeroHerkenrath 1
  • Fatal error with xcode 14 with Bad file descriptor

    Fatal error with xcode 14 with Bad file descriptor

    Hi all, after upgrading to xcode 14 we are getting a error on both M1 rossetta and intel:

    Embassy/SelectorEventLoop.swift:88: Fatal error: 'try!' expression unexpectedly raised an error: Embassy.OSError.ioError(number: 9, message: "Bad file descriptor")

    This happens after trying to restart (teardown-setup) a local server image

    testing with multiple iOS versions, testing with other xcode versions later.

    Anyone else run into this?

    opened by D3icidal 0
  • Providing path to json file instead of content of the json.

    Providing path to json file instead of content of the json.

    Instead of providing the JSON format structure after return, is there a way to point Ambassador to the .json file located in the project of the app we want to test?

    instead of

    router["/api/v2/users"] = JSONResponse() { _ -> Any in
        return [
            ["id": "01", "name": "john"],
            ["id": "02", "name": "tom"]
        ]
    }
    

    something like:

    router["/api/v2/users"] = JSONResponse() { _ -> Any in
        return [
           path/to/file/users.json
        ]
    }
    
    opened by bartzet 0
  • Retrieving request Headers through Router

    Retrieving request Headers through Router

    Is it possible to get request headers using Ambassador? I can't seem to find them in the environ variable alongside the other information.

    I expected to find custom headers or most common ones inside a environ field.

    opened by DevDema 0
  • Delay response doesnt work as expected

    Delay response doesnt work as expected

    Hello guys,

    I noticed that when I use delay response in ui test, and in this response I return some value to assert in UI test, delayed response sometimes come after I made assertion.

    So, first I mock response with delay like this router["/api/v2/users"] = DelayResponse(JSONResponse(......)), and after I assert value I got from response file, or assert ui element that I enabled in this mocked response. So this kind of tests are flaky, because depending on delay time, it gives back response after assertion.

    Deleting delay response solves this flakiness. But why it happens with DelayResponse?

    opened by ZhanatM 0
  • Support many states api responses

    Support many states api responses

    Hello.

    Is there any way to support many states API responses such as:

    • Call Login success will be responded:
    router["/api/v2/login"] = DelayResponse(JSONStringResponse(handler: { _ -> Any in
        return """
            {
              "acess_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
            }
        """
    }))
    
    • Call login failed will be responded:
    router["/api/v2/login"] = DelayResponse(JSONStringResponse(handler: { _ -> Any in
        return """
            {
              "message": "incorrect password"
            }
        """
    }))
    

    I mean I want to test a feature (login) with many cases as many API responses. Seem to now only one state can setup before a test.

    Thank you so much.

    opened by nvduc2910 0
Releases(v4.0.5)
  • v4.0.5(Oct 10, 2018)

    In the previous release we had v4.0.4 tag on wrong commit, as a result the version number in Podspec file was wrong. This release just bumps the version and make a new release.

    Source code(tar.gz)
    Source code(zip)
  • v4.0.4(Sep 17, 2018)

  • v4.0.3(Sep 14, 2018)

  • v4.0.2(Jun 21, 2018)

    Remove calls to deprecated property

    https://github.com/envoy/Ambassador/pull/28

    Thanks @JanNash for contribution.

    Also thank @GeroHerkenrath for remind us about this new release

    https://github.com/envoy/Ambassador/issues/34

    Source code(tar.gz)
    Source code(zip)
Owner
Envoy
Transforming modern workplaces and challenging the status quo with innovations that make office life and work more meaningful
Envoy
A native, lightweight and secure time-based (TOTP) & counter-based (HOTP) password client built for iOS

A native, lightweight and secure time-based (TOTP) & counter-based (HOTP) password client built for iOS Built by Tijme Gommers – Buy me a coffee via P

Raivo OTP 770 Jan 8, 2023
Lightweight library for web server applications in Swift on macOS and Linux powered by coroutines.

Why Zewo? • Support • Community • Contributing Zewo Zewo is a lightweight library for web applications in Swift. What sets Zewo apart? Zewo is not a w

Zewo 1.9k Dec 22, 2022
A lightweight library for writing HTTP web servers with Swift

Taylor Disclaimer: Not actively working on it anymore. You can check out some alternatives Swift 2.0 required. Working with Xcode 7.1. Disclaimer: It

Jorge Izquierdo 925 Nov 17, 2022
💧 A server-side Swift web framework.

Vapor is a web framework for Swift. It provides a beautifully expressive and easy to use foundation for your next website, API, or cloud project. Take

Vapor 22.4k Jan 2, 2023
A Swift web framework and HTTP server.

A Swift Web Framework and HTTP Server Summary Kitura is a web framework and web server that is created for web services written in Swift. For more inf

Kitura 7.6k Jan 6, 2023
A Ruby on Rails inspired Web Framework for Swift that runs on Linux and OS X

IMPORTANT! We don't see any way how to make web development as great as Ruby on Rails or Django with a very static nature of current Swift. We hope th

Saulius Grigaitis 2k Dec 5, 2022
A minimal, fast and unopinionated web framework for Swift

![Fire Image] (http://i.imgur.com/1qR6Nl4.png) Blackfire An extremely fast Swift web framework ?? Getting Started If you're familiar with express.js t

Elliott Minns 908 Dec 2, 2022
A Swift Multiplatform Single-threaded Non-blocking Web and Networking Framework

Serverside non-blocking IO in Swift Ask questions in our Slack channel! Lightning (formerly Edge) Node Lightning is an HTTP Server and TCP Client/Serv

SkyLab 316 Oct 6, 2022
libuv base Swift web HTTP server framework

Notice Trevi now open a Trevi Community. Yoseob/Trevi project split up into respective Trevi, lime, middlewares and sys packages at our community. If

leeyoseob 46 Jan 29, 2022
Minimal web framework and middleware for Swift

Kunugi Kunugi(椚) is minimal web framework and middleware systems for Swift. This is inpired by Node.js' Koa. Kunugi doesn't provide http server its se

Yusuke Ito 35 Apr 18, 2022
RestKit is a framework for consuming and modeling RESTful web resources on iOS and OS X

RestKit RestKit is a modern Objective-C framework for implementing RESTful web services clients on iOS and Mac OS X. It provides a powerful object map

The RestKit Project 10.2k Dec 29, 2022
A super fast & convenient object mapper tailored for your needs

A super fast & convenient object mapper tailored for your needs. Mapping objects to arrays or dictionaries can be a really cumbersome task, but those

Christoffer Winterkvist 246 Sep 9, 2022
A lightweight but powerful network library with simplified and expressive syntax based on AFNetworking.

XMNetworking English Document XMNetworking 是一个轻量的、简单易用但功能强大的网络库,基于 AFNetworking 3.0+ 封装。 其中,XM 前缀是我们团队 Xcode-Men 的缩写。 简介 如上图所示,XMNetworking 采用中心化的设计思想

KANGZUBIN 981 Dec 29, 2022
Swift Express is a simple, yet unopinionated web application server written in Swift

Documentation <h5 align="right"><a href="http://demo.swiftexpress.io/">Live ?? server running Demo <img src="https://cdn0.iconfinder.com/data/icons/

Crossroad Labs 850 Dec 2, 2022
Lightweight, flexible HTTP server framework written in Swift

Hummingbird Lightweight, flexible server framework written in Swift. Hummingbird consists of three main components, the core HTTP server, a minimal we

Hummingbird 245 Dec 30, 2022
Robust Swift networking for web APIs

Conduit Conduit is a session-based Swift HTTP networking and auth library. Within each session, requests are sent through a serial pipeline before bei

Mindbody 52 Oct 26, 2022
Frank is a DSL for quickly writing web applications in Swift

Frank Frank is a DSL for quickly writing web applications in Swift with type-safe path routing. Sources/main.swift import Frank // Handle GET request

Kyle Fuller Archive 377 Jun 29, 2022
High Performance (nearly)100% Swift Web server supporting dynamic content.

Dynamo - Dynamic Swift Web Server Starting this project the intention was to code the simplest possible Web Server entirely in Swift. Unfortunately I

John Holdsworth 68 Jul 25, 2022
A web API client in Swift built using Async/Await

Get A modern web API client in Swift built using Async/Await and Actors. let cli

Alexander Grebenyuk 745 Jan 3, 2023