{"id":19824732,"url":"https://github.com/combinecommunity/feedbacks","last_synced_at":"2025-07-23T20:05:42.575Z","repository":{"id":49531364,"uuid":"323186873","full_name":"CombineCommunity/Feedbacks","owner":"CombineCommunity","description":"Feedbacks is a tool to build feedback loops within a Swift based application. Feedbacks relies on Combine and is compatible with SwiftUI and UIKit","archived":false,"fork":false,"pushed_at":"2021-06-15T20:46:29.000Z","size":1855,"stargazers_count":50,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-25T13:55:31.389Z","etag":null,"topics":["dsl","feedback-loop","feedbacks","state-machine","swift"],"latest_commit_sha":null,"homepage":"","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/CombineCommunity.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-12-20T23:40:32.000Z","updated_at":"2025-01-01T12:07:55.000Z","dependencies_parsed_at":"2022-09-05T11:40:48.158Z","dependency_job_id":null,"html_url":"https://github.com/CombineCommunity/Feedbacks","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/CombineCommunity/Feedbacks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CombineCommunity%2FFeedbacks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CombineCommunity%2FFeedbacks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CombineCommunity%2FFeedbacks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CombineCommunity%2FFeedbacks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CombineCommunity","download_url":"https://codeload.github.com/CombineCommunity/Feedbacks/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CombineCommunity%2FFeedbacks/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266738795,"owners_count":23976473,"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","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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","feedback-loop","feedbacks","state-machine","swift"],"created_at":"2024-11-12T11:05:38.594Z","updated_at":"2025-07-23T20:05:42.547Z","avatar_url":"https://github.com/CombineCommunity.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"![](https://github.com/twittemb/Feedbacks/workflows/Tests/badge.svg)\n![](https://github.com/twittemb/Feedbacks/workflows/SwiftLint/badge.svg)\n[![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)\n ![platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20macOS%20%7C%20tvOS%20%7C%20watchOS-333333.svg)\n[![codecov](https://codecov.io/gh/CombineCommunity/Feedbacks/branch/main/graph/badge.svg)](https://codecov.io/gh/twittemb/Feedbacks)\n\n\u003cdiv style=\"text-align:center\"\u003e\n\u003cimg src=\"./Resources/logo_feedbacks_black.png\" height=\"300\" style=\"border-radius: 20px;\"/\u003e\n\u003c/div\u003e\n\n# About\n\n**Feedbacks is a tool to help you build reliable and composable features inside Swift applications. Each feature is based on a System, which stands for all the needed software components that communicate together to perform this feature.**\n\n**Feedbacks is entirely based on a declarative syntax that describes the behaviour of your Systems. If you like SwiftUI, you will enjoy Feedbacks as its syntax promotes compositional patterns and modifiers.**\n\n**A System relies on three things:**\n\n* an initial state\n* some side effects\n* a state machine\n\n# A prototype is worth a thousand pictures\n\nHere is a System that regulates the volume of a speaker based on an initial volume and a targeted volume.\n\n```swift\nstruct VolumeState: State { let value: Int }\nstruct IncreaseEvent: Event {}\nstruct DecreaseEvent: Event {}\n\nlet targetedVolume = 15\n        \nlet system = System {\n  InitialState {\n    VolumeState(value: 10)\n  }\n\t\n  Feedbacks {\n    Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -\u003e AnyPublisher\u003cEvent, Never\u003e in\n      if state.value \u003e= targetedVolume {\n        return Empty().eraseToAnyPublisher()\n      }\n\t\n      return Just(IncreaseEvent()).eraseToAnyPublisher()\n    }\n\t    \n    Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -\u003e AnyPublisher\u003cEvent, Never\u003e in\n      if state.value \u003c= targetedVolume {\n        return Empty().eraseToAnyPublisher()\n      }\n\t\n      return Just(DecreaseEvent()).eraseToAnyPublisher()\n    }\n  }\n\n  Transitions {\n    From(VolumeState.self) { state in\n      On(IncreaseEvent.self, transitionTo: VolumeState(value: state.value + 1))\n      On(DecreaseEvent.self, transitionTo: VolumeState(value: state.value - 1))\n    }\n  }\n}\n```\n\nLet's break it down.\n\n# A System is a state machine\n\n\u003e A state machine is an abstract machine that can be in exactly one of a finite number of states at any given time. The state machine can change from one state to another in response to some external events. The change from one state to another is called a transition. A state machine is defined by a list of its states, its initial state, and the conditions for each transition.\n\nA state machine can perfectly describe a feature inside an application. What is great about state machines is their predictability. From a given state and a given event, a transition will always return the same state.\n\nTo define a state machine, we need to define three things: the states, the events and the transitions. In the \"speaker volume example\", we have defined 1 state: the current volume, and 2 events: 1 for increasing the volume, and 1 for decreasing it. All you have to do is then to describe the transitions and the condition of their execution.\n\nHere is the state machine described in the aforementioned System.\n\n\u003cdiv style=\"text-align:center\"\u003e\n\u003cimg src=\"./Resources/state_machine.png\" height=\"250\" style=\"border-radius: 20px;\"/\u003e\n\u003c/div\u003e\n\nOn the one hand, defining transitions is about describing what is immutable in an application, what cannot change depending on external conditions, and what is highly predictable and testable.\n\nOn the other hand, an application often needs to access data from the network or a database, which depends on conditions that are outside the System (filesystem, data availability, ...). Those side effects can be defined inside Feedbacks.\n\n**We can sum up to this: everything that mutates a state that belongs to the System is a transition, everything that access a state outside the System is a side effect.**\n\n# The feedbacks\nA Feedback is a semantical wrapper around a side effect.\n\nIn the \"speaker volume example\", at some point we need to access an external property: the targeted volume. It is external to the System because it is something variable that can be set and stored independently from the System. Its access has to be segregated inside a side effect.\n\nA side effect is a function that reacts to an input state by producing an event that might trigger a transition. As a side effect can be asynchronous (fetching the network for instance), it should return a Publisher of events.\n\nIn our example, one feedback takes care of increasing the volume and the other is responsible for decreasing it. They are both executed everytime a new state is generated by a transition.\n\n## Scheduling\n\nThreading is very important to make a nice responsive application. A Scheduler is the Combine way of handling threading by switching portions of reactive streams on dispatch queues, operation queues or RunLoops.\n\nThe declarative syntax of Feedbacks allows to alter the behavior of a System by simply applying modifiers (like you would do with SwiftUI to change the frame for instance). Modifying the scheduling of a side effect is as simple as calling the `.execute(on:)` modifier.\n\n```swift\nFeedbacks {\n  Feedback(on: LoadingState.self, strategy: .continueOnNewState) { state -\u003e AnyPublisher\u003cEvent, Never\u003e in\n    performLongRunningOperation()\n      .map { FinishedLoadingEvent() }\n      .eraseToAnyPublisher()\n    }\n    .execute(on: DispatchQueue(label: \"A background queue\"))\n}\n```\n\nAs in SwiftUI, modifiers can be applied to the container:\n\n```swift\nFeedbacks {\n  Feedback(on: LoadingState.self, strategy: .continueOnNewState) { state -\u003e AnyPublisher\u003cEvent, Never\u003e in\n    ...\n  }\n    \n  Feedback(on: SelectedState.self, strategy: .continueOnNewState) { state -\u003e AnyPublisher\u003cEvent, Never\u003e in\n    ...\n  }\n}\n.execute(on: DispatchQueue(label: \"A background queue\"))\n```\n\nBoth side effects will be executed on the background queue.\n\nIt is also applicable to the transitions:\n\n```swift\nTransitions {\n  From(VolumeState.self) { state in\n    On(IncreaseEvent.self, transitionTo: VolumeState(value: state.value + 1))\n    On(DecreaseEvent.self, transitionTo: VolumeState(value: state.value - 1))\n  }\n}.execute(on: DispatchQueue(label: \"A background queue\"))\n```\n\n or to the whole system:\n \n ```swift\n System {\n  InitialState {\n    VolumeState(value: 10)\n  }\n\t\n  Feedbacks {\n    Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -\u003e AnyPublisher\u003cEvent, Never\u003e in\n      if state.value \u003e= targetedVolume {\n        return Empty().eraseToAnyPublisher()\n      }\n\t\n      return Just(IncreaseEvent()).eraseToAnyPublisher()\n    }\n\t    \n    Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -\u003e AnyPublisher\u003cEvent, Never\u003e in\n      if state.value \u003c= targetedVolume {\n        return Empty().eraseToAnyPublisher()\n      }\n\t\n      return Just(DecreaseEvent()).eraseToAnyPublisher()\n    }\n  }\n\n  Transitions {\n    From(VolumeState.self) { state in\n      On(IncreaseEvent.self, transitionTo: VolumeState(value: state.value + 1))\n      On(DecreaseEvent.self, transitionTo: VolumeState(value: state.value - 1))\n    }\n  }\n}.execute(on: DispatchQueue(label: \"A background queue\"))\n ```\n\n## Lifecycle\n\nThere are typical cases where a side effect consist of an asynchronous operation (like a network call). What happens if the very same side effect is called repeatedly, not waiting for the previous ones to end? Are the operations stacked? Are they cancelled when a new one is performed?\n\nWell, it depends 😁. Every feedback constructor that takes a State as a parameter can also be passed an ExecutionStrategy:\n\n* **.cancelOnNewState**, to cancel the previous operation when a new state is to be handled\n* **.continueOnNewState**, to let the previous operation naturally end when a new state is to be handled (events will then be concatenated).\n\n## Dependencies\n\nIt is unlikely that a side effect don't need dependencies to perform its job. By design, a side effect is a function that can take only a state as an input. Fortunately, Feedbacks provide factory functions to help with the injection of dependencies in your side effects.\n\n```swift\nenum MySideEffects {\n  static func load(\n      networkService: NetworkService,\n      databaseService: DataBaseService,\n      state: LoadingState\n  ) -\u003e AnyPublisher\u003cEvent, Never\u003e {\n    networkService\n      .fetch()\n      .map { databaseService.save($0) }\n      .map { LoadedEvent(result: $0) }\n      .eraseToAnyPublisher()\n  }\n}\n\nlet myNetworkService = MyNetworkService()\nlet myDatabaseService = MyDatabaseService()\nlet loadingEffect = SideEffect.make(MySideEffects.load, arg1: myNetworkService, arg2: myDatabaseService)\nlet feedback = Feedback(on: LoadingState.self, strategy: .cancelOnNewState, perform: loadingEffect)\n```\n\n`SideEffect.make()` factories will transform functions with several parameters (up to 6 including the state) into functions with 1 parameter (the state), on the condition of the state being the last one.\n\n# Let's gain some altitude\n\n**A System relies on three things:**\n\n* an initial state\n* some side effects\n* a state machine\n\nOnce these things are connected together, it forms a stream of States which we can subscribe to in order to run the System:\n\n```swift\nsystem.stream.sink { _ in }.store(\u0026subscriptions)\n\nor\n\nsystem.run() // the subscription will live as long as the system is kept in memory\n```\n\nA System forms a loop that is also referred to as a **feedback loop**, where the state is continuously adjusted until it reaches a stable value:\n\n\u003cdiv style=\"text-align:center\"\u003e\n\u003cimg src=\"./Resources/system.png\" height=\"300\" style=\"border-radius: 20px;\"/\u003e\n\u003c/div\u003e\n\n\n# Advanced usage\n## The modifiers\nHere is a list of the supported modifiers:\n\n| Modifier | Action | Can be applied to |\n| -------------- | -------------- | -------------- |\n| `.disable(disabled:)`| The target won't be executed as long as the `disabled` condition is true | \u003cul align=\"left\"\u003e\u003cli\u003eTransition\u003c/li\u003e\u003cli\u003eTransitions\u003c/li\u003e\u003cli\u003eFeedback\u003c/li\u003e\u003c/ul\u003e |\n| `.execute(on:)`| The target will be executed on the scheduler | \u003cul align=\"left\"\u003e\u003cli\u003eTransitions\u003c/li\u003e\u003cli\u003eFeedback\u003c/li\u003e\u003cli\u003eFeedbacks\u003c/li\u003e\u003cli\u003eSystem\u003c/li\u003e\u003c/ul\u003e |\n| `.onStateReceived(perform:)`| Execute the `perform` closure each time a new state is given as an input | \u003cul align=\"left\"\u003e\u003cli\u003eFeedback\u003c/li\u003e\u003cli\u003eFeedbacks\u003c/li\u003e\u003c/ul\u003e |\n| `.onEventEmitted(perform:)`| Execute the `perform` closure each time a new event is emitted | \u003cul align=\"left\"\u003e\u003cli\u003eFeedback\u003c/li\u003e\u003cli\u003eFeedbacks\u003c/li\u003e\u003c/ul\u003e |\n| `.attach(to:)`| Refer to the \"How to make systems communicate\" section | \u003cul align=\"left\"\u003e\u003cli\u003eSystem\u003c/li\u003e\u003cli\u003eUISystem\u003c/li\u003e\u003c/ul\u003e |\n| `.uiSystem(viewStateFactory:)`| Refer to the \"Using Feedbacks with SwiftUI and UIKit\" section | \u003cul align=\"left\"\u003e\u003cli\u003eSystem\u003c/li\u003e\u003c/ul\u003e |\n\nAs each modifier returns an updated instance of the target, we can chain them.\n\n```swift\nFeedback(...)\n  .execute(on: ...)\n  .onStateReceived {\n    ...\n  }\n  .onEventEmitted {\n    ...\n  }\n```\n\n## State and Event wildcards\n\nAlthough it is recommended to describe all the possible transitions in a state machine, it is still possible to take some shortcuts with wildcards.\n\n```swift\nTransitions {\n  From(ErrorState.self) {\n    On(AnyEvent.self, transitionTo: LoadingState())\n  }\n}\n```\n\nConsidering the state is ErrorState, this transition will produce a LoadingState whatever event is received.\n\n```swift\nTransitions {\n  From(AnyState.self) {\n    On(RefreshEvent.self, transitionTo: LoadingState())\n  }\n}\n```\n\nEverytime the RefreshEvent is received, this transition will produce a LoadingState whatever the previous state.\n\n## The different ways of instantiating a Feedback\n\nA Feedback is built from a side effect. A side effect is a function that takes a state as a parameter. There are two ways to build a Feedback:\n\n```swift\nFeedback(on: AnyState.self, strategy: .continueOnNewState) { state in\n  ...\n  .map { _ in MyEvent() }\n  .eraseToAnyPublisher()\n}\n```\n\nThis feedback will execute the side effect whatever the type of state that is produced. It could be useful if you want to perform a side effect each time a new state is generated, regardless of the type of State.\n\n```swift\nFeedback(on: LoadingState.self, strategy: .continueOnNewState) { state in\n  ...\n  .map { _ in MyEvent() }\n  .eraseToAnyPublisher()\n}\n```\nThis Feedback will execute the side effect only when it is of type LoadingState.\n\n## Composing Transitions\n\nThe more complex a System, the more we need to add transitions. It's a good practice to split them into logical units:\n\n```swift\nlet transitions = Transitions {\n  From(LoadingState.self) { state in\n    On(DataIsLoaded.self, transitionTo: LoadedState.self) { event in\n      LoadedState(page: state.page, data: event.data)\n    }\n    On(LoadingHasFailed.self, transitionTo: ErrorState())\n  }\n    \n  From(LoadedState.self) { state in\n    On(RefreshEvent.self, transitionTo: LoadingState.self) {\n      LoadingState(page: state.page)\n    }\n  }\n}\n```\n\nor even externalize them into properties:\n\n```swift\nlet loadingTransitions = From(LoadingState.self) { state in\n  On(DataIsLoaded.self, transitionTo: LoadedState.self) { event in\n    LoadedState(page: state.page, data: event.data)\n  }\n  On(LoadingHasFailed.self, transitionTo: ErrorState())\n}\n    \nlet loadedTransitions = From(LoadedState.self) { state in\n  On(RefreshEvent.self, transitionTo: LoadingState.self) {\n    LoadingState(page: state.page)\n  }\n}\n\nlet transitions = Transitions {\n  loadingTransitions\n  loadedTransitions\n}\n```\n\n## Unit testing you state machine\n\nIn order to ease the testing of your transitions you can import the \"FeedbacksTest\" library.\nIt provides helper functions on the \"Transitions\" type.\n\nOnce you have a system, you can retrieve its transitions: `let transitions = mySystem.transitions`:\n\n* `transitions.assertThat(from: VolumeState(value: 10), on: IncreaseEvent(), newStateIs: VolumeState(value: 11))`\n* `transitions.assertThatStateIsUnchanged(from: Loading(), on: Refresh())`\n\n\n## How to make Systems communicate?\n\nSystems should be self contained and limited to their business. We should pay attention to make them small and composable. It might occur that a feature is composed of several Systems. In that case we could want them to communicate together.\n\nThere is a pattern for that in OOP: Mediator. A Mediator acts as a communication bus between independent components in order to garantee their decoupling.\n\nFeedbacks come with two types of Mediators: `CurrentValueMediator` and `PassthroughMediator`. They are basically typealises of `CurrentValueSubject` and `PassthroughSubject`.\n\nTo attach two Systems together:\n\n```swift\nlet mediator = PassthroughMediator\u003cInt\u003e()\n\nlet systemA = System {\n  ...\n}.attach(to: mediator, onSystemState: LoadedState.self, emitMediatorValue: { _ in 1701 })\n\nlet systemB = System {\n  ...\n}.attach(to: mediator, onMediatorValue: 1701 , emitSystemEvent: { _ in LoadedDoneEvent() }))\n```\n\nWhen systemA emits a `LoadedState` state, the mediator will propagate the `1701` value among its subscribers and systemB will trigger a `LoadedDoneEvent`.\n\nThis way of doing is nice when you do not have a reference on the 2 systems at the same time. You can pass the mediator around or make sure a common instance is injected to you to make the link between your Systems.\n\nIf by chance you have a reference on both Systems, you can attach them without a mediator:\n\n```swift\nlet systemA = System {\n  ...\n}\n\nlet systemB = System {\n  ...\n}\n\nsystemA.attach(\n    to: systemB,\n    onSystemStateType: LoadedState.self,\n    emitAttachedSystemEvent: { stateFromA in\n      LoadedEvent(data: stateFromA.data)\n    }\n)\n```\n\nWhen systemA encounters the state `LoadedState`, systemB will trigger a `LoadedEvent` event.\n\n# Using Feedbacks with SwiftUI and UIKit\n\nAlthough a System can exist by itself without a view, it makes sense in our developer world to treat it as a way to produce a State that will be rendered on screen and expect events emitted by a user.\n\nFortunately, taking a State as an input for rendering and returning a stream of events from user interactions looks A LOT like the definition of a side effect; and we know how to handle them 😁 -- with a System of course. Feedbacks provides a `UISystem` class which is a decoration of a traditionnal `System`, but dedicated to UI interactions.\n\nDepending on the complexity of your use case, you can use `UISystem` in two ways:\n\n* for simple cases, you can instantiate a `UISystem` from a `System`: The resulting system will publish a `RawState`, which is a basic encapsulation of your states. You will have to write functions in your Views to extract the information you need from them. You can find an example of implementation in the [CounterApp demo application](https://github.com/CombineCommunity/Feedbacks/tree/main/Examples/Examples/CounterApp).\n* for more complex cases, you can instantiate a `UISystem` from a `System` and a `viewStateFactory` function: The resulting system will publish a `ViewState` which is the output from the `viewStateFactory` function. It allows to implement more complex mappings. You can find an example of implementation in the [GiphyApp demo application](https://github.com/CombineCommunity/Feedbacks/tree/main/Examples/Examples/GiphyApp).\n\n`UISystem` has some specifics:\n\n* it ensures the states are published on the main thread \n* as it is an ObservableObject, it publishes a `state` property we can listen to in SwiftUI views or UIKit ViewControllers\n* it offers an `emit(event:)` function to propagate user events in the System\n* it offers some helper functions to build SwiftUI Bindings\n\n```swift\n\nenum FeatureViewState: State {\n  case .displayLoading\n  case .displayData(data: Data)\n}\n\nlet stateToViewState: (State) -\u003e FeatureViewState = { state in\n  switch (state) {\n  case is LoadingState: return .displayLoading\n  case let loadedState as LoadedState: return .displayData(loadedState.data)\n  ...\n  }\n}\n\nlet system = UISystem(viewStateFactory: stateToViewState) {\n  InitialState {\n    LoadingState()\n  }\n    \n  Feedbacks {\n    ...\n  }\n    \n  Transitions {\n    ...\n  }\n}\n```\n\nAlternatively, we can build a `UISystem` from a traditionnal `System`:\n\n```swift\nlet system = System {\n  InitialState {\n    LoadingState()\n  }\n    \n  Feedbacks {\n    ...\n  }\n    \n  Transitions {\n    ...\n  }\n}\n\nlet uiSystem = system.uiSystem(viewStateFactory: stateToViewState)\n```\n\nOnce started, we can inject the `uiSystem` into a SwiftUI View:\n\n```swift\nstruct FeatureView: View {\n\n  @ObservedObject var system: UISystem\u003cFeatureViewState\u003e\n\n  var body: some View {\n    switch (self.system.state) {\n    case .displayLoading: ...\n    case let .displayData(data): ...\n    }\n  }\n\t\n  var button: some View {\n    Button {\n      Text(\"Click\")\n    } label: {\n      self.system.emit(RefreshEvent())\n    }\n  }\n}\n```\n\nor into a ViewController:\n\n```swift\nclass FeatureViewController: ViewController {\n  var subscriptions = [AnyCancellable]()\n\t\n  func viewDidLoad() {\n    self\n      .system\n      .$state\n      .sink { [weak self] state in self?.render(state) }\n      .store(in: \u0026self.subscriptions)\n  }\n\t\n  func onClick() {\n    self.system.emit(RefreshEvent())\n  }\n}\n```\n\n# Examples\n\nYou will find a demo application in the Examples folder of the project. We will add new examples as the time goes by.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcombinecommunity%2Ffeedbacks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcombinecommunity%2Ffeedbacks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcombinecommunity%2Ffeedbacks/lists"}