mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-01-12 03:21:33 +00:00
Introduce query language (resembling reprepro syntax).
This commit is contained in:
4
Makefile
4
Makefile
@@ -1,6 +1,6 @@
|
||||
GOVERSION=$(shell go version | awk '{print $$3;}')
|
||||
PACKAGES=database deb files http utils
|
||||
ALL_PACKAGES=aptly cmd console database deb files http utils
|
||||
PACKAGES=database deb files http query utils
|
||||
ALL_PACKAGES=aptly cmd console database deb files http query utils
|
||||
BINPATH=$(abspath ./_vendor/bin)
|
||||
GOM_ENVIRONMENT=-test
|
||||
PYTHON?=python
|
||||
|
||||
211
query/lex.go
Normal file
211
query/lex.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// itemType identifies the type of lex items.
|
||||
type itemType int
|
||||
|
||||
const eof = -1
|
||||
|
||||
const (
|
||||
itemError itemType = iota // error occurred;
|
||||
// value is text of error
|
||||
itemEOF
|
||||
itemLeftParen // (
|
||||
itemRightParen // )
|
||||
itemOr // |
|
||||
itemAnd // ,
|
||||
itemNot // !
|
||||
itemLt // <<
|
||||
itemLtEq // <=, <
|
||||
itemGt // >>
|
||||
itemGtEq // >=, >
|
||||
itemEq // =
|
||||
itemPatMatch // %
|
||||
itemRegexp // ~
|
||||
itemString
|
||||
)
|
||||
|
||||
// item represents a token returned from the scanner.
|
||||
type item struct {
|
||||
typ itemType // Type, such as itemNumber.
|
||||
val string // Value, such as "23.2".
|
||||
}
|
||||
|
||||
func (i item) String() string {
|
||||
if i.typ == itemString {
|
||||
return fmt.Sprintf("%#v", i.val)
|
||||
}
|
||||
if i.typ == itemEOF {
|
||||
return "<EOL>"
|
||||
}
|
||||
return i.val
|
||||
}
|
||||
|
||||
// stateFn represents the state of the scanner
|
||||
// as a function that returns the next state.
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
// lexer holds the state of the scanner.
|
||||
type lexer struct {
|
||||
name string // used only for error reports.
|
||||
input string // the string being scanned.
|
||||
start int // start position of this item.
|
||||
pos int // current position in the input.
|
||||
width int // width of last rune read from input.
|
||||
items chan item // channel of scanned items.
|
||||
last item
|
||||
}
|
||||
|
||||
func lex(name, input string) (*lexer, chan item) {
|
||||
l := &lexer{
|
||||
name: name,
|
||||
input: input,
|
||||
items: make(chan item),
|
||||
}
|
||||
go l.run() // Concurrently run state machine.
|
||||
return l, l.items
|
||||
}
|
||||
|
||||
// emit passes an item back to the client.
|
||||
func (l *lexer) emit(t itemType) {
|
||||
l.items <- item{t, l.input[l.start:l.pos]}
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// run lexes the input by executing state functions until
|
||||
// the state is nil.
|
||||
func (l *lexer) run() {
|
||||
for state := lexMain; state != nil; {
|
||||
state = state(l)
|
||||
}
|
||||
close(l.items) // No more tokens will be delivered.
|
||||
}
|
||||
|
||||
// next returns the next rune in the input.
|
||||
func (l *lexer) next() (r rune) {
|
||||
if l.pos >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
r, l.width =
|
||||
utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.pos += l.width
|
||||
return r
|
||||
}
|
||||
|
||||
// ignore skips over the pending input before this point.
|
||||
func (l *lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// backup steps back one rune.
|
||||
// Can be called only once per call of next.
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
// peek returns but does not consume
|
||||
// the next rune in the input.
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *lexer) Current() item {
|
||||
if l.last.typ == 0 {
|
||||
l.last = <-l.items
|
||||
}
|
||||
|
||||
return l.last
|
||||
}
|
||||
|
||||
func (l *lexer) Consume() {
|
||||
l.last = <-l.items
|
||||
}
|
||||
|
||||
// error returns an error token and terminates the scan
|
||||
// by passing back a nil pointer that will be the next
|
||||
// state, terminating l.run.
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
l.items <- item{
|
||||
itemError,
|
||||
fmt.Sprintf(format, args...),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lexMain(l *lexer) stateFn {
|
||||
switch r := l.next(); {
|
||||
case r == eof:
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
case unicode.IsSpace(r):
|
||||
l.ignore()
|
||||
case r == '(':
|
||||
l.emit(itemLeftParen)
|
||||
case r == ')':
|
||||
l.emit(itemRightParen)
|
||||
case r == '|':
|
||||
l.emit(itemOr)
|
||||
case r == ',':
|
||||
l.emit(itemAnd)
|
||||
case r == '!':
|
||||
l.emit(itemNot)
|
||||
case r == '<':
|
||||
r2 := l.next()
|
||||
if r2 == '<' {
|
||||
l.emit(itemLt)
|
||||
} else if r2 == '=' {
|
||||
l.emit(itemLtEq)
|
||||
} else {
|
||||
l.backup()
|
||||
l.emit(itemLtEq)
|
||||
}
|
||||
case r == '>':
|
||||
r2 := l.next()
|
||||
if r2 == '>' {
|
||||
l.emit(itemGt)
|
||||
} else if r2 == '=' {
|
||||
l.emit(itemGtEq)
|
||||
} else {
|
||||
l.backup()
|
||||
l.emit(itemGtEq)
|
||||
}
|
||||
case r == '=':
|
||||
l.emit(itemEq)
|
||||
case r == '%':
|
||||
l.emit(itemPatMatch)
|
||||
case r == '~':
|
||||
l.emit(itemRegexp)
|
||||
default:
|
||||
l.backup()
|
||||
return lexString
|
||||
}
|
||||
|
||||
return lexMain
|
||||
}
|
||||
|
||||
func lexString(l *lexer) stateFn {
|
||||
for {
|
||||
r := l.next()
|
||||
if unicode.IsSpace(r) || strings.IndexRune("()|,!<>=%~", r) > 0 {
|
||||
l.backup()
|
||||
l.emit(itemString)
|
||||
return lexMain(l)
|
||||
}
|
||||
|
||||
if r == eof {
|
||||
l.emit(itemString)
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
46
query/lex_test.go
Normal file
46
query/lex_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "launchpad.net/gocheck"
|
||||
)
|
||||
|
||||
type LexerSuite struct {
|
||||
}
|
||||
|
||||
var _ = Suite(&LexerSuite{})
|
||||
|
||||
func (s *LexerSuite) TestLexing(c *C) {
|
||||
_, ch := lex("query", "package (<< 1.3), $Source | !app")
|
||||
|
||||
c.Check(<-ch, Equals, item{typ: itemString, val: "package"})
|
||||
c.Check(<-ch, Equals, item{typ: itemLeftParen, val: "("})
|
||||
c.Check(<-ch, Equals, item{typ: itemLt, val: "<<"})
|
||||
c.Check(<-ch, Equals, item{typ: itemString, val: "1.3"})
|
||||
c.Check(<-ch, Equals, item{typ: itemRightParen, val: ")"})
|
||||
c.Check(<-ch, Equals, item{typ: itemAnd, val: ","})
|
||||
c.Check(<-ch, Equals, item{typ: itemString, val: "$Source"})
|
||||
c.Check(<-ch, Equals, item{typ: itemOr, val: "|"})
|
||||
c.Check(<-ch, Equals, item{typ: itemNot, val: "!"})
|
||||
c.Check(<-ch, Equals, item{typ: itemString, val: "app"})
|
||||
c.Check(<-ch, Equals, item{typ: itemEOF, val: ""})
|
||||
}
|
||||
|
||||
func (s *LexerSuite) TestConsume(c *C) {
|
||||
l, _ := lex("query", "package (<< 1.3)")
|
||||
|
||||
c.Check(l.Current(), Equals, item{typ: itemString, val: "package"})
|
||||
c.Check(l.Current(), Equals, item{typ: itemString, val: "package"})
|
||||
l.Consume()
|
||||
c.Check(l.Current(), Equals, item{typ: itemLeftParen, val: "("})
|
||||
l.Consume()
|
||||
c.Check(l.Current(), Equals, item{typ: itemLt, val: "<<"})
|
||||
}
|
||||
|
||||
func (s *LexerSuite) TestString(c *C) {
|
||||
l, _ := lex("query", "package (<< 1.3)")
|
||||
|
||||
c.Check(fmt.Sprintf("%s", l.Current()), Equals, "\"package\"")
|
||||
l.Consume()
|
||||
c.Check(fmt.Sprintf("%s", l.Current()), Equals, "(")
|
||||
}
|
||||
17
query/query.go
Normal file
17
query/query.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Package query implements query language for
|
||||
package query
|
||||
|
||||
/*
|
||||
|
||||
Query language resembling Debian dependencies and reprepro
|
||||
queries: http://mirrorer.alioth.debian.org/reprepro.1.html
|
||||
|
||||
Query := A | A '|' Query
|
||||
A := B | B ',' A
|
||||
B := C | '!' B
|
||||
C := '(' Query ')' | D
|
||||
D := <field> <condition>
|
||||
field := <package-name> | <field> | $special_field
|
||||
condition := '(' <operator> value ')' |
|
||||
operator := | << | < | <= | > | >> | >= | = | % | ~
|
||||
*/
|
||||
11
query/query_test.go
Normal file
11
query/query_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
. "launchpad.net/gocheck"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Launch gocheck tests
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
164
query/syntax.go
Normal file
164
query/syntax.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type parser struct {
|
||||
name string // used only for error reports.
|
||||
input *lexer // the input lexer
|
||||
err error // error stored while parsing
|
||||
}
|
||||
|
||||
func parse(input *lexer) (PackageQuery, error) {
|
||||
p := &parser{
|
||||
name: input.name,
|
||||
input: input,
|
||||
}
|
||||
query := p.parse()
|
||||
if p.err != nil {
|
||||
return nil, p.err
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// Entry into parser
|
||||
func (p *parser) parse() PackageQuery {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
p.err = fmt.Errorf("parsing failed: %s", r)
|
||||
}
|
||||
}()
|
||||
|
||||
q := p.Query()
|
||||
if p.input.Current().typ != itemEOF {
|
||||
panic(fmt.Sprintf("unexpected token %s: expecting end of query", p.input.Current()))
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// Query := A | A '|' Query
|
||||
func (p *parser) Query() PackageQuery {
|
||||
q := p.A()
|
||||
if p.input.Current().typ == itemOr {
|
||||
p.input.Consume()
|
||||
return &OrQuery{L: q, R: p.Query()}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// A := B | B ',' A
|
||||
func (p *parser) A() PackageQuery {
|
||||
q := p.B()
|
||||
if p.input.Current().typ == itemAnd {
|
||||
p.input.Consume()
|
||||
return &AndQuery{L: q, R: p.A()}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// B := C | '!' B
|
||||
func (p *parser) B() PackageQuery {
|
||||
if p.input.Current().typ == itemNot {
|
||||
p.input.Consume()
|
||||
return &NotQuery{Q: p.B()}
|
||||
}
|
||||
return p.C()
|
||||
}
|
||||
|
||||
// C := '(' Query ')' | D
|
||||
func (p *parser) C() PackageQuery {
|
||||
if p.input.Current().typ == itemLeftParen {
|
||||
p.input.Consume()
|
||||
q := p.Query()
|
||||
if p.input.Current().typ != itemRightParen {
|
||||
panic(fmt.Sprintf("unexpected token %s: expecting ')'", p.input.Current()))
|
||||
}
|
||||
p.input.Consume()
|
||||
return q
|
||||
}
|
||||
return p.D()
|
||||
}
|
||||
|
||||
func operatorToRelation(operator itemType) int {
|
||||
switch operator {
|
||||
case 0:
|
||||
return deb.VersionDontCare
|
||||
case itemLt:
|
||||
return deb.VersionLess
|
||||
case itemLtEq:
|
||||
return deb.VersionLessOrEqual
|
||||
case itemGt:
|
||||
return deb.VersionGreater
|
||||
case itemGtEq:
|
||||
return deb.VersionGreaterOrEqual
|
||||
case itemEq:
|
||||
return deb.VersionEqual
|
||||
case itemPatMatch:
|
||||
return deb.VersionPatternMatch
|
||||
case itemRegexp:
|
||||
return deb.VersionRegexp
|
||||
}
|
||||
panic("unable to map token to relation")
|
||||
}
|
||||
|
||||
// D := <field> <condition>
|
||||
// field := <package-name> | <field> | $special_field
|
||||
func (p *parser) D() PackageQuery {
|
||||
if p.input.Current().typ != itemString {
|
||||
panic(fmt.Sprintf("unexpected token %s: expecting field or package name", p.input.Current()))
|
||||
}
|
||||
|
||||
field := p.input.Current().val
|
||||
p.input.Consume()
|
||||
|
||||
operator, value := p.Condition()
|
||||
|
||||
r, _ := utf8.DecodeRuneInString(field)
|
||||
if strings.HasPrefix(field, "$") || unicode.IsUpper(r) {
|
||||
// special field or regular field
|
||||
return &FieldQuery{Field: field, Relation: operatorToRelation(operator), Value: value}
|
||||
}
|
||||
|
||||
// regular dependency-like query
|
||||
return &DependencyQuery{Dep: deb.Dependency{Pkg: field, Relation: operatorToRelation(operator), Version: value}}
|
||||
}
|
||||
|
||||
// condition := '(' <operator> value ')' |
|
||||
// operator := | << | < | <= | > | >> | >= | = | % | ~
|
||||
func (p *parser) Condition() (operator itemType, value string) {
|
||||
if p.input.Current().typ != itemLeftParen {
|
||||
return
|
||||
}
|
||||
p.input.Consume()
|
||||
|
||||
if p.input.Current().typ == itemLt ||
|
||||
p.input.Current().typ == itemGt ||
|
||||
p.input.Current().typ == itemLtEq ||
|
||||
p.input.Current().typ == itemGtEq ||
|
||||
p.input.Current().typ == itemEq ||
|
||||
p.input.Current().typ == itemPatMatch ||
|
||||
p.input.Current().typ == itemRegexp {
|
||||
operator = p.input.Current().typ
|
||||
p.input.Consume()
|
||||
} else {
|
||||
operator = itemEq
|
||||
}
|
||||
|
||||
if p.input.Current().typ != itemString {
|
||||
panic(fmt.Sprintf("unexpected token %s: expecting value", p.input.Current()))
|
||||
}
|
||||
value = p.input.Current().val
|
||||
p.input.Consume()
|
||||
|
||||
if p.input.Current().typ != itemRightParen {
|
||||
panic(fmt.Sprintf("unexpected token %s: expecting ')'", p.input.Current()))
|
||||
}
|
||||
p.input.Consume()
|
||||
|
||||
return
|
||||
}
|
||||
64
query/syntax_test.go
Normal file
64
query/syntax_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/smira/aptly/deb"
|
||||
. "launchpad.net/gocheck"
|
||||
)
|
||||
|
||||
type SyntaxSuite struct {
|
||||
}
|
||||
|
||||
var _ = Suite(&SyntaxSuite{})
|
||||
|
||||
func (s *SyntaxSuite) TestParsing(c *C) {
|
||||
l, _ := lex("query", "package (<< 1.3), $Source")
|
||||
q, err := parse(l)
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(q.(*AndQuery).L, DeepEquals, &DependencyQuery{Dep: deb.Dependency{Pkg: "package", Relation: deb.VersionLess, Version: "1.3"}})
|
||||
c.Check(q.(*AndQuery).R, DeepEquals, &FieldQuery{Field: "$Source"})
|
||||
|
||||
l, _ = lex("query", "package (1.3), Name (lala) | !$Source")
|
||||
q, err = parse(l)
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(q.(*OrQuery).L.(*AndQuery).L, DeepEquals, &DependencyQuery{Dep: deb.Dependency{Pkg: "package", Relation: deb.VersionEqual, Version: "1.3"}})
|
||||
c.Check(q.(*OrQuery).L.(*AndQuery).R, DeepEquals, &FieldQuery{Field: "Name", Relation: deb.VersionEqual, Value: "lala"})
|
||||
c.Check(q.(*OrQuery).R.(*NotQuery).Q, DeepEquals, &FieldQuery{Field: "$Source"})
|
||||
|
||||
l, _ = lex("query", "package, ((!(Name | $Source (~ a.*))))")
|
||||
q, err = parse(l)
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(q.(*AndQuery).L, DeepEquals, &DependencyQuery{Dep: deb.Dependency{Pkg: "package", Relation: deb.VersionDontCare}})
|
||||
c.Check(q.(*AndQuery).R.(*NotQuery).Q.(*OrQuery).L, DeepEquals, &FieldQuery{Field: "Name", Relation: deb.VersionDontCare})
|
||||
c.Check(q.(*AndQuery).R.(*NotQuery).Q.(*OrQuery).R, DeepEquals, &FieldQuery{Field: "$Source", Relation: deb.VersionRegexp, Value: "a.*"})
|
||||
|
||||
l, _ = lex("query", "package (> 5.3.7)")
|
||||
q, err = parse(l)
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(q, DeepEquals, &DependencyQuery{Dep: deb.Dependency{Pkg: "package", Relation: deb.VersionGreaterOrEqual, Version: "5.3.7"}})
|
||||
}
|
||||
|
||||
func (s *SyntaxSuite) TestParsingErrors(c *C) {
|
||||
l, _ := lex("query", "package (> 5.3.7), ")
|
||||
_, err := parse(l)
|
||||
c.Check(err, ErrorMatches, "parsing failed: unexpected token <EOL>: expecting field or package name")
|
||||
|
||||
l, _ = lex("query", "package>5.3.7)")
|
||||
_, err = parse(l)
|
||||
c.Check(err, ErrorMatches, "parsing failed: unexpected token >: expecting end of query")
|
||||
|
||||
l, _ = lex("query", "package | !|")
|
||||
_, err = parse(l)
|
||||
c.Check(err, ErrorMatches, "parsing failed: unexpected token |: expecting field or package name")
|
||||
|
||||
l, _ = lex("query", "((package )")
|
||||
_, err = parse(l)
|
||||
c.Check(err, ErrorMatches, "parsing failed: unexpected token <EOL>: expecting '\\)'")
|
||||
|
||||
l, _ = lex("query", "!package )")
|
||||
_, err = parse(l)
|
||||
c.Check(err, ErrorMatches, "parsing failed: unexpected token \\): expecting end of query")
|
||||
}
|
||||
62
query/tree.go
Normal file
62
query/tree.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/smira/aptly/deb"
|
||||
)
|
||||
|
||||
// PackageQuery is interface of predicate on Package
|
||||
type PackageQuery interface {
|
||||
Matches(pkg *deb.Package) bool
|
||||
}
|
||||
|
||||
// OrQuery is L | R
|
||||
type OrQuery struct {
|
||||
L, R PackageQuery
|
||||
}
|
||||
|
||||
// AndQuery is L , R
|
||||
type AndQuery struct {
|
||||
L, R PackageQuery
|
||||
}
|
||||
|
||||
// NotQuery is ! Q
|
||||
type NotQuery struct {
|
||||
Q PackageQuery
|
||||
}
|
||||
|
||||
// FieldQuery is generic request against field
|
||||
type FieldQuery struct {
|
||||
Field string
|
||||
Relation int
|
||||
Value string
|
||||
}
|
||||
|
||||
// DependencyQuery is generic Debian-dependency like query
|
||||
type DependencyQuery struct {
|
||||
Dep deb.Dependency
|
||||
}
|
||||
|
||||
// Matches if any of L, R matches
|
||||
func (q *OrQuery) Matches(pkg *deb.Package) bool {
|
||||
return q.L.Matches(pkg) || q.R.Matches(pkg)
|
||||
}
|
||||
|
||||
// Matches if both of L, R matches
|
||||
func (q *AndQuery) Matches(pkg *deb.Package) bool {
|
||||
return q.L.Matches(pkg) && q.R.Matches(pkg)
|
||||
}
|
||||
|
||||
// Matches if not matches
|
||||
func (q *NotQuery) Matches(pkg *deb.Package) bool {
|
||||
return !q.Q.Matches(pkg)
|
||||
}
|
||||
|
||||
// Matches on generic field
|
||||
func (q *FieldQuery) Matches(pkg *deb.Package) bool {
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
// Matches on dependency condition
|
||||
func (q *DependencyQuery) Matches(pkg *deb.Package) bool {
|
||||
return pkg.MatchesDependency(q.Dep)
|
||||
}
|
||||
Reference in New Issue
Block a user