generated from snivilised/astrolib
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from snivilised/feat/add-tapable
feat: add prototype tapable code (#4)
- Loading branch information
Showing
9 changed files
with
581 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
# 🪅 Design Notes | ||
|
||
<!-- MD013/Line Length --> | ||
<!-- MarkDownLint-disable MD013 --> | ||
|
||
<!-- MD014/commands-show-output: Dollar signs used before commands without showing output mark down lint --> | ||
<!-- MarkDownLint-disable MD014 --> | ||
|
||
<!-- MD033/no-inline-html: Inline HTML --> | ||
<!-- MarkDownLint-disable MD033 --> | ||
|
||
<!-- MD040/fenced-code-language: Fenced code blocks should have a language specified --> | ||
<!-- MarkDownLint-disable MD040 --> | ||
|
||
<!-- MD028/no-blanks-blockquote: Blank line inside blockquote --> | ||
<!-- MarkDownLint-disable MD028 --> | ||
|
||
<!-- MD010/no-hard-tabs: Hard tabs --> | ||
<!-- MarkDownLint-disable MD010 --> | ||
|
||
## Tapable | ||
|
||
Given an interface definition: | ||
|
||
```go | ||
type ( | ||
// Activity represents an entity that performs an action that is tapable. The type | ||
// F should be a function signature, defined by the tapable, so F is not really | ||
// any, but it can't be enforced to be a func, so do not try to instantiate with | ||
// a type that is not a func. | ||
Activity[F any] interface { | ||
On(name string, handler F) | ||
} | ||
|
||
// Role is similar to Activity, but it allows the tapable entity to request | ||
// that the client specify which specific behaviour needs to be tapped. This is | ||
// useful when the entity has multi behaviours that clients may wish to tap. | ||
// To enable a client to augment the core action, Tap returns a func of type F which | ||
// represents the core behaviour. If required, the client should store this and then | ||
// invoke it as appropriate when it is called back. | ||
Role[F any, E constraints.Integer] interface { | ||
Tap(name string, role E, fn F) F | ||
} | ||
) | ||
``` | ||
|
||
a component can declare that it wants to expose a hook to enable external customisation of a behaviour. That piece of behaviour is the unit which become tab-able. So we have a Component, it's action and a Client that want to tap into this behaviour, by providing a hook. | ||
|
||
```go | ||
// Widget is some domain abstraction | ||
type Widget struct { | ||
Name string | ||
Amount int | ||
} | ||
|
||
// FuncSimpleWithWidgetAndError is the action func; this function | ||
// can be hooked/is tapable. In fact, this function can either enable core | ||
// functionality to be overridden, decorated with auxiliary behaviour or | ||
// simply be a life-cycle event; ie we have come to a significant point in | ||
// the component's workflow and the some external entity's function needs | ||
// to be invoked. | ||
type FuncSimpleWithWidgetAndError func(name string, amount int) (*Widget, error) | ||
|
||
type ActionHook struct { | ||
name string | ||
action FuncSimpleWithWidgetAndError | ||
} | ||
|
||
// Tap invoked by client to enable registration of the hook | ||
func (h *ActionHook) Tap(_ string, fn FuncSimpleWithWidgetAndError) { | ||
h.action = fn | ||
} | ||
|
||
// Component contains tapable behaviour | ||
type Component struct { | ||
hook ActionHook | ||
} | ||
|
||
func (c *Component) DoWork() { | ||
if _, err := c.hook.action("work", 0); err != nil { | ||
panic(fmt.Errorf("work failed: '%v'", err)) | ||
} | ||
} | ||
|
||
type Client struct { | ||
} | ||
|
||
func (c *Client) WithComponent(component *Component) { | ||
component.hook.Tap("client", func(name string, amount int) (*Widget, error) { | ||
widget := &Widget{ | ||
Name: name, | ||
Amount: amount, | ||
} | ||
return widget, nil | ||
}) | ||
} | ||
|
||
``` | ||
|
||
So we have 3 scenarios to consider: override core behaviour, supplement core behaviour or life cycle. | ||
|
||
### Override | ||
|
||
In extendio, we have the following hook-able actions: | ||
|
||
```go | ||
// TraverseHooks defines the suite of items that can be customised by the client | ||
type TraverseHooks struct { | ||
QueryStatus QueryStatusHookFn | ||
ReadDirectory ReadDirectoryHookFn | ||
FolderSubPath SubPathHookFn | ||
FileSubPath SubPathHookFn | ||
InitFilters FilterInitHookFn | ||
Sort SortEntriesHookFn | ||
Extend ExtendHookFn | ||
} | ||
``` | ||
|
||
These are all core functions that can be overridden by the client. Furthermore, only a single instance is invoked; ie for each one, just a single action is invoked, its neither a notification or broadcast mechanism. | ||
|
||
It may also be pertinent to use the tapable.Role, with the roles being defined as QueryStatus, ReadDirectory, FolderSubPath, ... using a new enum definition. This would serve as an indication to the client that only 1 external entity is able to tap these hook-able actions. | ||
|
||
### Supplement | ||
|
||
This is a piggy-back on top of the Override scenario, where we would like to augment the core functionality. This then makes the hook, a synchronous notification mechanism, which allows the client to control how core functionality is invoked. The client may choose to invoke custom behaviour before/after the core behaviour or completely override it altogether. The client should be able to invoke the core behaviour, which implies that the Tap should return the default func for this action. | ||
|
||
### Life Cycle |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
package tapable | ||
|
||
import ( | ||
"fmt" | ||
|
||
"golang.org/x/exp/constraints" | ||
) | ||
|
||
// tapable: enables entities to expose hooks | ||
|
||
type ( | ||
// ActivityL represents an entity that performs an action that is tapable. The type | ||
// F should be a function signature, defined by the tapable, so F is not really | ||
// any, but it can't be enforced to be a func, so do not try to instantiate with | ||
// a type that is not a func. | ||
// To enable a client to augment the core action, Tap returns a func of type F which | ||
// represents the core behaviour. If required, the client should store this and then | ||
// invoke it as appropriate when it is called back. | ||
ActivityL[F any] interface { | ||
Tap(name string, fn F) (F, error) | ||
} | ||
|
||
// RoleL is similar to Activity, but it allows the tapable entity to request | ||
// that the client specify which specific behaviour needs to be tapped. This is | ||
// useful when the entity has multiple behaviours that clients may wish to tap. | ||
RoleL[F any, E constraints.Integer] interface { | ||
Tap(name string, role E, fn F) error | ||
} | ||
|
||
// NotifyL used for life cycle events. There can be multiple subscribers to | ||
// life cycle events | ||
NotifyL[F any, E constraints.Integer] interface { | ||
On(name string, event E, handler F) error | ||
} | ||
) | ||
|
||
// we should have MonoHook (1 client allowed); | ||
// or MultiHook (multiple clients allowed) | ||
|
||
// scratch ... | ||
|
||
// type FuncSimpleWithError func() error | ||
|
||
// type SimpleHookFunc Activity[FuncSimpleWithError] | ||
|
||
// Widget is some domain abstraction | ||
type Widget struct { | ||
Name string | ||
Amount int | ||
} | ||
|
||
// FuncSimpleWithWidgetAndError is the action func; invoke this function | ||
// that can be hooked/tapable. In fact, this function can either enable core | ||
// functionality to be overridden, decorated with auxiliary behaviour or | ||
// simply as a life-cycle event; ie we have come to a significant point in | ||
// the component's workflow and the some external entity's function needs | ||
// to be invoked. | ||
type FuncSimpleWithWidgetAndError func(name string, amount int) (*Widget, error) | ||
|
||
type ActionHook struct { | ||
name string | ||
action FuncSimpleWithWidgetAndError | ||
} | ||
|
||
// Tap invoked by client to enable registration of the hook | ||
func (h *ActionHook) Tap(_ string, fn FuncSimpleWithWidgetAndError) { | ||
h.action = fn | ||
} | ||
|
||
type NotificationHook struct { | ||
name string | ||
action FuncSimpleWithWidgetAndError | ||
} | ||
|
||
// Tap invoked by client to enable registration of the hook | ||
func (h *NotificationHook) On(_ string, fn FuncSimpleWithWidgetAndError) { | ||
h.action = fn | ||
} | ||
|
||
// Component contains tapable behaviour | ||
type Component struct { | ||
hook ActionHook | ||
} | ||
|
||
func (c *Component) DoWork() { | ||
if _, err := c.hook.action("work", 0); err != nil { | ||
panic(fmt.Errorf("work failed: '%v'", err)) | ||
} | ||
} | ||
|
||
type Client struct { | ||
} | ||
|
||
func (c *Client) WithComponent(from *Component) { | ||
from.hook.Tap("client", func(name string, amount int) (*Widget, error) { | ||
widget := &Widget{ | ||
Name: name, | ||
Amount: amount, | ||
} | ||
return widget, nil | ||
}) | ||
} | ||
|
||
// Receiver contains tapable behaviour received as a notification | ||
type Receiver struct { | ||
hook NotificationHook | ||
} | ||
|
||
func (r *Receiver) When(from *Component) { | ||
from.hook.Tap("client", func(name string, amount int) (*Widget, error) { | ||
widget := &Widget{ | ||
Name: name, | ||
Amount: amount, | ||
} | ||
return widget, nil | ||
}) | ||
} | ||
|
||
// The above is still confused. Let's start again we have these scenarios: | ||
// | ||
// --> internal (during bootstrap): ==> actually, this is di, not tap | ||
// * 1 component needs to custom another | ||
// | ||
// --> external (via options): | ||
// * notification of life-cycle events (broadcast) | [On/Notify] (eg, OnBegin/On(enums.cycle.begin)) | ||
// * customise core behaviour, by role (targeted) | [Tap/Role] (eg, ReadDirectory, role=directory-reader) | ||
// * | ||
|
||
// Since we now have finer grain control; ie there are more but smaller packages | ||
// organised as features, each feature can expose its own set of hooks. Having | ||
// said this, I can still only think of nav as needing to expose hooks, but others | ||
// may emerge. | ||
// | ||
// For a component, we have the following situations | ||
// - broadcast, multiple callbacks | ||
// - targeted, single callback | ||
// | ||
// - may expose multiple hooks with different signatures | ||
// the problem this poses is that we can't have a collection of different | ||
// items. This means we need to define a hook container struct that contains the hooks. | ||
// The component aggregates this hook container with a member called hooks. | ||
// For example, in extendio, the options contains a hooks struct TraverseHooks: | ||
// | ||
// type TraverseHooks struct { | ||
// QueryStatus QueryStatusHookFn | ||
// ReadDirectory ReadDirectoryHookFn | ||
// FolderSubPath SubPathHookFn | ||
// FileSubPath SubPathHookFn | ||
// InitFilters FilterInitHookFn | ||
// Sort SortEntriesHookFn | ||
// Extend ExtendHookFn | ||
// } | ||
// | ||
// But we need to ba able to tap these, | ||
// | ||
// if hooks is of type TraverseHooks, in object Component | ||
// component.hooks.ReadDirectory.tap("name", hookFn) | ||
// therefore ReadDirectoryHookFn, can't be the function, there must be a | ||
// level of indirection in-between, | ||
// | ||
// in TraverseHooks, ReadDirectory must be of type ReadDirectoryHook, | ||
// which is an instantiation of a generic type: | ||
// HookFunc[F any], where HookFunc contains the Tap function | ||
// | ||
// type ReadDirectoryHook tapable.HookFunc[ReadDirectoryHookFn] | ||
// | ||
// type TraverseHooks struct { | ||
// ReadDirectory ReadDirectoryHook | ||
// } |
Oops, something went wrong.