ganno

package module
v0.0.0-...-e638228 Latest Latest
Warning

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

Go to latest
Published: Mar 4, 2022 License: Apache-2.0 Imports: 3 Imported by: 2

README

Build Status codecov Go Report Card GoDoc

ganno

Package ganno implements java-style annotations in Go.

If your unfamiliar with java's annotation system, you can look at their annotations basics guide

This library includes a lexer (based on the goblex lexer library) and a pluggable annotation parser.

API Documentation: https://godoc.org/github.com/brainicorn/ganno

Issue Tracker

Java-Style Annotations

Java-Style anotations are written using the following format:

@<ident>(<ident>=<valueOrValues>,...)

examples:


@simpleAnnotation()

@simplewithParam(mykey=myval)

@quotedVal(somekey="my value")

@multipleParams(magic="wizards", awesome="unicorns")

@multipleVals(mypets=["dog", "kitty cat"])

Annotations may also be split across multiple lines even within single-line comments:


// @stuffILike(
// 	instrument="drums"
// 	,mypets=[
// 		"dog"
// 		,"kitty cat"
// 	]
// 	,food="nutritional units"
// )

Features

  • Highly tested
  • Ready to use out of the box
  • Anotations can be parsed from:
    • single line text
    • multi-line text
    • multi-line comment blocks
    • single-line comments
    • multiple single-line comment blocks
  • Runtime pluggable
    • Supports custom Annotation types
      • Custom annotations can provide strongly-typed sttribute accessors
    • Custom annotation factories can be registered at runtime
    • Factories can enforce strict validation of attributes
    • Factories and custom Annotations can be re-used/distributed as libraries
  • Extensible
    • Provides interfaces for implementing custom AnnotationParsers and or Annotations collections

Basic Use

Out of the box, ganno is ready to use, but it's good to understand a couple of things:

  • The parser returns an Annotations object as well as any validation errors while parsing
    • Validation errors are returned in a slice and the annotation that errored is discarded
    • The Annotations object provides accessors for All() annotations as well as ByName(name)
  • The default annotation object returned provides an Attributes() method which returns a map[string][]string where the map key is the attribute name and the value is a slice of strings. This is to support multi-value atrributes and single-value attributes are a slice of 1.

Here's a simple example of parsing an @pets annotation:

input := `my @pet(name="fluffy buns", hasFur=true) is soooo cute!`

parser := ganno.NewAnnotationParser()

annos, errs := parser.Parse(input)

//there were some validation errors, we could inspect them, but let's just panic the first one
if len(errs) > 0 {
	panic(errs[0])
}

//since we know there's only one...
ourPet := annos.All()[0]

// get our pet's name
var name := "unknown"
if nameSlice, nameOk := ourPet.Attributes()["name"]; nameOk {
	name = nameSlice[0]
}

var hasFur := false
// get our pet's fur status
furSlice, furOk := ourPet.Attributes()["hasfur"]; if furOk {
	if hasFur, err := strconv.ParseBool(furSlice[0]); err != nil {
		hasFur = false
	}
}

fmt.Printf("%s is fluffy? %t\n", name, hasFur)
// Output: fluffy buns is fluffy? true

While the above example certainly works, there's a lot of validation that's going on that's not very reusable and is error prone. Let's see how we can use a plugin to solve this...

Advanced Use

By creating a custom annotation type and a factory for it, we can encapsulate all of the above validation logic and provide a cleaner interface when dealing with @pet annotations...

First off, let's create the "plugin" bits that could be put into their own package and/or library if desired....

// PetAnno is a custom Annotation type for @pet() annotations
type PetAnno struct {
	Attrs map[string][]string
}

func (a *PetAnno) AnnotationName() string {
	return "pet"
}

func (a *PetAnno) Attributes() map[string][]string {
	return a.Attrs
}

// HasFur is a strongly-typed accessor for the "hasfur" attribute
func (a *PetAnno) Hasfur() bool {
	b, _ := strconv.ParseBool(a.Attrs["hasfur"][0])

	return b
}

// Name is an accessor for the "name" attribute
func (a *PetAnno) Name() string {
	return a.Attrs["name"][0]
}

// PetAnnoFactory is our custom factory that can validate @pet attributes and return new PetAnnos
type PetAnnoFactory struct{}

func (f *PetAnnoFactory) ValidateAndCreate(name string, attrs map[string][]string) (Annotation, error) {
	furs, furOk := attrs["hasfur"]
	_, nameOk := attrs["name"]

	// require the hasfur attr
	if !furOk {
		return nil, fmt.Errorf("pet annotation requires the attribute %q", "hasfur")
	}

	// require the name attr
	if !nameOk {
		return nil, fmt.Errorf("pet annotation requires the attribute %q", "name")
	}

	// make sure hasfur is a parsable boolean
	if _, err := strconv.ParseBool(furs[0]); err != nil {
		return nil, fmt.Errorf("pet annotation attribute %q must have a boolean value", "hasfur")
	}

	return &PetAnno{
		Attrs: attrs,
	}, nil
}

With all of that in place, our dealings with @pet annotations is simplfied:

parser := NewAnnotationParser()

//register our factory
parser.RegisterFactory("pet", &PetAnnoFactory{})

input := `my @pet(name="fluffy buns", hasFur=true) is soooo cute!`

annos, errs := parser.Parse(input)

if len(errs) > 0 {
	panic(errs[0])
}

// get our one pet annotation and cast it to a PetAnno
mypet := annos.ByName("pet")[0].(*PetAnno)

fmt.Printf("%s is fluffy? %t\n", mypet.Name(), mypet.Hasfur())
// Output: fluffy buns is fluffy? true

API Documentation: https://godoc.org/github.com/brainicorn/ganno

Issue Tracker

Contributors

Pull requests, issues and comments welcome. For pull requests:

  • Add tests for new features and bug fixes
  • Follow the existing style
  • Separate unrelated changes into multiple pull requests

See the existing issues for things to start contributing.

For bigger changes, make sure you start a discussion first by creating an issue and explaining the intended change.

License

Apache 2.0 licensed, see LICENSE.txt file.

Documentation

Overview

Package ganno implements java-style annotations in Go. This library includes a lexer (based on https://github.com/brainicorn/goblex) and a pluggable annotation parser.

Annotations can be written in the following formats:

@simpleAnnotation()

@simplewithParam(mykey=myval)

@quotedVal(somekey="my value")

@multipleParams(magic="wizards", awesome="unicorns")

@multipleVals(mypets=["dog", "kitty cat"])

Annotations may also be split across multiple lines:

@stuffILike(
	instrument="drums"
	,mypets=[
		"dog"
		,"kitty cat"
	]
	,food="nutritional units"
)

Although this library can be used out of the box as-is, consumer can also "plug-in" custom annotation types that can validate discovered annotations and provide strongly-type attribute accessors.

Custom Annotations and their factories can be re-used/distributed as libraries and registered with parsers at runtime.

see example below

Example
package main

import (
	"fmt"
	"strconv"
)

// PetAnno is a custom Annotation type for @pet() annotations
type PetAnno struct {
	Attrs map[string][]string
}

func (a *PetAnno) AnnotationName() string {
	return "pet"
}

func (a *PetAnno) Attributes() map[string][]string {
	return a.Attrs
}

// HasFur is a strongly-typed accessor for the "hasfur" attribute
func (a *PetAnno) Hasfur() bool {
	b, _ := strconv.ParseBool(a.Attrs["hasfur"][0])

	return b
}

// Name is an accessor for the "name" attribute
func (a *PetAnno) Name() string {
	return a.Attrs["name"][0]
}

// PetAnnoFactory is our custom factory that can validate @pet attributes and return new PetAnnos
type PetAnnoFactory struct{}

func (f *PetAnnoFactory) ValidateAndCreate(name string, attrs map[string][]string) (Annotation, error) {
	furs, furOk := attrs["hasfur"]
	_, nameOk := attrs["name"]

	// require the hasfur attr
	if !furOk {
		return nil, fmt.Errorf("pet annotation requires the attribute %q", "hasfur")
	}

	// require the name attr
	if !nameOk {
		return nil, fmt.Errorf("pet annotation requires the attribute %q", "name")
	}

	// make sure hasfur is a parsable boolean
	if _, err := strconv.ParseBool(furs[0]); err != nil {
		return nil, fmt.Errorf("pet annotation attribute %q must have a boolean value", "hasfur")
	}

	return &PetAnno{
		Attrs: attrs,
	}, nil
}

func main() {

	parser := NewAnnotationParser()

	//register our factory
	parser.RegisterFactory("pet", &PetAnnoFactory{})

	input := `my @pet(name="fluffy buns", hasFur=true) is soooo cute!`

	annos, errs := parser.Parse(input)

	if len(errs) > 0 {
		panic(errs[0])
	}

	// get our one pet annotation and cast it to a PetAnno
	mypet := annos.ByName("pet")[0].(*PetAnno)

	fmt.Printf("%s is fluffy? %t\n", mypet.Name(), mypet.Hasfur())
}
Output:

fluffy buns is fluffy? true

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func LexBegin

func LexBegin(lexer *goblex.Lexer) goblex.LexFn

LexBegin is the entry point LexFn for lexing java style annotations. Parsers should pass this function as the begin parameter when calling goblex.NewLexer

Types

type Annotation

type Annotation interface {

	// AnnotationName is the name of the annotation between the @ symbol and the (.
	// The value needs to be a valid ident and comparisons are case-insensitive
	AnnotationName() string

	// Attributes holds any key/value pairs inside of the ().
	// The map key is the (loer-cased) key found in the key/val pair. The value is a slice of strings.
	// The value is a slice even if the annotation has a single value to support multi-value k/v pairs.
	Attributes() map[string][]string
}

Annotation is the interface for a single annotation instance. This is what is returned by an AnnotationFactory. Consumers can implement their own custom annotations using this interface.

Custom implementations can/should provide strongly typed accessors for their specific attributes.

type AnnotationFactory

type AnnotationFactory interface {
	// ValidateAndCreate is caled by parsers when they find an annotation whose (lower-case) name
	// matches the name of the registered factory. This is the same value passed in as name to this
	// method which allows registering the same factory multiple times under different names.
	//
	// attrs is the map of k/v pairs found when parsing the annotation. This method should use the attrs
	// to validate that the annotation has all expected/required parameters.
	//
	// Any validation error should be returned as error.
	ValidateAndCreate(name string, attrs map[string][]string) (Annotation, error)
}

AnnotationFactory is the interface consumers can implement to provide custom Annotation creation.

AnnotationFactories can be registered with the parser by name.

type AnnotationParser

type AnnotationParser interface {
	// RegisterFactory registers an AnnotationFactory with the supplied name. The name will be
	// lower-case compared with the names of discovered annotations to choose the proper factory for
	// creation.
	//
	// If name is blank or a factory with the same name has already been registered an error will be
	// retured.
	RegisterFactory(name string, factory AnnotationFactory) error

	// Parse lexes all of the tokens in the input string and returns an Annotations object which holds
	// all of the valid discovered annotations. The annotations may be of various types/implementations
	// based on the registered factories and can be cast to those specific types.
	//
	// If a validation error is returned by the factory during creation, the annotation will not be
	// added to the Annotations and the error will be put into the returned errors slice.
	Parse(input string) (Annotations, []error)
}

AnnotationParser is the interface for parsing a string and returning Annotations. The returned Annnotations object should be populated with Annotation objects created using the registered AnnotationFactory objects.

Consumers of this library shouldn't need to implement this interface unless they have very specific custom parsing needs.

func NewAnnotationParser

func NewAnnotationParser() AnnotationParser

NewAnnotationParser creates an AnnotationParser that can be used to discover annotations.

type Annotations

type Annotations interface {
	// All returns a slice of all found Annotation objects
	All() []Annotation

	// ByName retrieves a slice of Annotation objects whose name matches name. The name comparison
	// should use strings.ToLower before comparing.
	ByName(name string) []Annotation
}

Annotations is the interface for holding the collection of annotations found during parsing. This interface provides helper methods for retrieving all annotations or annotations by name.

For the most part consumers do not need to implement this unless they implement a custom AnnotationParser and have a specific need to augment annotation retrieval.

Jump to

Keyboard shortcuts

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