Skip to content

Commit

Permalink
block declarations (#4)
Browse files Browse the repository at this point in the history
* block declaration support

* remove documentation for dropped commands
  • Loading branch information
hugowetterberg authored Sep 17, 2024
1 parent f41e767 commit b5f799c
Show file tree
Hide file tree
Showing 35 changed files with 2,184 additions and 771 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22.3'
go-version: '1.23.0'
cache: false # Let golangcilint handle caching
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: v1.59
version: v1.60
args: --timeout=4m
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22.3'
go-version: '1.23.0'
- name: Run go tests
run: |
go test ./...
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
linters:
enable:
- bodyclose
- copyloopvar
- dogsled
- dupl
- errcheck
- errorlint
- exhaustive
- exportloopref
- forbidigo
- gci
- gochecknoinits
Expand Down
114 changes: 70 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,88 @@

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

## Local testing
## Breaking changes

For running the actual tests and benchmarks, see the section on [Testing](#markdown-header-testing).
### v0.9.0

The easiest way to test specifications against documents is by running the "revisor" command like so:
I this release we remove the ability to define global blocks and attributes that automatically are available for all document types. Globals was a mistake and have now been replaced by block definitions:

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

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

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

``` bash
$ revisor document ./testdata/example-article.json
meta block 2 (tt/slugline): undeclared block type or rel
attribute "type" of meta block 2 (tt/slugline): undeclared block attribute
attribute "value" of meta block 2 (tt/slugline): undeclared block attribute
content block 2 (tt/visual): undeclared block type or rel
attribute "type" of content block 2 (tt/visual): undeclared block attribute
data attribute "caption" of content block 2 (tt/visual): unknown attribute
link 1 self(tt/picture) of content block 2 (tt/visual): undeclared block type or rel
attribute "type" of link 1 self(tt/picture) of content block 2 (tt/visual): undeclared block attribute
attribute "uri" of link 1 self(tt/picture) of content block 2 (tt/visual): undeclared block attribute
attribute "url" of link 1 self(tt/picture) of content block 2 (tt/visual): undeclared block attribute
attribute "rel" of link 1 self(tt/picture) of content block 2 (tt/visual): undeclared block attribute
data attribute "credit" of link 1 self(tt/picture) of content block 2 (tt/visual): unknown attribute
data attribute "height" of link 1 self(tt/picture) of content block 2 (tt/visual): unknown attribute
data attribute "hiresScale" of link 1 self(tt/picture) of content block 2 (tt/visual): unknown attribute
data attribute "width" of link 1 self(tt/picture) of content block 2 (tt/visual): unknown attribute
content block 3 (tt/dateline): undeclared block type or rel
attribute "type" of content block 3 (tt/dateline): undeclared block attribute
data attribute "text" of content block 3 (tt/dateline): unknown attribute
documents had validation errors
``` json
{
"version": 1,
"name": "block-example",
"documents": [
{
"name": "Article",
"description": "An editorial article",
"declares": "core/article",
"content": [
{"ref": "core://text"}
]
}
],
"content": [
{
"id": "core://text",
"block": {
"description": "A standard text block",
"declares": {"type":"core/text"},
"attributes": {
"role": {
"optional":true,
"enum": ["heading-1", "heading-2", "preamble"]
}
},
"data": {
"text":{
"allowEmpty":true,
"format": "html"
}
}
}
}
]
}
```

Use the flag `-spec ./constraints/tt.json` to load the organisation specific constraints for TT.

### Running a revisor server
Inline blocks can still be declared as before.

It's also possible to run revisor as a service with the `serve` command, it takes the same `--spec`/`--core-spec` as the `document` command, and adds `--addr` to control the address to listen to.
When using `ref` it's possible to extend the block in the same block constraint. Writing this:

Start the server in one shell:

``` bash
$ revisor serve
``` json
{
"version": 1,
"name": "block-example",
"documents": [
{
"name": "Article",
...
"meta": [
{
"ref": "core://newsvalue",
"count": 1
}
]
...
```

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

``` bash
$ curl --data @testdata/example-article.json localhost:8000
``` json
...
"meta": [
{
"ref": "core://newsvalue"
},
{
"match": {"type": "core/newsvalue"},
"count": 1
}
]
...
```

You should get the same validation errors as in the previous example, but in JSON format. An empty array is returned for valid documents.
...as any constraints will be treated as a block constraint with a `match` directive equivalent to the `declares` object of the referenced block.

## Writing specifications

Expand Down
138 changes: 116 additions & 22 deletions block_constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,38 +45,49 @@ func (bk BlockKind) Description(n int) string {
return "blocks"
}

// BlocksFrom allows a block to borrow definitions for its child blocks from a
// document type.
type BlocksFrom struct {
DocType string `json:"docType,omitempty"`
Global bool `json:"global,omitempty"`
Kind BlockKind `json:"kind"`
// BlockSignature is the signature of a block declaration.
type BlockSignature struct {
Type string `json:"type,omitempty"`
Rel string `json:"rel,omitempty"`
Role string `json:"role,omitempty"`
}

// BorrowedBlocks wraps a block constraint set that has been borrowed.
type BorrowedBlocks struct {
Kind BlockKind
Source BlockConstraintSet
}
func (bs BlockSignature) AsConstraint() ConstraintMap {
m := make(map[string]StringConstraint)

// BlockConstraints implements the BlockConstraintsSet interface.
func (bb BorrowedBlocks) BlockConstraints(kind BlockKind) []*BlockConstraint {
if bb.Kind != kind {
return nil
if bs.Type != "" {
m[string(blockAttrType)] = StringConstraint{
Const: strPtr(bs.Type),
}
}

if bs.Rel != "" {
m[string(blockAttrRel)] = StringConstraint{
Const: strPtr(bs.Rel),
}
}

if bs.Role != "" {
m[string(blockAttrRole)] = StringConstraint{
Const: strPtr(bs.Role),
}
}

return bb.Source.BlockConstraints(kind)
return MakeConstraintMap(m)
}

// BlockSignature is the signature of a block declaration.
type BlockSignature struct {
Type string `json:"type,omitempty"`
Rel string `json:"rel,omitempty"`
Role string `json:"role,omitempty"`
func strPtr(v string) *string {
return &v
}

type BlockDefinition struct {
ID string `json:"id"`
Block BlockConstraint `json:"block"`
}

// BlockConstraint is a specification for a block.
type BlockConstraint struct {
Ref string `json:"ref,omitempty"`
Declares *BlockSignature `json:"declares,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Expand All @@ -89,10 +100,81 @@ type BlockConstraint struct {
Content []*BlockConstraint `json:"content,omitempty"`
Attributes ConstraintMap `json:"attributes,omitempty"`
Data ConstraintMap `json:"data,omitempty"`
BlocksFrom []BlocksFrom `json:"blocksFrom,omitempty"`
Deprecated *Deprecation `json:"deprecated,omitempty"`
}

// IsNoop returns true if the constraint doesn't affect anything.
func (bc BlockConstraint) IsNoop() bool {
return bc.Ref == "" && bc.Declares == nil && bc.Count == nil &&
bc.MaxCount == nil && bc.MinCount == nil &&
len(bc.Links) == 0 && len(bc.Meta) == 0 && len(bc.Content) == 0 &&
len(bc.Attributes.Keys) == 0 && len(bc.Data.Keys) == 0 &&
bc.Deprecated == nil
}

func (bc BlockConstraint) Copy() *BlockConstraint {
return &BlockConstraint{
Ref: bc.Ref,
Declares: bSigCopy(bc.Declares),
Name: bc.Name,
Description: bc.Description,
Match: bc.Match.Copy(),
Count: intPtrCopy(bc.Count),
MaxCount: intPtrCopy(bc.MaxCount),
MinCount: intPtrCopy(bc.MinCount),
Links: bsListCopy(bc.Links),
Meta: bsListCopy(bc.Meta),
Content: bsListCopy(bc.Content),
Attributes: bc.Attributes.Copy(),
Data: bc.Data.Copy(),
Deprecated: deprCopy(bc.Deprecated),
}
}

func bSigCopy(v *BlockSignature) *BlockSignature {
if v == nil {
return nil
}

s := *v

return &s
}

func intPtrCopy(v *int) *int {
if v == nil {
return nil
}

n := *v

return &n
}

func bsListCopy(b []*BlockConstraint) []*BlockConstraint {
if len(b) == 0 {
return nil
}

s := make([]*BlockConstraint, len(b))

for i := range b {
s[i] = b[i].Copy()
}

return s
}

func deprCopy(v *Deprecation) *Deprecation {
if v == nil {
return nil
}

d := *v

return &d
}

// BlockConstraints implements the BlockConstraintsSet interface.
func (bc BlockConstraint) BlockConstraints(kind BlockKind) []*BlockConstraint {
switch kind {
Expand All @@ -107,6 +189,18 @@ func (bc BlockConstraint) BlockConstraints(kind BlockKind) []*BlockConstraint {
return nil
}

// SetBlockConstraints implements the BlockConstraintsSet interface.
func (bc *BlockConstraint) SetBlockConstraints(kind BlockKind, blocks []*BlockConstraint) {
switch kind {
case BlockKindLink:
bc.Links = blocks
case BlockKindMeta:
bc.Meta = blocks
case BlockKindContent:
bc.Content = blocks
}
}

// Match describes if and how a block constraint matches a block.
type Match int

Expand Down
Loading

0 comments on commit b5f799c

Please sign in to comment.