{"id":20713830,"url":"https://github.com/diffplug/atplug","last_synced_at":"2025-04-23T08:08:08.416Z","repository":{"id":43766225,"uuid":"70096030","full_name":"diffplug/atplug","owner":"diffplug","description":"AtPlug: Sockets and Plugs without the boilerplate","archived":false,"fork":false,"pushed_at":"2025-04-11T16:48:12.000Z","size":1381,"stargazers_count":8,"open_issues_count":11,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-23T08:08:00.920Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/diffplug.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-10-05T20:18:58.000Z","updated_at":"2025-03-14T18:18:12.000Z","dependencies_parsed_at":"2024-01-26T03:31:08.302Z","dependency_job_id":"27693397-b8cd-43ce-b4c3-72ca754c345b","html_url":"https://github.com/diffplug/atplug","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diffplug%2Fatplug","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diffplug%2Fatplug/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diffplug%2Fatplug/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diffplug%2Fatplug/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/diffplug","download_url":"https://codeload.github.com/diffplug/atplug/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250395288,"owners_count":21423400,"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":[],"created_at":"2024-11-17T02:27:31.510Z","updated_at":"2025-04-23T08:08:08.393Z","avatar_url":"https://github.com/diffplug.png","language":"Kotlin","readme":"# \u003cimg align=\"left\" src=\"_images/logo_128.png\"\u003e AtPlug: Sockets and Plugs without boilerplate\n\n[![Gradle Plugin Portal](https://img.shields.io/gradle-plugin-portal/v/com.diffplug.atplug?color=blue\u0026label=gradle%20plugin%20portal)](https://plugins.gradle.org/plugin/com.diffplug.atplug)\n[![Maven central](https://img.shields.io/maven-central/v/com.diffplug.atplug/atplug-runtime?color=blue\u0026label=com.diffplug.atplug%3Aatplug-runtime)](https://search.maven.org/artifact/com.diffplug.atplug/atplug-runtime)\n[![Changelog](https://img.shields.io/badge/changelog-here-blue)](CHANGELOG.md)\n[![Apache 2.0](https://img.shields.io/badge/license-apache--2.0-blue.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0))\n\n## AtPlug is...\n\n- a plugin system for the JVM\n  - written in pure Kotlin, might port to Kotlin Multiplatform [someday](https://github.com/diffplug/atplug/issues/1).\n- that generates all plugin metadata for you\n  - write Java/Kotlin/Scala code, *never* write error-prone metadata manually\n- lets you filter the available plugins based on their metadata\n  - defer classloading to the last possible instant\n- easy mocking for unit tests\n\nAtPlug has three components:\n\n- a small runtime `com.diffplug.atplug:atplug-runtime`\n- a buildtime step which generates plugin metadata\n  - Gradle plugin: [`com.diffplug.atplug`](https://plugins.gradle.org/plugin/com.diffplug.atplug)\n  - Contributions welcome for maven, etc.\n- a harness for mocking in tests `com.diffplug.atplug:atplug-test-harness`\n  - built-in support for JUnit5, PRs for other test frameworks welcome\n\nIt is in production usage at [DiffPlug](https://www.diffplug.com).\n\n## How it works\n\nLet's say you're building a drawing application, and you want a plugin system to allow users to contribute different shapes. The socket interface might look something like this:\n\n```kotlin\ninterface Shape {\n  fun draw(g: Graphics)\n}\n```\n\nLet's say our system has 100 different `Shape` plugins.  Loading all 100 plugins will take a long time, so we'd like to describe which shapes are available without having to actually load it.\n\nWe can accomplish this in AtPlug by adding a method to the socket interface marked with `@Metadata`.  The annotation is a documentation hint that this method should return a constant value which will be used to generate static metadata about the plugin.\n\n```kotlin\ninterface Shape {\n  @Metadata fun name(): String\n  @Metadata fun previewSvgIcon(): String\n  fun draw(g: Graphics)\n}\n```\n\nThe AtPlug runtime stores metadata about a plugin in a `Map\u003cString, String\u003e` which gets saved into a metadata file.  This is the mechanism which allows us to inspect all the `Shape` plugins in the system without loading their classes.\n\nTo take advantage of this, we need to an object `Shape.Socket : SocketOwner` which will take a `Shape` instance and return a `Map\u003cString, String\u003e`.  This will be used during the build step to generate AtPlug metadata files.\n\n```kotlin\ninterface Shape {\n  object Socket : SocketOwner.SingletonById\u003cShape\u003e(Shape::class.java) {\n    const val KEY_SVG_ICON = \"svgIcon\"\n    override fun metadata(plug: Shape) = mapOf(\n            Pair(KEY_ID, plug.name()),\n            Pair(KEY_SVG_ICON, plug.previewSvgIcon()))\n  }\n}\n```\n\nNow your users can declare an instance of `Shape` and annotate it with `@Plug(Shape.class)`.\n\n```kotlin\n@Plug(Shape::class)\nclass Circle : Shape {\n  override fun name() = \"Circle\"\n  override fun previewSvgIcon() = \"icons/circle.svg\"\n  override fun draw(g: Graphics) = g.drawCircle()\n}\n```\n\nNow when you run `./gradlew jar`, you will have a resource file called `ATPLUG-INF/com.package.Circle.json` with content like this:\n\n```json\n{ \"implementation\": \"com.package.Circle\",\n  \"provides\": \"com.api.Shape\",\n  \"properties\": {\n    \"id\": \"Circle\",\n    \"svgIcon\": \"icons/circle.svg\"\n  }\n}\n```\n\nAnd the manifest of the Jar file will have a field `AtPlug-Component` which points to all the json files in the `ATPLUG-INF` directory. You never have to edit these files, but there's no magic. The `metadata` function which you wrote for the socket generates all the json files.\n\nTo use the plugin system, you can do:\n\n```kotlin\nShape.Socket.availableIds(): List\u003cString\u003e\nShape.Socket.descriptorForId(id: String): PlugDescriptor?\nShape.Socket.singletonForId(id: String): Shape?\n```\n\nWhich are all public methods of `SocketOwner.SingletonById`. You can add more methods too for your usecase.\n\n### (Id vs Descriptor) and (Singleton vs Ephemeral)\n\nThe `Socket` is responsible for:\n\n- generating metadata (at buildtime)\n- maintaining the runtime registry of available plugins\n- instantiating the actual objects from their metadata\n\nWhen it comes to the registry of available plugins, there are two obvious design points:\n\n- declare some String which functions as a unique id =\u003e `Id`\n- parse the `Map\u003cString, String\u003e` into a descriptor class, and run filters against the set of parsed descriptors to get all the plugins which apply to a given situation =\u003e `Descriptor`.\n\nWhen it comes to instantiating the actual objects from their metadata, there are again two obvious designs:\n\n- Once a plugin is instantiated, cache it forever and return the same instance each time =\u003e `Singleton`\n- Call the plugin constructor each time it is instantiated, so that you may end up with multiple instances of a single plugin, and unused instances can be garbage collected =\u003e `Ephemeral`\n\nIn most cases, if a plugin has a unique id, then it also makes sense to treat that plugin as a global singleton =\u003e `SocketOwner.SingletonById`. Likewise, if plugins do not have unique ids, then their concept of identity probably doesn't matter so there's no need to cache them as singletons =\u003e `SocketOwner.EphemeralByDescriptor`.\n\nThose two classes, `SingletonById` and `EphemeralByDescriptor`, are the only two options we provide out of the box - we did not fill the full 2x2 matrix (no `SingletonByDescriptor` or `EphemeralById`) because we have not found a need anywhere in our codebase for the other cases. You are free to implement `SocketOwner` yourself from scratch if you want a different design point.\n\nThe public methods of `SingletonById` are just above this section. `EphemeralByDescriptor` doesn't have any public methods, only protected methods which you can use to build an API appropriate to your case.\n\n```kotlin\nabstract class EphemeralByDescriptor\u003cT, ParsedDescriptor\u003e {\n  protected abstract fun parse(plugDescriptor: PlugDescriptor): ParsedDescriptor\n  protected fun \u003cR\u003e computeAgainstDescriptors(compute: Function\u003cSet\u003cParsedDescriptor\u003e, R\u003e) : R\n  protected fun \u003cR\u003e forEachDescriptor(forEach: Consumer\u003cParsedDescriptor\u003e)\n  protected fun descriptorsFor(predicate: Predicate\u003cParsedDescriptor\u003e): List\u003cParsedDescriptor\u003e\n  protected fun instantiateFor(predicate: Predicate\u003cParsedDescriptor\u003e): List\u003cT\u003e\n  protected fun instantiateFirst(predicateDescriptor: Predicate\u003cParsedDescriptor\u003e, order: Comparator\u003cParsedDescriptor\u003e, predicateInstance: Predicate\u003cT\u003e): T?\n}\n```\n\n### Working from Java\n\nThe examples above are Kotlin, but you can also use Java. To declare the socket, just have a field `static final SocketOwner socket`, as shown below:\n\n```java\npublic interface Shape {\n  public static final SocketOwner.Id\u003cShape\u003e socket = new SocketOwner.SingletonId\u003cShape\u003e(Shape.class) {\n    @Override\n    public Map\u003cString, String\u003e metadata(Shape plug) {\n      Map\u003cString, String\u003e map = new HashMap\u003c\u003e();\n      map.put(KEY_ID, plug.name());\n      return map;\n    }\n  };\n}\n```\n\nSockets don't have to be interfaces - abstract classes or even concrete classes would work fine too.\n\n### OSGi compatibility\n\nThis project used to be called \"AutOSGi\", and rather than generating `.json` it generated metadata compatible with OSGi Declarative Services. We found that OSGi caused more trouble than it was worth, and ended up removing it. However, it would be pretty easy to add it back in, see the [`graveyard/osgi` tag](https://github.com/diffplug/atplug/releases/tag/graveyard%2Fosgi) to get back to the OSGi version. Happy to merge a PR which optionally puts this functionality back in.\n\n## Requirements\n\nJava 8+.\n\n## Acknowledgements\n\n* Maintained by [DiffPlug](http://www.diffplug.com/).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiffplug%2Fatplug","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiffplug%2Fatplug","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiffplug%2Fatplug/lists"}