{"id":2126,"url":"https://github.com/devlucky/Kakapo","last_synced_at":"2025-08-02T23:32:02.189Z","repository":{"id":56917806,"uuid":"54993813","full_name":"devlucky/Kakapo","owner":"devlucky","description":"🐤Dynamically Mock server behaviors and responses in Swift","archived":false,"fork":false,"pushed_at":"2018-10-25T17:45:04.000Z","size":2061,"stargazers_count":759,"open_issues_count":21,"forks_count":44,"subscribers_count":19,"default_branch":"master","last_synced_at":"2024-11-18T04:22:39.768Z","etag":null,"topics":["dsl","func","json-api","kakapo","opinionated","opinionated-dsl","plugable","plugable-interface","unplugged"],"latest_commit_sha":null,"homepage":"devlucky.github.io/kakapo-swift","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devlucky.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-03-29T16:57:21.000Z","updated_at":"2024-10-17T01:36:07.000Z","dependencies_parsed_at":"2022-08-21T04:50:52.600Z","dependency_job_id":null,"html_url":"https://github.com/devlucky/Kakapo","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlucky%2FKakapo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlucky%2FKakapo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlucky%2FKakapo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlucky%2FKakapo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devlucky","download_url":"https://codeload.github.com/devlucky/Kakapo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228503126,"owners_count":17930517,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","func","json-api","kakapo","opinionated","opinionated-dsl","plugable","plugable-interface","unplugged"],"created_at":"2024-01-05T20:16:05.296Z","updated_at":"2024-12-06T17:30:44.804Z","avatar_url":"https://github.com/devlucky.png","language":"Swift","readme":"# Kakapo ![partyparrot](http://cultofthepartyparrot.com/sirocco.gif)\n[![Language: Swift](https://img.shields.io/badge/lang-Swift-yellow.svg?style=flat)](https://developer.apple.com/swift/)\n[![Build Status](https://travis-ci.org/devlucky/Kakapo.svg?branch=master)](https://travis-ci.org/devlucky/Kakapo)\n[![Version](https://img.shields.io/cocoapods/v/Kakapo.svg?style=flat)](http://cocoapods.org/pods/Kakapo)\n[![DocCov](https://img.shields.io/cocoapods/metrics/doc-percent/Kakapo.svg)](http://cocoadocs.org/docsets/Kakapo)\n[![codecov](https://codecov.io/gh/devlucky/Kakapo/branch/master/graph/badge.svg)](https://codecov.io/gh/devlucky/Kakapo)\n[![codebeat badge](https://codebeat.co/badges/69a42ece-740c-4a29-b25a-598deaf61fca)](https://codebeat.co/projects/github-com-devlucky-kakapo)\n[![License](https://img.shields.io/cocoapods/l/Kakapo.svg?style=flat)](http://cocoapods.org/pods/Kakapo)\n[![Platform](https://img.shields.io/cocoapods/p/Kakapo.svg?style=flat)](http://cocoapods.org/pods/Kakapo)\n[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n\n\u003e Dynamically Mock server behaviors and responses.\n\n## Contents\n- [Why Kakapo?](#why-kakapo)\n- [Features](#features)\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Serializable protocol](#serializable-protocol)\n  - [Router: Register and Intercept](#router---register-and-intercept)  \n    - [Third-party network libraries](#third-party-libraries)\n  - [Leverage the Store - Dynamic mocking](#leverage-the-store---dynamic-mocking)\n  - [CustomSerializable](#customserializable)\n  - [JSONAPI](#jsonapi)\n  - [Expanding Null values with Property Policy](#expanding-null-values-with-property-policy)\n  - [Key customization - Serialization Transformer](#key-customization---serialization-transformer)\n  - [Customize responses with ResponseFieldsProvider](#customize-responses-with-responsefieldsprovider)\n- [Roadmap](#roadmap)\n- [Examples](#examples)\n- [Authors](#authors)\n\nKakapo is a dynamic mocking library. It allows you to replicate your backend APIs and logic.  \nWith Kakapo you can easily prototype your application based on your API specifications.\n\n## Why Kakapo?\n\nA common approach when testing network requests is to stub them with fake network responses from local files or recorded requests. This has some disadvantages:\n\n- All files need to be updated when the APIs are updated.\n- Lots of files have to be generated and included in the project.\n- Are just static responses that can only be used for unit tests since they don't reflect backend behaviors and state.\n\nWhile still this approach may work good, Kakapo will be a game changer in your network tests: it will give you complete control when it comes to simulating backend behaviors. Moreover, is not just unit testing: you can even take a step further and prototype your application before having a real service behind!  \nWith Kakapo you can just create Swift structs/classes/enums that are automatically serialized to JSON.\n\n\n\u003e 7 billion people on Earth\n\u003e\n\u003e Less than 150 Kakapo\n\u003e\n\u003e Time is critical [donate to Kakapo recovery](http://kakaporecovery.org.nz/adopt-a-kakapo/)\n\n## Features\n\n  * Dynamic mocking\n  * Prototyping\n  * Swift 3.0 compatible (from version 2.0.0, master branch)\n  * Swift 2.2 and 2.3 compatible (from version 1.0.0, branch feature/legacy-swift)\n  * Compatible with [![Platform](https://img.shields.io/cocoapods/p/Kakapo.svg?style=flat)](http://cocoapods.org/pods/Kakapo)\n  * Protocol oriented and pluggable\n  * Fully customizable by defining custom serialization and custom responses\n  * Out-of-the-box serialization\n  * [JSONAPI](http://jsonapi.org) support\n\n## Installation\n\nUsing [CocoaPods](http://cocoapods.org/):\n\n```ruby\nuse_frameworks!\npod 'Kakapo'\n```\n\nUsing [Carthage](https://github.com/Carthage/Carthage):\n\n```\ngithub \"devlucky/Kakapo\"\n```\n\n## Usage\n\n\u003e NOTE: The project also contains a [README.playground](https://github.com/devlucky/Kakapo/tree/master/README.playground).\n\u003e Check it out to see some examples of the key features of Kakapo.\n\nKakapo is made with an easy-to-use design in mind. To quickly get started, you can create a `Router` that intercepts network requests like this:\n\n```Swift\nlet router = Router.register(\"http://www.test.com\")\nrouter.get(\"/users\") { request in\n  return [\"id\" : 2, \"name\": \"Kakapo\"]\n}\n```\n\nYou might be wondering where the dynamic part is; here is when the different modules of Kakapo take place:\n\n```Swift\nlet store = Store()\nstore.create(User.self, number: 20)\n\nrouter.get(\"/users\") { request in\n  return store.findAll(User.self)\n}\n```\n\nNow, we've created 20 random `User` objects and mocked our request to return them.  \n\nLet's get a closer look to the different features:\n\n### Serializable protocol\n\nKakapo uses the `Serializable` protocol in order to serialize objects to JSON. *Any type* can be serialized as long as it conforms to this protocol:\n\n```Swift\nstruct User: Serializable {\n  let name: String\n}\n\nlet user = User(name: \"Alex\")\nlet serializedUser = user.serialized()\n//  -\u003e [\"name\": \"Alex\"]\n```\n\nAlso, standard library types are supported: this means that `Array`, `Dictionary` or `Optional` can be serialized:\n\n```Swift\nlet serializedUserArray = [user].serialized()\n// -\u003e [[\"name\": \"Alex\"]]\nlet serializedUserDictionary = [\"test\": user].serialized()\n// -\u003e [\"test\": [\"name\": \"Alex\"]]\n```\n\n### Router - Register and Intercept\n\nKakapo uses `Router`s in order to keep track of the registered endpoints that have to be intercepted.  \nYou can match *any* relative path from the registered base URL, as long as the components are matching the request's components. You can use wildcard components:\n\n```Swift\nlet router = Router.register(\"http://www.test.com\")\n\n// Will match http://www.test.com/users/28\nrouter.get(\"/users/:id\") { ... }\n\n// Will match http://www.test.com/users/28/comments/123\nrouter.get(\"/users/:id/comments/:comment_id\") { ... }\n```\n\nThe handler will have to return a `Serializable` object that will define the response once the URL of a request is matched.\nWhen a `Router` intercepts a request, it automatically serializes the `Serializable` object returned by the handler and converts it to `Data`.\n\n```Swift\nrouter.get(\"/users/:id\") { request in\n  return [\"id\": request.components[\"id\"]!, \"name\": \"Joan\"]\n}\n```\n\nNow everything is ready to test your mocked API; you can perform your request as you usually would do:\n\n```Swift\nlet session = URLSession.shared\nlet url = URL(string: \"http://www.test.com/users/1\")!\nsession.dataTask(with: url) { (data, _, _) in\n    // handle response\n}.resume()\n```\n\n\u003e Note: query parameters are not affecting the route match\n\u003e `http://www.test.com/users/1?foo=bar` would also be matched\n\nIn the previous example the handler was returning a simple `Dictionary`; while this works because `Dictionary` is already `Serializable`, you can also create your own entities that conform to `Serializable`:\n\n```Swift\nstruct User: Serializable {\n    let firstName: String\n    let lastName: String\n    let id: String\n}\n\nrouter.get(\"/users/:id\") { request in\n  return User(firstName: \"Joan\", lastName: \"Romano\", id: request.components[\"id\"]!)\n}\n```\n\nWhen a request is matched, the RouteHandler receives a `Request` object that represents your request including components, query parameters, httpBody and httpHeaders. The `Request` object can be useful when building dynamic responses.\n\n#### Third-Party Libraries\n\nThird-Party libraries that use the Foundation networking APIs are also supported but you might need to set a proper `URLSessionConfiguration`.  \nFor example, to setup `Alamofire`:\n\n```swift\nlet configuration = URLSessionConfiguration.default\nconfiguration.protocolClasses = [Server.self]\nlet sessionManager = SessionManager(configuration: configuration)\n```\n\n### Leverage the Store - Dynamic mocking\n\nKakapo gets even more powerful when using your Routers together with the Store. You can create, insert, remove, update or find objects.\n\nThis lets you mock the APIs behaviors as if you were using a real backend. This is the **dynamic** side of Kakapo.\n\nTo create entities that can be used with the store, your types need to conform to the `Storable` protocol.\n\n```Swift\nstruct Article: Storable, Serializable {\n    let id: String\n    let text: String\n\n    init(id: String, store: Store) {\n        self.id = id\n        self.text = randomString() // you might use some faker library like Fakery!\n    }\n}\n```\n\nAn example usage could be to retrieve a specific `Article`:\n\n```Swift\nlet store = Store()\nstore.create(Article.self, number: 20)\n\nrouter.get(\"/articles/:id\") { request in\n  let articleId = request.components[\"id\"]!\n  return store.find(Article.self, id: articleId)\n}\n```\n\nOf course you can perform any logic which fits your needs:\n\n```Swift\nrouter.post(\"/article/:id\") { request in\n    return store.insert { (id) -\u003e Article in\n        return Article(id: id, text: \"text from the body\")\n    }\n}\n\nrouter.del(\"/article/:id\") { request in\n  let articleId = request.components[\"id\"]!\n  let article = store.find(Article.self, id: articleId)!\n  try! store.delete(article)\n\n  return [\"status\": \"success\"]\n}\n```\n\n### CustomSerializable\n\nIn [Serializable](#serializable-protocol) we described how your classes can be serialized. The serialization, by default, will `Mirror` (using Swift's reflection) an entity by recursively serializing its properties.\n\nWhenever a different behavior is needed, you can instead conform to `CustomSerializable` to provide your custom serialization.\n\nFor instance, `Array` uses `CustomSerializable` to return an `Array` containing its serialized elements. `Dictionary`, similarly, is serialized by creating a `Dictionary` with the same keys and serialized values.\n\nFor other examples of `CustomSerializable` and how to use it to create more complex serializations, take a look at the `JSONAPISerializer` implementation.\n\n### JSONAPI\n\nSince Kakapo was built with JSONAPI support in mind, `JSONAPISerializer` is able to serialize your entity into JSON conforming to [jsonapi.org](http://jsonapi.org).\n\nYour entities, in order to be serialized conforming to JSONAPI, need to conform to `JSONAPIEntity` protocol.  \n\nLet's see an example:\n\n```Swift\nstruct Cat: JSONAPIEntity {\n    let id: String\n    let name: String\n}\n\nstruct User: JSONAPIEntity {\n    let id: String\n    let name: String\n    let cats: [Cat]\n}\n```\n\nNote that `JSONAPIEntity` objects are already `Serializable` and you could just use them together with your Routers. However, to completely follow the JSONAPI structure in your responses, you should wrap them into a `JSONAPISerializer` struct:\n\n```Swift\nrouter.get(\"/users/:id\") { request in\n  let cats = [Cat(id: \"33\", name: \"Joan\"), Cat(id: \"44\", name: \"Hez\")]\n  let user = User(id: \"11\", name: \"Alex\", cats: cats)\n  return JSONAPISerializer(user)\n}\n```\n\n### Expanding Null values with Property Policy\n\nWhen serializing to JSON, you may want to represent a property value as `null`. For this, you can use the `PropertyPolicy` enum. It is similar to `Optional`, providing an additional `.null` case:\n\n```Swift\npublic enum PropertyPolicy\u003cWrapped\u003e: CustomSerializable {\n    case None\n    case Null\n    case Some(Wrapped)\n}\n```\n\nIt's only purpose is to be serialized in 3 different ways, to cover all possible behaviors of an Optional property.\n`PropertyPolicy` works exactly as `Optional` properties:\n- `.none` -\u003e property not included in the serialization\n- `.some(wrapped)` -\u003e serialize `wrapped`\n\nThe additional case ,`.null`, is serialized as `null` when converted to json.\n\n```Swift\nPropertyPolicy\u003cInt\u003e.none.serialized() // nil\nPropertyPolicy\u003cInt\u003e.null.serialized() // NSNull\nPropertyPolicy\u003cInt\u003e.some(1).serialized() // 1\n```\n\n### Key customization - Serialization Transformer\n\nThe keys of the JSON generated by the serialization are directly reflecting the property names of your entities. However, you might need different behaviors. For instance, many APIs use `snake_case` keys but almost everyone use `camelCase` properties in Swift.  \nTo transform the keys you can use `SerializationTransformer`. Objects conforming to this protocol are able to transform the keys of a wrapped object at serialization time.  \n\nFor a concrete implementation, check `SnakecaseTransformer`: a struct that implements `SerializationTransformer` to convert keys into snake case:\n\n```Swift\nlet user = User(userName: \"Alex\")\nlet serialized = SnakecaseTransformer(user).serialized()\nprint(serialized) // [ \"user_name\" : \"Alex\" ]\n```\n\n### Customize responses with ResponseFieldsProvider\n\nIf your responses need to specify status code (which will be 200 by default) and/or header fields, you can take advantage of `ResponseFieldsProvider` to customize your responses.\n\nKakapo provides a default `ResponseFieldsProvider` implementation in the Response struct, which you can use to wrap your Serializable objects:\n\n```Swift\nrouter.get(\"/users/:id\"){ request in\n    return Response(statusCode: 400, body: user, headerFields: [\"access_token\" : \"094850348502\"])\n}\n\nlet url = URL(string: \"http://www.test.com/users/2\")!\nsession.dataTaskWithURL() { (data, response, _) in\n    let allHeaders = response.allHeaderFields\n    let statusCode = response.statusCode\n    print(allHeaders[\"access_token\"]) // 094850348502\n    print(statusCode) // 400\n}.resume()\n```\n\nOtherwise your `Serializable` object can directly implement the protocol: take a look at `JSONAPIError` to see another example.\n\n## Roadmap\n\nEven though Kakapo is ready to use, it is not meant to be shipped to the App Store although you can also do it! In fact, you might see it in action in some Apple stores since it was used to mock some features of Runtastic's demo app; however, it's at its early stage and we would love to hear your thoughts. We encourage you to open an issue if you have any questions, feedbacks or you just want to propose new features.\n\n- Full JSON API support [#67](https://github.com/devlucky/Kakapo/issues/67)\n- Reverse and Recursive relationships [#16](https://github.com/devlucky/Kakapo/issues/16)\n- Custom Serializers for common json specifications\n\n## Examples\n\n### Newsfeed [![BuddyBuild](https://dashboard.buddybuild.com/api/statusImage?appID=57e58ce073e94e0100c34a01\u0026branch=master\u0026build=latest)](https://dashboard.buddybuild.com/apps/57e58ce073e94e0100c34a01/build/latest)\n\nMake sure you check the [demo app](https://github.com/devlucky/Kakapo/tree/master/Examples/NewsFeed) we created using Kakapo: a prototyped newsfeed app which lets the user create new posts and like/unlike them.  \nTo quickly try it use: `pod try Kakapo`\n\n![](https://raw.githubusercontent.com/devlucky/Kakapo/master/Examples/NewsFeed/newsfeed.png)\n\n## Authors\n\n[@MP0w](https://github.com/MP0w) - [@zzarcon](https://github.com/zzarcon) - [@joanromano](https://github.com/joanromano)\n","funding_links":[],"categories":["Testing","Libs","HarmonyOS","Swift"],"sub_categories":["Other Testing","Testing","Other free courses","Windows Manager"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevlucky%2FKakapo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevlucky%2FKakapo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevlucky%2FKakapo/lists"}