GithubHelp home page GithubHelp logo

intuit / hooks Goto Github PK

View Code? Open in Web Editor NEW
37.0 8.0 6.0 826 KB

Hooks is a little module for plugins, in Kotlin

Home Page: https://intuit.github.io/hooks/

License: MIT License

Kotlin 100.00%
plugin-framework plugins kotlin-compiler-plugin gradle-plugin maven-plugin hooks

hooks's Introduction

Hooks Logo

Hooks is a little module for plugins, in Kotlin


Auto Release CircleCI Maven Central Gradle Plugin Portal GitHub top language KtLint All Contributors

Hooks represent "pluggable" points in a software model. They provide a mechanism for tapping into such points to get updates, or apply additional functionality to some typed object. Included in the hooks library are:

  • A variety of hooks to support different plugin behavior: Basic, Waterfall, Bail, Loop
  • Asynchronous support built on Kotlin coroutines
  • Support for additional hook context and interceptors

Along with the base library, we created a Kotlin symbol processor to enable hooks to be created with a simple typed-based DSL, limiting the redundancy and overhead required to subclass a hook.

Visit our site for information about how to use hooks.

Inspiration

At Intuit, we're big fans of tapable. We use it in some of our core systems to enable teams to augment and extend our frameworks to solve their customer problems. Since our backend systems are primarily JVM-based, we really missed tapable when working in service code. Hooks is our implementation of tapable as a library for the JVM plus an Arrow Meta Compiler Plugin to make it easier to use.

Structure

  • hooks - The actual implementation of the hooks
  • processor - A Kotlin Symbol Processor that generates hook subclasses for you
  • gradle-plugin - A Gradle plugin to make using the processor easier
  • maven-plugin - A Maven Kotlin plugin extension to make using the processor easier
  • example-library - A library that exposes extension points for consumers using the hooks' call function
  • example-application - The Application that demonstrates extending a library by calling the hooks' tap function

🍻 Contributing 🍻

Feel free to make an issue or open a pull request if you have an improvement and new plugin to propose!

Make sure to read our code of conduct.

🔨 Start Developing 🔨

To get set up, fork and clone the project.

Build

Build and verify all checks:

./gradlew build

Publish locally to use in other projects:

./gradlew publishToMavenLocal

Test

Recompile changes and run all tests:

./gradlew test

Run example app

./gradlew run

Cleaning

./gradlew clean

Linting

Linting is done with ktlint and configured using JLLeitschuh's ktlint Gradle plugin.

Format code according to linting standards:

./gradlew ktlintFormat

Verify code meets linting standards:

./gradlew ktlintCheck

API Validation

To ensure that binary compatibility is maintained across non-breaking releases, the public API is validated using the Kotlin binary compatibility validator tool.

Update the API dumps:

./gradlew apiDump

Verify the public API matches the API dumps:

./gradlew apiCheck

Documentation

The docs site is built using the Orchid tool and takes inspiration from the stikt.io docs site.

Run the docs locally:

./gradlew orchidServe

The knit tool is also used to generate tests driven from markdown snippets to ensure documentation is maintained and up-to-date.

Update all generated markdown tests:

./gradlew knit

Verify the generated tests match the latest markdown changes:

./gradlew knitCheck

Versioning

This project follows the semantic versioning strategy and uses Auto to automate releases on CI. PRs must be labeled with an appropriate Auto label to denote what type of release should occur when merged. With the binary compatibility validator tool, we can follow this set of rules to determine release types:

  • Red on an API diff is breaking and requires a major bump
  • Green on an API diff is a new feature and requires a minor bump
  • No API diff, but has a code change requires a patch bump
  • Else, apply the corresponding label for documentation or build, etc.

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Jeremiah Zucker

⚠️ 💻 📖 🚇

David Stone

📖 ⚠️ 💻

Andrew Lisowski

📖 🚇 ⚠️ 💻

Kelly Harrop

🎨

brocollie08

⚠️ 💻

This project follows the all-contributors specification. Contributions of any kind welcome!

License

FOSSA Status

This product includes software developed by the Apache Software Foundation (http://www.apache.org/).

hooks's People

Contributors

brocollie08 avatar hipstersmoothie avatar stabbylambda avatar sugarmanz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hooks's Issues

Functional Hook APIs

Is your feature request related to a problem? Please describe.

When tapping a hook that provides an abstract data type, we may only care about a specific implementation. A simple early return allows us to "filter" out types we don't care about:

interface Super

class A : Super
class B : Super

val hook: SyncHook<(Super) -> Unit>
hook.tap(name) { super ->
    if (super !is A) return@tap

    // ...
}

Describe the solution you'd like

It'd be awesome to have functional methods for hooks, such that we can patternize filtering, data manipulation, etc.

hook.filterIsInstance<A>().tap(name) { a ->
    // ...
}

Describe alternatives you've considered

Tapping can effectively be thought of as collecting. Maybe it's worth adopting similar terminology? Probably not. Maybe a conversion layer to Flows would solve better than trying to coerce hooks as a Flow-like data type.

Additional context

This is somewhat difficult to consider for the more complex hook types, since return value is more important. We'd want to ensure we have appropriate handling for each hook type, or not support certain hook types at all.

Bug with HooksDsl

Possible bug

gradle assemble produces the error stack below with the hooks plugin

To Reproduce

in build.gradle

plugins {
    id("com.intuit.hooks")
}

running assemble task with above plugin, no additional code that can be used for hooks generation with DSL

> Task :player:compileReleaseKotlin FAILED
e: java.lang.IllegalStateException: Unrecognized modifier: fun
        at arrow.meta.internal.kastree.ast.psi.Converter$convertModifiers$1.invoke(Converter.kt:468)
        at arrow.meta.internal.kastree.ast.psi.Converter$convertModifiers$1.invoke(Converter.kt:453)
        at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:210)
        at kotlin.sequences.FilteringSequence$iterator$1.calcNext(Sequences.kt:170)
        at kotlin.sequences.FilteringSequence$iterator$1.hasNext(Sequences.kt:194)
        at kotlin.sequences.SequencesKt___SequencesKt.toCollection(_Sequences.kt:786)
        at kotlin.sequences.SequencesKt___SequencesKt.toMutableList(_Sequences.kt:816)
        at kotlin.sequences.SequencesKt___SequencesKt.toList(_Sequences.kt:807)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertModifiers(Converter.kt:472)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertModifiers(Converter.kt:451)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertStructured(Converter.kt:608)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertDecl(Converter.kt:291)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertClassBody(Converter.kt:805)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertStructured(Converter.kt:627)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertDecl(Converter.kt:291)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertFile(Converter.kt:393)
        at arrow.meta.internal.kastree.ast.psi.Converter.convertFile$default(Converter.kt:388)
        at arrow.meta.quotes.MetaExtensionsKt$classDeclaration$$inlined$typedQuote$1$1.invoke(TypedQuote.kt:196)
        at arrow.meta.quotes.MetaExtensionsKt$classDeclaration$$inlined$typedQuote$1$1.invoke(TypedQuote.kt:89)
        at arrow.meta.dsl.analysis.AnalysisSyntax$analysis$1.doAnalysis(AnalysisSyntax.kt:66)
        at arrow.meta.internal.registry.InternalRegistry$registerAnalysisHandler$1$1.doAnalysis(InternalRegistry.kt:535)
        at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:120)
        at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:86)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler$analyze$1.invoke(KotlinToJVMBytecodeCompiler.kt:252)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler$analyze$1.invoke(KotlinToJVMBytecodeCompiler.kt:243)
        at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:113)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:243)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.repeatAnalysisIfNeeded(KotlinToJVMBytecodeCompiler.kt:213)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:90)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli$default(KotlinToJVMBytecodeCompiler.kt:56)
        at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:169)
        at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:52)
        at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:92)
        at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:44)
        at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:98)
        at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:412)
        at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:112)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileIncrementally(IncrementalCompilerRunner.kt:358)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileIncrementally$default(IncrementalCompilerRunner.kt:300)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl$rebuild(IncrementalCompilerRunner.kt:119)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:170)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:81)
        at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:607)
        at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:96)
        at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1658)
        at jdk.internal.reflect.GeneratedMethodAccessor108.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:359)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:691)
        at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
        at java.base/java.lang.Thread.run(Thread.java:832)

Environment information:

gradle version 6.7.1
AndroidStudio Arctic Fox 2020.3.1

[Enhancement] Add better resolution for re-generated classes

Currently, we have this not-so-great mechanism for handling re-generated classes.. just delete the generated files before running the compiler plugin again. This is poor, but especially for larger projects who may be spending a not insignificant portion of time generating classes. I did some investigation again and it seems as though the Arrow Proofs compiler plugin might be solving this issue and we could potentially use that as an example. Essentially, it seems as though they plug into more steps of the compiler to better inform the compiler of what the plugin is doing.

ProofsPlugin.kt
Where I think it may be handling better resolution

But, I could be off on that and this may require some additional information.

Waterfall hooks to accept receiver as return type

Is your feature request related to a problem? Please describe.

Currently, you'll get a validation error for the following:

@SyncWaterfall<A.(b: B) -> A>
val hook: Hook

Waterfall hooks must specify the same types for the first parameter and the return type

Describe the solution you'd like

Receivers are just syntactic sugar for first parameters, so it'd be cool to accept the receiver as the accumulator type for a waterfall hook.

Imperative Hook APIs

Been brainstorming a bit about imperative hook APIs:

var someController: SataController? = null
container.hooks.someController.tap("some controller") { sc -> 
    someController = sc
}
// ...
someController?.doSomething()

There is a little bit of overhead here for simply tracking the current value in a variable to use at a later point. It’s not terrible, but it get’s pretty bad pretty quick if you need something more nested:

var update: Update? = null
container.hooks.someController.tap("some controller") { sc ->
    sc?.hooks?.nestedController?.tap("nested controller") { nc ->
        nc?.hooks?.onUpdate?.tap("update") { _update ->
            update = _update
        }
    }
}

With Kotlin, we can do something called property delegation, essentially some syntactic sugar for consolidating logic around custom getters and setters. The idea, is that we can do something like:

val someController by container.hooks.someController()

Powered by something like this under the hood:

operator fun <T1> Hook1<T1, Unit>.invoke() = Capture(this)

class Capture<T1>(hook: Hook1<T1, Unit>) {

    private var current: T1? = null

    init {
        hook.tap("capture") { _, value: T1 ->
            current = value
        }
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T1 = current 
        ?: throw IllegalStateException("hook not currently active")
}

Relatively slim extension for capturing a hooks latest value. However, it falls short when we look at the nested hooks case. Because accessing the value of the captured hook is stateful, we cannot just add another capture:

val someController by container.hooks.someController()
val nestedController by someController.hooks.nestedController()

I thought a bit about how much effort it would take to delegate instance methods to the Capture instance itself, but I think that requires too much overhead in how hook instances are defined (would require an interface to delegate to, or some more code gen for duplicating and forwarding instance APIs) and wouldn’t really be user friendly, even if the end API would be somewhat nice:

val nested by container.hooks.someController().hooks.nestedController()

Instead, I’m currently looking into a callback model, much like how taps exist now, but would still enable the delegation API:

val nested by container.hooks.someController { hooks.nestedController }

And then for additional levels:

val update by container.hooks.someController { hooks.nestedController }.capture { hooks.onUpdate }

Thoughts? Really open to anything, as I’ve got some concerns about how this could be a dangerous pitfall for those who don’t understand hooks.. at least with tapping, it’s very clear that you’re responding when you need to. I’m not even sure how many valid use cases there are for imperatively introspecting the latest value of the hook outside the tap. Especially when you take into account different types of hooks, and how many of them are really pipeline/event based, rather than stateful sync hooks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.