platform

package
v1.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2021 License: Apache-2.0 Imports: 14 Imported by: 0

README

Prudence: Extension Guide

Prudence is designed to be extensible in a few ways, detailed here.

Plugins would normally be written in Go, against the APIs in this platform package. Check out the plugin example to see how it all works together.

Though you could potentially embed Prudence in your custom Go application, the more common use would be to customize the prudence command to be bundled with your plugins. That's what the XPrudence tool is for, documented separately.

A note about versions

From Prudence 1.1.0 and onwards the Prudence platform package should maintain its contract between minor versions of Prudence. I.e. extensions written against Prudence 1.1.6 should work with Prudence 1.1.12. The latter may add more features, but should not remove or change the functionality of existing ones. In other words, if a breaking change needs to be introduced to this package then the minor version of Prudence would be bumped. Thus extensions written against Prudence 1.2.0 would not be guaranteed to work with Prudence 1.1.x.

This discipline will not be maintained for 1.0.x versions. In that early stage we will be doing more frequent API changes as the platform moves towards maturity and stability.

Plugins

Prudence plugins are Go modules, so they must have a go.mod file. However, note that your module name does not have to be URL-based if you don't need to publish it online (the XPrudence tool lets you simply specify a --directory for a plugin). In our included example we indeed get away with calling it simply "myplugin", which we initialized like so:

go mod init myplugin

Your module's Go code will likely, at the very least, import this package, "github.com/tliron/prudence/platform". And it would also have at least one "init()" function to register your extensions. See below for details.

Otherwise, there are no special requirements. You can, for example, use any package name you want and import anything else. In our example we will call our package "plugin".

JavaScript APIs

Prudence's JavaScript engine, goja, has excellent integration with Go code, converting values and functions to and from JavaScript for you. This means that in many cases you can just hand over normal Go code and not worry about JavaScript specificities. That said, you can receive and create goja types directly for deeper integration, including support for constructor functions (JavaScript's "new" keyword). See the discussion at Runtime.ToValue for more information.

Let's create a plugin that exposes the BadgerDB API to JavaScript:

package plugin

import (
    badger "github.com/dgraph-io/badger/v3"
    "github.com/tliron/prudence/platform"
)

func init() {
    platform.RegisterAPI("badger", API{})
}

func (self API) Open(path string) (*badger.DB, error) {
    return badger.Open(badger.DefaultOptions(path))
}

That's really all there is to it! "badger.open" will return a database instance and all its methods and types should work fine in JavaScript, including sophisticated things like passing JavaScript functions to Go code:

const db = badger.open(prudence.joinFilePath(__dirname, 'db'));

exports.counter = function(context) {
    var counter;

    db.update(function(txn) {
        try {
            txn.get('counter').value(function(value) {
                counter = parseInt(prudence.bytesToString(value));
                return null;
            });
        } catch (e) {
            counter = 0;
        }

        txn.set('counter', prudence.stringToBytes(counter + 1));
        return null;
    });

    return counter;
};

Custom Types

Prudence has built-in types like "Server", "Router", "Static", "MemoryCache", etc., and you can add your own. To do this, you need to register a "create" function:

import "github.com/tliron/kutil/js"

func init() {
    platform.RegisterType("MyType", CreateMyType)
}

type MyType struct{}

// platform.CreateFunc signature
func CreateMyType(config map[string]interface{}, context *js.Context) (interface{}, error) {
    return MyType{}
}

The "config" argument contains the arbitrary data provided in JavaScript's "new". If not provided it will be an empty map (not a "nil" value). The "context" argument provides access to the JavaScript runtime environment in which the object is being created. This is useful especially for calling "context.Resolve", which will let you process relative URLs in the "config".

Like the JavaScript APIs discussed above, your custom types can really do anything you want them to do. However, you're likely going to be interacting with the Prudence platform in the following ways.

Handlers

If your type implements the "rest.Handler" interface then it can be used as a handler anywhere in Prudence, just like "Router", "Resource", and "Static". Example:

import "github.com/tliron/prudence/rest"

type MyType struct{
    message string
}

// rest.Handler interface
func (self MyType) Handle(context *rest.Context) bool {
    context.WriteString(self.message + "\n")
    return true
}
Startables

If your type implements the "platform.Startable" interface then it can be used as an argument for "prudence.start". This is a simple interface that just has "Start" and "Stop" methods. The only built-in startable in Prudence is "Server".

Note that "prudence.start" expects your "Start" implementation to be blocking. It will run it in a goroutine for you. That means that you likely should not be create another goroutine in "Start". Example:

type MyType struct{
    stop chan bool
}

// platform.Startable interface
func (self MyType) Start() error {
    <-self.stop // block until a value is sent
    return nil
}

// platform.Startable interface
func (self MyType) Stop() error {
    stop <- true // send a value (and unblock "Start")
    return nil
}
Cache Backends

If your type implements the "platform.CacheBackend" interface then it can be used as an argument for "prudence.setCacheBackend".

Note that only the "LoadRepresentation" method is expected to be synchronous, meaning that it must return a "CachedRepresentation" if it exists in the cache. The other methods can (and perhaps should) be asynchronous, meaning that they can return quickly and do the actual work in the background. Example using an imaginary database:

// platform.CacheBackend interface
func (self MyType) LoadRepresentation(key platform.CacheKey) (*platform.CachedRepresentation, bool) {
    if value, ok := db.Get("rep:" + key); ok {
        return unpackCachedRepresentaiton(value), true
    } else {
        return nil, false
    }
}

// platform.CacheBackend interface
func (self MyType) StoreRepresentation(key platform.CacheKey, cached *platform.CachedRepresentation) {
    go func() {
        db.Set("rep:" + key, packCachedRepresentation(cached))
        for _, name := range cached.Groups {
            db.AddToList("grp:" + name, key)
        }
    }()
}

// platform.CacheBackend interface
func (self MyType) DeleteRepresentation(key platform.CacheKey) {
    go func() {
        db.Delete("rep:" + key)
    }()
}

// platform.CacheBackend interface
func (self MyType) DeleteGroup(name platform.CacheKey) {
    go func() {
        if list, ok := db.GetList("grp:" + name); ok {
            for _, key := range list {
                db.Delete("rep:" + key)
            }
            db.Delete("grp: " + name)
        }
    }()
}

JST Sugar

If the built-in JavaScript Template sugar is not sweet enough for you then you can add your own.

Your custom tag is registered on a prefix, which is a string that will be checked against what immediately follows the <% opening delimiter. Note that not only must it be unique so that it won't overlap with other tags, but also that it should be unambiguous. Thus you shouldn't register both the the - and the -> prefixes because the former is included in the latter.

Your tag implementation has two arguments, a "JSTContext" and the raw text between the two JST delimiters (which includes your prefix). Your implementation can do anything, but what it most likely will do is write JavaScript source code into the context. Remember that this source code is eventually integrated in-place into the JST, which is in the end one big "present" hook function.

The returned value is usually "false", which means that Prudence will swallow the trailing newline character just after the tag's end delimiter. This is what we want with most tags, as it avoids filling your output with empty lines. However, you can return "true" to disable this, which is what the "expression" sugar, <%=, does. Also note that the user can explicitly disable this effect by putting a / just before the end delimiter: /%>.

Example:

func init() {
    platform.RegisterTag("~", EncodeInBed)
}

// platform.HandleTagFunc signature
func EncodeInBed(context *platform.JSTContext, code string) bool {
    code = code[1:]
    context.WriteLiteral(strings.TrimSpace(code) + " in bed")
    return false
}

And then using it in JST:

<div>
    <%~ I like to watch TV %>
</div>

Renderers

The Prudence renderer API is quite straightforward: it accepts text as input and returns text as output. What the renderer actually does, of course, can be quite sophisticated. It could be an entire language implementation. Here's a trivial example:

import "github.com/tliron/kutil/js"

func init() {
    platform.RegisterRenderer("doublespace", RenderDoubleSpace)
}

// platform.RenderFunc signature
func RenderDoubleSpace(content string, context *js.Context) (string, error) {
    return strings.ReplaceAll(context, " ", "  "), nil
}

Note that the JavaScript context is provided as an argument. This is to allow sophisticated renderers to integrate with the resolver, module, etc.

Documentation

Index

Constants

View Source
const (
	EncodingTypeUnsupported = EncodingType(-1)
	EncodingTypeIdentity    = EncodingType(0)
	EncodingTypeBrotli      = EncodingType(1)
	EncodingTypeGZip        = EncodingType(2)
	EncodingTypeFlate       = EncodingType(3)
)

Variables

View Source
var NCSAFilename string

Functions

func AsConfigList

func AsConfigList(value ard.Value) ard.List

func AsStringList

func AsStringList(value ard.Value) []string

func Create

func Create(config ard.StringMap, context *js.Context) (interface{}, error)

func DecodeBrotli added in v1.0.2

func DecodeBrotli(bytes_ []byte, writer io.Writer) error

func DecodeFlate added in v1.0.2

func DecodeFlate(bytes_ []byte, writer io.Writer) error

func DecodeGZip added in v1.0.2

func DecodeGZip(bytes_ []byte, writer io.Writer) error

func EncodeBrotli added in v1.0.2

func EncodeBrotli(bytes []byte, writer io.Writer) error

func EncodeFlate added in v1.0.2

func EncodeFlate(bytes []byte, writer io.Writer) error

func EncodeGZip added in v1.0.2

func EncodeGZip(bytes []byte, writer io.Writer) error

func OnAPIs

func OnAPIs(f func(name string, api interface{}) bool)

func OnTags

func OnTags(f func(prefix string, handle HandleTagFunc) bool)

func OnTypes

func OnTypes(f func(type_ string, create CreateFunc) bool)

func RegisterAPI

func RegisterAPI(name string, api interface{})

func RegisterRenderer

func RegisterRenderer(renderer string, render RenderFunc)

func RegisterTag

func RegisterTag(prefix string, handle HandleTagFunc)

func RegisterType

func RegisterType(type_ string, create CreateFunc)

func Render

func Render(content string, renderer string, context *js.Context) (string, error)

func SetCacheBackend

func SetCacheBackend(cacheBackend_ CacheBackend)

func Start

func Start(startables []Startable) error

func Stop

func Stop()

func ToStringList

func ToStringList(list ard.List) []string

Types

type CacheBackend

type CacheBackend interface {
	LoadRepresentation(key CacheKey) (*CachedRepresentation, bool)  // sync
	StoreRepresentation(key CacheKey, cached *CachedRepresentation) // async
	DeleteRepresentation(key CacheKey)                              // async
	DeleteGroup(name CacheKey)                                      // async
}

func GetCacheBackend

func GetCacheBackend() CacheBackend

type CacheKey

type CacheKey string

type CachedRepresentation

type CachedRepresentation struct {
	Groups     []CacheKey
	Headers    map[string][]string
	Body       map[EncodingType][]byte
	Expiration time.Time
}

func (*CachedRepresentation) Expired

func (self *CachedRepresentation) Expired() bool

func (*CachedRepresentation) GetBody

func (self *CachedRepresentation) GetBody(encoding EncodingType) ([]byte, bool)

func (*CachedRepresentation) String

func (self *CachedRepresentation) String() string

fmt.Stringer interface

func (*CachedRepresentation) TimeToLive

func (self *CachedRepresentation) TimeToLive() float64

In seconds

func (*CachedRepresentation) Update

func (self *CachedRepresentation) Update(key CacheKey)

type CreateFunc

type CreateFunc func(config ard.StringMap, context *js.Context) (interface{}, error)

func GetType

func GetType(type_ string) (CreateFunc, error)

type EncodingType

type EncodingType int

func (EncodingType) String

func (self EncodingType) String() string

fmt.Stringer interface

type HandleTagFunc added in v1.0.1

type HandleTagFunc func(context *JSTContext, code string) bool // return true to allow trailing newlines

type JSTContext

type JSTContext struct {
	Builder strings.Builder
	// contains filtered or unexported fields
}

func (*JSTContext) NextSuffix

func (self *JSTContext) NextSuffix() string

func (*JSTContext) WriteLiteral

func (self *JSTContext) WriteLiteral(literal string)

type RenderFunc

type RenderFunc func(content string, context *js.Context) (string, error)

func GetRenderer

func GetRenderer(renderer string) (RenderFunc, error)

type StartEntry

type StartEntry struct {
	Startable Startable
}

type StartGroup

type StartGroup struct {
	Startables []Startable
	// contains filtered or unexported fields
}

func NewStartGroup

func NewStartGroup(startables []Startable) *StartGroup

func (*StartGroup) Start

func (self *StartGroup) Start()

func (*StartGroup) Stop

func (self *StartGroup) Stop()

type Startable

type Startable interface {
	Start() error
	Stop() error
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL