revisor

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Feb 9, 2024 License: MIT Imports: 14 Imported by: 0

README

Revisor

Revisor allows you to define specifications for NavigaDoc contents as a series of declarations and pattern matching extensions to existing declarations.

Local testing

For running the actual tests and benchmarks, see the section on Testing.

The easiest way to test specifications against documents is by running the "revisor" command like so:

$ revisor document ./testdata/article-borked.json

That will validate the document using only the specifications in "./constraints/naviga.json".

Try running the same validation against a document with customer-specific content:

$ revisor document ./testdata/example-article.json 
affd4b2e-9709-45a7-a668-3abea1e1eac6 x-im/article ==>
link 2 resourceType(x-im/resourceType): undeclared block type or rel
link 3 template(x-im/template): undeclared block type or rel
link 4 commentingVisible(x-im/commentingVisible): undeclared block type or rel
link 5 commentingMode(x-im/commentingMode): undeclared block type or rel
meta block 1 (x-smf-sndp/tag): undeclared block type or rel
data attribute "selectedTimestamp" of meta block 1 (x-smf-sndp/tag): unknown attribute
data attribute "title" of meta block 1 (x-smf-sndp/tag): unknown attribute
data attribute "contentRepoId" of meta block 1 (x-smf-sndp/tag): unknown attribute
data attribute "resourceSubtype" of meta block 1 (x-smf-sndp/tag): unknown attribute
property "x-smf/newsSource": undeclared property
property "x-smf/originalPublishingChannel": undeclared property
property "x-smf/originalPublishingChannelName": undeclared property
documents had validation errors

Use the flag -spec ./constraints/example.json to load the specifications from "./constraints/example.json" as well.

Running a revisor server

It's also possible to run revisor as a service with the serve command, it takes the same --spec/--naviga-spec as the document command, and adds --addr to control the address to listen to.

Start the server in one shell:

$ revisor serve

...and post the example article to it in another using curl:

$ curl --data @testdata/example-article.json localhost:8000

You should get the same validation errors as in the previous example, but in JSON format. An empty array is returned for valid documents.

Writing specifications

The main entities points in a specification are documents, blocks and properties. Documents are declared by type, blocks by type and/or rel, and properties by name. An entity is not valid if we don't have a matching declaration for it, regardless of whether somebody has pattern-matched against it.

Both pattern matching and a lot of the validation that's performed is done though key value pairs of a name and a string constraint. Say that we want to match all links that have a rel of "subject", "channel", or "section" and add the ability to have "broader" links added to them, the specification would then look like this:

{
  "name": "Associated with and broader links",
  "description": "Extends subject, channel, and section links with broader links",
  "match": {"rel": {
    "enum": ["subject", "channel", "section"]
  }},
  "links": [
    {
      "declares": {"rel":"broader"},
      "attributes": {
        "type": {},
        "title": {}
      }
    }
  ]
}

Here we declare that links with rel "broader" are valid for all blocks that matches our expression, see "Block attributes" for a list of attributes that can be used in pattern matching. We also define that the attributes type and title must be present. The {"enum":...} object and the empty objects ({}) for type and title are all examples of string constraints.

String constraints
Name Use
optional Set to true if the value doesn't have to be present
allowEmpty Set to true if an empty value is ok
const A specific "value" that must match
enum A list ["of", "values"] where one must match
pattern A regular expression that the value must match
glob A list of glob patterns ["http://**", "https://**"] where one must match
template A Go template expression that the value must match, not available for pattern matching.
format A named format that the value must follow
time A time format specification
Formats

The following formats are available:

  • RFC3339: an RFC3339 timestamp ("2022-05-11T14:10:32Z")
  • int: an integer ("1234")
  • float: a floating point number ("12.34")
  • bool: a boolean ("true" or "false")
  • html: validate the contents as HTML

When using the format "html" it's also possible to use htmlPolicy to use a specific HTML policy. See the section on HTML policies.

Time formats

A Go time parsing layout (see the time package for documentation) that should be used to validate the timestamp.

Globs

Glob matching uses https://github.com/gobwas/glob for matching, and the glob patterns are compiled with "/" and "+" as separators.

Templates

The template expressions have the following values available:

  • Blocks: the current block as this, and if the block has a parent block that is available as parent.

  • Documents: the current document as this.

  • Properties: the current property as this, and the document as parent.

Templating cannot be used for pattern matching, just for validation.

Writing a document specification

A specification for a document contains:

  • documentation attributes name and description
  • a declaration (declares) or pattern matching rule (match)
  • attribute constraints (attributes)
  • meta, links, and content block specifications
  • properties specifications
{
  "name": "Planning item",
  "description": "Planned news coverage",
  "declares": "x-im/newscoverage",
  "meta": [
    {
      "name": "Main metadata block",
      "declares": {"type":"x-im/newscoverage"},
      "count": 1,
      "data": {
        "dateGranularity": {"enum":["date", "datetime"]},
        "description": {"allowEmpty":true},
        "start": {"format":"RFC3339"},
        "end": {"format":"RFC3339"},
        "priority": {},
        "publicDescription":{"allowEmpty":true},
        "slug": {"allowEmpty":true}
      }
    }
  ],
  "links": [
    {
      "declares": {"type": "x-im/assignment"},
      "links": [
        {
          "declares": {
            "rel":"assignment", "type": "x-im/assignment"
          },
          "attributes": {
            "uuid": {}
          }
        }
      ]
    }
  ],
  "properties": {
    "nrp:sector": {
      "value": {},
      "maxCount": 1,
      "parameters": {
        "literal": {}
      }
    }
  }
}
Writing a block specification

A block specification can contain:

  • documentation attributes name and description
  • a declaration (declares) or pattern matching rule (match)
  • attribute constraints (attributes)
  • data constraints
  • meta, links, and content block specifications
  • properties specifications
  • count, minCount and maxCount to control how many times a block can occur in the list of blocks it's in
  • blocksFrom directives that borrows the allowed blocks from a declared document type.
{
  "declares": {"type": "x-im/socialembed"},
  "links": [
    {
      "declares": {"rel":"self", "type":"x-im/tweet"},
      "maxCount": 1,
      "attributes": {
        "uri": {"glob":["im://tweet/*"]},
        "url": {"glob":["https://twitter.com/narendramodi/status/*"]}
      }
    },
    {
      "declares": {"rel":"alternate", "type":"text/html"},
      "maxCount": 1,
      "attributes": {
        "url": {"glob":["https://**"]},
        "title": {}
      },
      "data": {
        "context": {},
        "provider": {}
      }
    }
  ]
}
Writing a property specification
  • a documentation attribute description
  • a value string constraint
  • data string constraints
  • parameter string constraints (parameters)
  • count, minCount and maxCount to control how many times a block can occur in the list of blocks it's in
        "imext:haspublishedversion": {
          "description": "Is set to true if the document has been published previously",
          "value": {"format":"bool"},
          "count": 1
        },
        "infoSource": {
          "parameters": {
            "literal": {}
          },
          "maxCount": 1
        }
HTML policies

HTML policies are used to restrict what elements and attributes can be used in strings with the format "html". Attributes are defined as string constraints on elements. The default policy could look like this:

  "htmlPolicies": [
    {
      "name": "default",
      "elements": {
        "strong": {
          "attributes": {
            "id": {"optional":true}
          }
        },
        "a": {
          "attributes": {
            "id": {"optional":true},
            "href": {}
          }
        }
      }
    },
    {
      "name": "table",
      "uses": "default",
      "elements": {
        "tr": {
          "attributes": {
            "id": {"optional":true}
          }
        },
        "td": {
          "attributes": {
            "id": {"optional":true}
          }
        },
        "th": {
          "attributes": {
            "id": {"optional":true}
          }
        }
      }
    }
  ]

All "html" strings that use the default policy would then be able to use <strong> and <a>, and the "href" attribute would be requred for <a>. A "html" string that uses the "table" policy would be able to use everything from the default policy and <tr>, <td>, and <th>.

A customer can extend HTML policies using the "extend" attribute:

  "htmlPolicies": [
    {
      "extends": "default",
      "elements": {
        "personTag": {
          "attributes": {
            "id": {}
          }
        }
      }
    }
  ]

This would add support for "/" (HTML is case insensitive) to the default policy, and any policies that use it. Only one level of "extends" and "uses" is allowed, further chaining policies will result in an error.

Attribute reference
Document attributes

A list of available document attributes, and whether they can be used in pattern matching.

Name Description Match
uuid The document uuid No
uri The URI that identifies the document No
url A web-browsable location for the document No
type The type of the document Yes
subtype The document sub-type Yes
language The document language No
status Publication status No
title The document title No
provider The system or entity that provided the content Yes
Block attributes

A list of available block attributes, and whether they can be used in pattern matching.

Name Description Match
uuid The UUID of the document the block represents No
type The type of the block Yes
uri Identifies a resource in in URI form Yes
url A web-browsable location for the block Yes
title Human readable title of the block No
rel The relationship the block describes Yes
name A name that identifies the block Yes
value A generic value for the block Yes
contenttype The content type of the resource that the block describes Yes
role The role that the block or resource has Yes

Testing

Revisor implements a file-driven test in TestValidateDocument that checks so that all the "testdata/results/*.json" files match the validation results for the corresponding document under "testdata/". Result files with the prefix "base-" will be validated against "constraints/naviga.json", for result files with the prefix "example-" the "constraints/example.json" constraints will be used as well.

If the constraints have been updated, or new example documents have been added, the result files can be regenerated using ./update-test-results.sh.

Benchmarks

The benchmark BenchmarkValidateDocument tests the performance of validating "testdata/example-article.json" against the naviga and example organisation contsraint sets.

To run the benchmark execute:

$ go test -bench . -benchmem -cpu 1

Add the flags -memprofile memprofile.out -cpuprofile profile.out to collect CPU and memory profiles. Run go tool pprof -web profile.out for the respective profile files to open a profile graph in your web browser.

Comparing benchmarks

Install benchstat: go install golang.org/x/perf/cmd/benchstat@latest.

Run the benchmark on the unchanged code (stash your changes or check out main):

$ go test -bench . -benchmem -count 5 -cpu 1 | tee old.txt

Then run the benchmarks on the new code:

$ go test -bench . -benchmem -count 5 -cpu 1 | tee old.txt

Finally, run benchstat to get a summary of the change:

$ benchstat old.txt new.txt
name              old time/op    new time/op    delta
ValidateDocument     203µs ± 7%      99µs ± 3%  -51.03%  (p=0.008 n=5+5)

name              old alloc/op   new alloc/op   delta
ValidateDocument     134kB ± 0%      35kB ± 0%  -73.74%  (p=0.008 n=5+5)

name              old allocs/op  new allocs/op  delta
ValidateDocument     1.05k ± 0%     0.59k ± 0%  -43.48%  (p=0.008 n=5+5)
Fuzz tests

There are two fuzz targets in the project: FuzzValidationWide that allows fuzzing of the document and two constraint sets. It will load the naviga constraints, the example organisation constraints, and all documents in "./testdata/" and add them as fuzzing seeds. FuzzValidationConstraints adds all constraint sets from the "./constraints/" and adds them as fuzzing seeds. The fuzzing operation is then done against all documents in "./testdata/".

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ValidateEntity

func ValidateEntity(data []byte) (int, error)

ValidateEntity checks if the entity that starts at the beginning of the byte slice is valid and returns its length in bytes.

Types

type BlockConstraint

type BlockConstraint struct {
	Declares    *BlockSignature    `json:"declares,omitempty"`
	Name        string             `json:"name,omitempty"`
	Description string             `json:"description,omitempty"`
	Match       ConstraintMap      `json:"match,omitempty"`
	Count       *int               `json:"count,omitempty"`
	MaxCount    *int               `json:"maxCount,omitempty"`
	MinCount    *int               `json:"minCount,omitempty"`
	Links       []*BlockConstraint `json:"links,omitempty"`
	Meta        []*BlockConstraint `json:"meta,omitempty"`
	Content     []*BlockConstraint `json:"content,omitempty"`
	Attributes  ConstraintMap      `json:"attributes,omitempty"`
	Data        ConstraintMap      `json:"data,omitempty"`
	BlocksFrom  []BlocksFrom       `json:"blocksFrom,omitempty"`
}

BlockConstraint is a specification for a block.

func (BlockConstraint) BlockConstraints

func (bc BlockConstraint) BlockConstraints(kind BlockKind) []*BlockConstraint

BlockConstraints implements the BlockConstraintsSet interface.

func (BlockConstraint) DescribeCountConstraint

func (bc BlockConstraint) DescribeCountConstraint(kind BlockKind) string

DescribeCountConstraint returns a human readable (english) description of the count contstraint for the block constraint.

func (BlockConstraint) Matches

func (bc BlockConstraint) Matches(b *doc.Block) (Match, []string)

Matches checks if the given block matches the constraint and returns the names of the attributes that matched.

type BlockConstraintSet

type BlockConstraintSet interface {
	// BlockConstraints returns the constraints of the specified kind.
	BlockConstraints(kind BlockKind) []*BlockConstraint
}

type BlockKind

type BlockKind string

BlockKind describes the different kinds of blocks that are available.

const (
	BlockKindLink    BlockKind = "link"
	BlockKindMeta    BlockKind = "meta"
	BlockKindContent BlockKind = "content"
)

The different kinds of blocks that a block source can have.

func (BlockKind) Description

func (bk BlockKind) Description(n int) string

Description returns the pluralised name of the block kind.

type BlockSignature

type BlockSignature struct {
	Type string `json:"type,omitempty"`
	Rel  string `json:"rel,omitempty"`
}

BlockSignature is the signature of a block declaration.

type BlockSource

type BlockSource interface {
	// GetBlocks returns the child blocks of the specified type.
	GetBlocks(kind BlockKind) []doc.Block
}

BlockSource acts as an intermediary to allow us to write code that can treat both documents and blocks as a source of blocks.

type BlocksFrom

type BlocksFrom struct {
	DocType string    `json:"docType,omitempty"`
	Global  bool      `json:"global,omitempty"`
	Kind    BlockKind `json:"kind"`
}

BlocksFrom allows a block to borrow definitions for its child blocks from a document type.

type BorrowedBlocks

type BorrowedBlocks struct {
	Kind   BlockKind
	Source BlockConstraintSet
}

BorrowedBlocks wraps a block constraint set that has been borrowed.

func (BorrowedBlocks) BlockConstraints

func (bb BorrowedBlocks) BlockConstraints(kind BlockKind) []*BlockConstraint

BlockConstraints implements the BlockConstraintsSet interface.

type ConstraintMap

type ConstraintMap map[string]StringConstraint

func (ConstraintMap) Requirements

func (cm ConstraintMap) Requirements() string

type ConstraintSet

type ConstraintSet struct {
	Version      int                   `json:"version,omitempty"`
	Schema       string                `json:"$schema,omitempty"`
	Name         string                `json:"name"`
	Documents    []DocumentConstraint  `json:"documents,omitempty"`
	Links        []*BlockConstraint    `json:"links,omitempty"`
	Meta         []*BlockConstraint    `json:"meta,omitempty"`
	Content      []*BlockConstraint    `json:"content,omitempty"`
	Properties   PropertyConstraintMap `json:"properties,omitempty"`
	Attributes   ConstraintMap         `json:"attributes,omitempty"`
	HTMLPolicies []HTMLPolicy          `json:"htmlPolicies,omitempty"`
}

func (ConstraintSet) BlockConstraints

func (cs ConstraintSet) BlockConstraints(kind BlockKind) []*BlockConstraint

func (ConstraintSet) Validate

func (cs ConstraintSet) Validate() error

type DocumentBlocks

type DocumentBlocks struct {
	// contains filtered or unexported fields
}

func NewDocumentBlocks

func NewDocumentBlocks(document *doc.Document) DocumentBlocks

func (DocumentBlocks) GetBlocks

func (db DocumentBlocks) GetBlocks(kind BlockKind) []doc.Block

type DocumentConstraint

type DocumentConstraint struct {
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
	// Declares is used to declare a document type.
	Declares string `json:"declares,omitempty"`
	// Match is used to extend other document declarations.
	Match      ConstraintMap         `json:"match,omitempty"`
	Links      []*BlockConstraint    `json:"links,omitempty"`
	Meta       []*BlockConstraint    `json:"meta,omitempty"`
	Content    []*BlockConstraint    `json:"content,omitempty"`
	Properties PropertyConstraintMap `json:"properties,omitempty"`
	Attributes ConstraintMap         `json:"attributes,omitempty"`
}

DocumentConstraint describes a set of constraints for a document. Either by declaring a document type, or matching against a document that has been declared somewhere else.

func (DocumentConstraint) BlockConstraints

func (dc DocumentConstraint) BlockConstraints(kind BlockKind) []*BlockConstraint

BlockConstraints implements the BlockConstraintsSet interface.

func (DocumentConstraint) Matches

func (dc DocumentConstraint) Matches(
	d *doc.Document, vCtx *ValidationContext,
) Match

Matches checks if the given document matches the constraint.

type EntityRef

type EntityRef struct {
	RefType   RefType   `json:"refType"`
	BlockKind BlockKind `json:"kind,omitempty"`
	Index     int       `json:"index,omitempty"`
	Name      string    `json:"name,omitempty"`
	Type      string    `json:"type,omitempty"`
	Rel       string    `json:"rel,omitempty"`
}

func (EntityRef) String

func (er EntityRef) String() string

type Glob

type Glob struct {
	// contains filtered or unexported fields
}

Glob is used to represent a compiled glob pattern that can be used with JSON marshalling and unmarshalling.

func CompileGlob

func CompileGlob(pattern string) (*Glob, error)

CompileGlob compiles a glob pattern.

func (*Glob) MarshalJSON

func (g *Glob) MarshalJSON() ([]byte, error)

func (*Glob) Match

func (g *Glob) Match(s string) bool

Match checks if the string matches the pattern.

func (*Glob) UnmarshalJSON

func (g *Glob) UnmarshalJSON(data []byte) error

type GlobList

type GlobList []*Glob

GlobList is a Glob slice with some convenience functions.

func (GlobList) MatchOrEmpty

func (gl GlobList) MatchOrEmpty(v string) bool

MatchOrEmpty returns true if the value matches any of the glob patterns, or if the list is nil or empty.

func (GlobList) String

func (gl GlobList) String() string

String returns a human readable (english) description of the glob constraint.

type HTMLElement

type HTMLElement struct {
	Attributes ConstraintMap `json:"attributes,omitempty"`
}

HTMLElement describes the constraints for a HTML element.

type HTMLPolicy

type HTMLPolicy struct {
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`

	// Uses will base the policy on another policy.
	Uses string `json:"uses,omitempty"`
	// Extends will add the declared elements to another policy.
	Extends string `json:"extends,omitempty"`

	Elements map[string]HTMLElement `json:"elements"`
	// contains filtered or unexported fields
}

HTMLPolicy is used to declare supported elements, and what attributes they can have.

func (*HTMLPolicy) Check

func (hp *HTMLPolicy) Check(v string) error

Check that the given value follows the constraints of the policy.

type HTMLPolicySet

type HTMLPolicySet struct {
	// contains filtered or unexported fields
}

HTMLPolicySet is a set of declared HTML policies.

func NewHTMLPolicySet

func NewHTMLPolicySet() *HTMLPolicySet

func (*HTMLPolicySet) Add

func (s *HTMLPolicySet) Add(source string, policies ...HTMLPolicy) error

Add policies to the set.

func (*HTMLPolicySet) Resolve

func (s *HTMLPolicySet) Resolve() (map[string]*HTMLPolicy, error)

Resolve all extensions and usages and return the finished policies.

type Match

type Match int

Match describes if and how a block constraint matches a block.

const (
	NoMatch Match = iota
	Matches
	MatchDeclaration
)

Match constants for no match / match / matched declaration.

type NestedBlocks

type NestedBlocks struct {
	// contains filtered or unexported fields
}

func NewNestedBlocks

func NewNestedBlocks(block *doc.Block) NestedBlocks

func (NestedBlocks) GetBlocks

func (nb NestedBlocks) GetBlocks(kind BlockKind) []doc.Block

type PropertyConstraint

type PropertyConstraint struct {
	Description string            `json:"description,omitempty"`
	Value       *StringConstraint `json:"value,omitempty"`
	Parameters  ConstraintMap     `json:"parameters,omitempty"`
	Count       *int              `json:"count"`
	MinCount    *int              `json:"minCount"`
	MaxCount    *int              `json:"maxCount"`
}

PropertyConstraint is a declaration for a document property.

func (PropertyConstraint) DescribeCountConstraint

func (pc PropertyConstraint) DescribeCountConstraint(name string) string

DescribeCountConstraint returns a human readable (english) description of the count contstraint for the property constraint.

type PropertyConstraintMap

type PropertyConstraintMap map[string]PropertyConstraint

PropertyConstraintMap is used to declare named document properties.

type RefType

type RefType string
const (
	RefTypeBlock     RefType = "block"
	RefTypeProperty  RefType = "property"
	RefTypeAttribute RefType = "attribute"
	RefTypeData      RefType = "data attribute"
	RefTypeParameter RefType = "parameter"
)

func (RefType) String

func (rt RefType) String() string

type Regexp

type Regexp struct {
	// contains filtered or unexported fields
}

func (*Regexp) MarshalJSON

func (r *Regexp) MarshalJSON() ([]byte, error)

func (*Regexp) Match

func (r *Regexp) Match(v string) bool

func (*Regexp) String

func (r *Regexp) String() string

func (*Regexp) UnmarshalJSON

func (r *Regexp) UnmarshalJSON(data []byte) error

type StringConstraint

type StringConstraint struct {
	Name        string       `json:"name,omitempty"`
	Description string       `json:"description,omitempty"`
	Optional    bool         `json:"optional,omitempty"`
	AllowEmpty  bool         `json:"allowEmpty,omitempty"`
	Const       *string      `json:"const,omitempty"`
	Enum        []string     `json:"enum,omitempty"`
	Pattern     *Regexp      `json:"pattern,omitempty"`
	Glob        GlobList     `json:"glob,omitempty"`
	Template    *Template    `json:"template,omitempty"`
	Format      StringFormat `json:"format,omitempty"`
	Time        string       `json:"time,omitempty"`
	HTMLPolicy  string       `json:"htmlPolicy,omitempty"`
}

func (*StringConstraint) Requirement

func (sc *StringConstraint) Requirement() string

func (*StringConstraint) Validate

func (sc *StringConstraint) Validate(
	value string, exists bool, vCtx *ValidationContext,
) error

type StringFormat

type StringFormat string
const (
	StringFormatNone    StringFormat = ""
	StringFormatRFC3339 StringFormat = "RFC3339"
	StringFormatInt     StringFormat = "int"
	StringFormatFloat   StringFormat = "float"
	StringFormatBoolean StringFormat = "bool"
	StringFormatHTML    StringFormat = "html"
	StringFormatUUID    StringFormat = "uuid"
)

func (StringFormat) Describe

func (f StringFormat) Describe() string

type Template

type Template struct {
	// contains filtered or unexported fields
}

func (*Template) MarshalJSON

func (t *Template) MarshalJSON() ([]byte, error)

func (*Template) Render

func (t *Template) Render(data TemplateValues) (string, error)

func (*Template) String

func (t *Template) String() string

func (*Template) UnmarshalJSON

func (t *Template) UnmarshalJSON(data []byte) error

type TemplateValue

type TemplateValue func() (interface{}, error)

func BlockTemplateValue

func BlockTemplateValue(b *doc.Block) TemplateValue

func DocumentTemplateValue

func DocumentTemplateValue(d *doc.Document) TemplateValue

func PropertyTemplateValue

func PropertyTemplateValue(p *doc.Property) TemplateValue

type TemplateValues

type TemplateValues map[string]TemplateValue

type ValidationContext

type ValidationContext struct {
	TemplateData TemplateValues
	ValidateHTML func(policyName, value string) error
}

type ValidationResult

type ValidationResult struct {
	Entity []EntityRef `json:"entity,omitempty"`
	Error  string      `json:"error,omitempty"`
}

func (ValidationResult) String

func (vr ValidationResult) String() string

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

func NewValidator

func NewValidator(
	constraints ...ConstraintSet,
) (*Validator, error)

func (*Validator) ValidateDocument

func (v *Validator) ValidateDocument(document *doc.Document) []ValidationResult

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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