Home Download Docs Code Community
     1	/*
     2	Copyright 2014 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	// These are the search-atom definitions (see expr.go).
    18	
    19	package search
    20	
    21	import (
    22		"bytes"
    23		"context"
    24		"encoding/json"
    25		"errors"
    26		"fmt"
    27		"regexp"
    28		"strconv"
    29		"strings"
    30		"time"
    31	
    32		"go4.org/types"
    33		"perkeep.org/internal/geocode"
    34		"perkeep.org/pkg/schema/nodeattr"
    35	)
    36	
    37	const base = "0000-01-01T00:00:00Z"
    38	
    39	var (
    40		// used for width/height ranges. 10 is max length of 32-bit
    41		// int (strconv.Atoi on 32-bit platforms), even though a max
    42		// JPEG dimension is only 16-bit.
    43		whRangeExpr = regexp.MustCompile(`^(\d{0,10})-(\d{0,10})$`)
    44		whValueExpr = regexp.MustCompile(`^(\d{1,10})$`)
    45	)
    46	
    47	// Atoms holds the parsed words of an atom without the colons.
    48	// Eg. tag:holiday becomes atom{"tag", []string{"holiday"}}
    49	// Note that the form of camlisearch atoms implies that len(args) > 0
    50	type atom struct {
    51		predicate string
    52		args      []string
    53	}
    54	
    55	func (a atom) String() string {
    56		s := bytes.NewBufferString(a.predicate)
    57		for _, a := range a.args {
    58			s.WriteRune(':')
    59			s.WriteString(a)
    60		}
    61		return s.String()
    62	}
    63	
    64	// Keyword determines by its matcher when a predicate is used.
    65	type keyword interface {
    66		// Name is the part before the first colon, or the whole atom.
    67		Name() string
    68		// Description provides user documentation for this keyword.  Should
    69		// return documentation for max/min values, usage help, or examples.
    70		Description() string
    71		// Match gets called with the predicate and arguments that were parsed.
    72		// It should return true if it wishes to handle this search atom.
    73		// An error if the number of arguments mismatches.
    74		Match(a atom) (bool, error)
    75		// Predicates will be called with the args array from an atom instance.
    76		// Note that len(args) > 0 (see atom-struct comment above).
    77		// It should return a pointer to a Constraint object, expressing the meaning of
    78		// its keyword.
    79		Predicate(ctx context.Context, args []string) (*Constraint, error)
    80	}
    81	
    82	var keywords []keyword
    83	
    84	// RegisterKeyword registers search atom types.
    85	// TODO (sls) Export for applications? (together with keyword and atom)
    86	func registerKeyword(k keyword) {
    87		keywords = append(keywords, k)
    88	}
    89	
    90	// SearchHelp returns JSON of an array of predicate names and descriptions.
    91	func SearchHelp() string {
    92		type help struct{ Name, Description string }
    93		h := []help{}
    94		for _, p := range keywords {
    95			h = append(h, help{p.Name(), p.Description()})
    96		}
    97		b, err := json.MarshalIndent(h, "", "  ")
    98		if err != nil {
    99			return "Error marshalling"
   100		}
   101		return string(b)
   102	}
   103	
   104	func init() {
   105		// Core predicates
   106		registerKeyword(newAfter())
   107		registerKeyword(newBefore())
   108		registerKeyword(newAttribute())
   109		registerKeyword(newChildrenOf())
   110		registerKeyword(newParentOf())
   111		registerKeyword(newFormat())
   112		registerKeyword(newTag())
   113		registerKeyword(newTitle())
   114		registerKeyword(newRef())
   115	
   116		// Image predicates
   117		registerKeyword(newIsImage())
   118		registerKeyword(newHeight())
   119		registerKeyword(newIsLandscape())
   120		registerKeyword(newIsPano())
   121		registerKeyword(newIsPortait())
   122		registerKeyword(newWidth())
   123	
   124		// File predicates
   125		registerKeyword(newFilename())
   126	
   127		// Custom predicates
   128		registerKeyword(newIsPost())
   129		registerKeyword(newIsLike())
   130		registerKeyword(newIsCheckin())
   131		registerKeyword(newIsUntagged())
   132	
   133		// Location predicates
   134		registerKeyword(newHasLocation())
   135		registerKeyword(newNamedLocation())
   136		registerKeyword(newLocation())
   137	
   138		// People predicates
   139		registerKeyword(newWith())
   140	}
   141	
   142	// Helper implementation for mixing into keyword implementations
   143	// that match the full keyword, i.e. 'is:pano'
   144	type matchEqual string
   145	
   146	func (me matchEqual) Name() string {
   147		return string(me)
   148	}
   149	
   150	func (me matchEqual) Match(a atom) (bool, error) {
   151		return string(me) == a.String(), nil
   152	}
   153	
   154	// Helper implementation for mixing into keyword implementations
   155	// that match only the beginning of the keyword, and get their parameters from
   156	// the rest, i.e. 'width:' for searches like 'width:100-200'.
   157	type matchPrefix struct {
   158		prefix string
   159		count  int
   160	}
   161	
   162	func newMatchPrefix(p string) matchPrefix {
   163		return matchPrefix{prefix: p, count: 1}
   164	}
   165	
   166	func (mp matchPrefix) Name() string {
   167		return mp.prefix
   168	}
   169	func (mp matchPrefix) Match(a atom) (bool, error) {
   170		if mp.prefix == a.predicate {
   171			if len(a.args) != mp.count {
   172				return true, fmt.Errorf("Wrong number of arguments for %q, given %d, expected %d", mp.prefix, len(a.args), mp.count)
   173			}
   174			return true, nil
   175		}
   176		return false, nil
   177	}
   178	
   179	// Core predicates
   180	
   181	type after struct {
   182		matchPrefix
   183	}
   184	
   185	func newAfter() keyword {
   186		return after{newMatchPrefix("after")}
   187	}
   188	
   189	func (a after) Description() string {
   190		return "date format is RFC3339, but can be shortened as required.\n" +
   191			"i.e. 2011-01-01 is Jan 1 of year 2011 and \"2011\" means the same."
   192	}
   193	
   194	func (a after) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   195		t, err := parseTimePrefix(args[0])
   196		if err != nil {
   197			return nil, err
   198		}
   199		tc := &TimeConstraint{}
   200		tc.After = types.Time3339(t)
   201		c := &Constraint{
   202			Permanode: &PermanodeConstraint{
   203				Time: tc,
   204			},
   205		}
   206		return c, nil
   207	}
   208	
   209	type before struct {
   210		matchPrefix
   211	}
   212	
   213	func newBefore() keyword {
   214		return before{newMatchPrefix("before")}
   215	}
   216	
   217	func (b before) Description() string {
   218		return "date format is RFC3339, but can be shortened as required.\n" +
   219			"i.e. 2011-01-01 is Jan 1 of year 2011 and \"2011\" means the same."
   220	}
   221	
   222	func (b before) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   223		t, err := parseTimePrefix(args[0])
   224		if err != nil {
   225			return nil, err
   226		}
   227		tc := &TimeConstraint{}
   228		tc.Before = types.Time3339(t)
   229		c := &Constraint{
   230			Permanode: &PermanodeConstraint{
   231				Time: tc,
   232			},
   233		}
   234		return c, nil
   235	}
   236	
   237	type attribute struct {
   238		matchPrefix
   239	}
   240	
   241	func newAttribute() keyword {
   242		return attribute{matchPrefix{"attr", 2}}
   243	}
   244	
   245	func (a attribute) Description() string {
   246		return "match on attribute. Use attr:foo:bar to match nodes having their foo\n" +
   247			"attribute set to bar or attr:foo:~bar to do a substring\n" +
   248			"case-insensitive search for 'bar' in attribute foo"
   249	}
   250	
   251	func (a attribute) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   252		c := permWithAttr(args[0], args[1])
   253		if strings.HasPrefix(args[1], "~") {
   254			// Substring. Hack. Figure out better way to do this.
   255			c.Permanode.Value = ""
   256			c.Permanode.ValueMatches = &StringConstraint{
   257				Contains:        args[1][1:],
   258				CaseInsensitive: true,
   259			}
   260		}
   261		return c, nil
   262	}
   263	
   264	type childrenOf struct {
   265		matchPrefix
   266	}
   267	
   268	func newChildrenOf() keyword {
   269		return childrenOf{newMatchPrefix("childrenof")}
   270	}
   271	
   272	func (k childrenOf) Description() string {
   273		return "Find child permanodes of a parent permanode (or prefix of a parent\n" +
   274			"permanode): childrenof:sha1-527cf12 Only matches permanodes currently."
   275	}
   276	
   277	func (k childrenOf) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   278		c := &Constraint{
   279			Permanode: &PermanodeConstraint{
   280				Relation: &RelationConstraint{
   281					Relation: "parent",
   282					Any: &Constraint{
   283						BlobRefPrefix: args[0],
   284					},
   285				},
   286			},
   287		}
   288		return c, nil
   289	}
   290	
   291	type parentOf struct {
   292		matchPrefix
   293	}
   294	
   295	func newParentOf() keyword {
   296		return parentOf{newMatchPrefix("parentof")}
   297	}
   298	
   299	func (k parentOf) Description() string {
   300		return "Find parent permanodes of a child permanode (or prefix of a child\n" +
   301			"permanode): parentof:sha1-527cf12 Only matches permanodes currently."
   302	}
   303	
   304	func (k parentOf) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   305		c := &Constraint{
   306			Permanode: &PermanodeConstraint{
   307				Relation: &RelationConstraint{
   308					Relation: "child",
   309					Any: &Constraint{
   310						BlobRefPrefix: args[0],
   311					},
   312				},
   313			},
   314		}
   315		return c, nil
   316	}
   317	
   318	type format struct {
   319		matchPrefix
   320	}
   321	
   322	func newFormat() keyword {
   323		return format{newMatchPrefix("format")}
   324	}
   325	
   326	func (f format) Description() string {
   327		return "file's format (or MIME-type) such as jpg, pdf, tiff."
   328	}
   329	
   330	func (f format) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   331		mimeType, err := mimeFromFormat(args[0])
   332		if err != nil {
   333			return nil, err
   334		}
   335		c := permOfFile(&FileConstraint{
   336			MIMEType: &StringConstraint{
   337				Equals: mimeType,
   338			},
   339		})
   340		return c, nil
   341	}
   342	
   343	type tag struct {
   344		matchPrefix
   345	}
   346	
   347	func newTag() keyword {
   348		return tag{newMatchPrefix("tag")}
   349	}
   350	
   351	func (t tag) Description() string {
   352		return "match on a tag"
   353	}
   354	
   355	func (t tag) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   356		return permWithAttr("tag", args[0]), nil
   357	}
   358	
   359	type with struct {
   360		matchPrefix
   361	}
   362	
   363	func newWith() keyword {
   364		return with{newMatchPrefix("with")}
   365	}
   366	
   367	func (w with) Description() string {
   368		return "match people containing substring in their first or last name"
   369	}
   370	
   371	func (w with) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   372		// TODO(katepek): write a query optimizer or a separate matcher
   373		c := &Constraint{
   374			// TODO(katepek): Does this work with repeated values for "with"?
   375			// Select all permanodes where attribute "with" points to permanodes with the foursquare person type
   376			// and with first or last name partially matching the query string
   377			Permanode: &PermanodeConstraint{
   378				Attr: "with",
   379				ValueInSet: andConst(
   380					&Constraint{
   381						Permanode: &PermanodeConstraint{
   382							Attr:  nodeattr.Type,
   383							Value: "foursquare.com:person",
   384						},
   385					},
   386					orConst(
   387						permWithAttrSubstr(nodeattr.GivenName, &StringConstraint{
   388							Contains:        args[0],
   389							CaseInsensitive: true,
   390						}),
   391						permWithAttrSubstr(nodeattr.FamilyName, &StringConstraint{
   392							Contains:        args[0],
   393							CaseInsensitive: true,
   394						}),
   395					),
   396				),
   397			},
   398		}
   399		return c, nil
   400	}
   401	
   402	type title struct {
   403		matchPrefix
   404	}
   405	
   406	func newTitle() keyword {
   407		return title{newMatchPrefix("title")}
   408	}
   409	
   410	func (t title) Description() string {
   411		return "match nodes containing substring in their title"
   412	}
   413	
   414	func (t title) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   415		c := &Constraint{
   416			Permanode: &PermanodeConstraint{
   417				Attr:       nodeattr.Title,
   418				SkipHidden: true,
   419				ValueMatches: &StringConstraint{
   420					Contains:        args[0],
   421					CaseInsensitive: true,
   422				},
   423			},
   424		}
   425		return c, nil
   426	}
   427	
   428	type ref struct {
   429		matchPrefix
   430	}
   431	
   432	func newRef() keyword {
   433		return ref{newMatchPrefix("ref")}
   434	}
   435	
   436	func (r ref) Description() string {
   437		return "match nodes whose blobRef starts with the given substring"
   438	}
   439	
   440	func (r ref) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   441		return &Constraint{
   442			BlobRefPrefix: args[0],
   443		}, nil
   444	}
   445	
   446	// Image predicates
   447	
   448	type isImage struct {
   449		matchEqual
   450	}
   451	
   452	func newIsImage() keyword {
   453		return isImage{"is:image"}
   454	}
   455	
   456	func (k isImage) Description() string {
   457		return "object is an image"
   458	}
   459	
   460	func (k isImage) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   461		c := &Constraint{
   462			Permanode: &PermanodeConstraint{
   463				Attr: nodeattr.CamliContent,
   464				ValueInSet: &Constraint{
   465					File: &FileConstraint{
   466						IsImage: true,
   467					},
   468				},
   469			},
   470		}
   471		return c, nil
   472	}
   473	
   474	type isLandscape struct {
   475		matchEqual
   476	}
   477	
   478	func newIsLandscape() keyword {
   479		return isLandscape{"is:landscape"}
   480	}
   481	
   482	func (k isLandscape) Description() string {
   483		return "the image has a landscape aspect"
   484	}
   485	
   486	func (k isLandscape) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   487		return whRatio(&FloatConstraint{Min: 1.0}), nil
   488	}
   489	
   490	type isPano struct {
   491		matchEqual
   492	}
   493	
   494	func newIsPano() keyword {
   495		return isPano{"is:pano"}
   496	}
   497	
   498	func (k isPano) Description() string {
   499		return "the image's aspect ratio is over 2 - panorama picture."
   500	}
   501	
   502	func (k isPano) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   503		return whRatio(&FloatConstraint{Min: 2.0}), nil
   504	}
   505	
   506	type isPortait struct {
   507		matchEqual
   508	}
   509	
   510	func newIsPortait() keyword {
   511		return isPortait{"is:portrait"}
   512	}
   513	
   514	func (k isPortait) Description() string {
   515		return "the image has a portrait aspect"
   516	}
   517	
   518	func (k isPortait) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   519		return whRatio(&FloatConstraint{Max: 1.0}), nil
   520	}
   521	
   522	type width struct {
   523		matchPrefix
   524	}
   525	
   526	func newWidth() keyword {
   527		return width{newMatchPrefix("width")}
   528	}
   529	
   530	func (w width) Description() string {
   531		return "use width:min-max to match images having a width of at least min\n" +
   532			"and at most max. Use width:min- to specify only an underbound and\n" +
   533			"width:-max to specify only an upperbound.\n" +
   534			"Exact matches should use width:640 "
   535	}
   536	
   537	func (w width) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   538		mins, maxs, err := parseWHExpression(args[0])
   539		if err != nil {
   540			return nil, err
   541		}
   542		c := permOfFile(&FileConstraint{
   543			IsImage: true,
   544			Width:   whIntConstraint(mins, maxs),
   545		})
   546		return c, nil
   547	}
   548	
   549	type height struct {
   550		matchPrefix
   551	}
   552	
   553	func newHeight() keyword {
   554		return height{newMatchPrefix("height")}
   555	}
   556	
   557	func (h height) Description() string {
   558		return "use height:min-max to match images having a height of at least min\n" +
   559			"and at most max. Use height:min- to specify only an underbound and\n" +
   560			"height:-max to specify only an upperbound.\n" +
   561			"Exact matches should use height:480"
   562	}
   563	
   564	func (h height) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   565		mins, maxs, err := parseWHExpression(args[0])
   566		if err != nil {
   567			return nil, err
   568		}
   569		c := permOfFile(&FileConstraint{
   570			IsImage: true,
   571			Height:  whIntConstraint(mins, maxs),
   572		})
   573		return c, nil
   574	}
   575	
   576	// Location predicates
   577	
   578	// namedLocation matches e.g. `loc:Paris` or `loc:"New York, New York"` queries.
   579	type namedLocation struct {
   580		matchPrefix
   581	}
   582	
   583	func newNamedLocation() keyword {
   584		return namedLocation{newMatchPrefix("loc")}
   585	}
   586	
   587	func (l namedLocation) Description() string {
   588		return "matches images and permanodes having a location near\n" +
   589			"the specified location.  Locations are resolved using\n" +
   590			"maps.googleapis.com. For example: loc:\"new york, new york\" "
   591	}
   592	
   593	func locationPredicate(ctx context.Context, rects []geocode.Rect) (*Constraint, error) {
   594		var c *Constraint
   595		for i, rect := range rects {
   596			loc := &LocationConstraint{
   597				West:  rect.SouthWest.Long,
   598				East:  rect.NorthEast.Long,
   599				North: rect.NorthEast.Lat,
   600				South: rect.SouthWest.Lat,
   601			}
   602			permLoc := &Constraint{
   603				Permanode: &PermanodeConstraint{
   604					Location: loc,
   605				},
   606			}
   607			if i == 0 {
   608				c = permLoc
   609			} else {
   610				c = orConst(c, permLoc)
   611			}
   612		}
   613		return c, nil
   614	}
   615	
   616	func (l namedLocation) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   617		where := args[0]
   618		rects, err := geocode.Lookup(ctx, where)
   619		if err != nil {
   620			return nil, err
   621		}
   622		if len(rects) == 0 {
   623			return nil, fmt.Errorf("No location found for %q", where)
   624		}
   625		return locationPredicate(ctx, rects)
   626	}
   627	
   628	// location matches "locrect:N,W,S,E" queries.
   629	type location struct {
   630		matchPrefix
   631	}
   632	
   633	func newLocation() keyword {
   634		return location{newMatchPrefix("locrect")}
   635	}
   636	
   637	func (l location) Description() string {
   638		return "matches images and permanodes having a location within\n" +
   639			"the specified location area. The area is defined by its\n " +
   640			"North-West corner, followed and comma-separated by its\n " +
   641			"South-East corner. Each corner is defined by its latitude,\n " +
   642			"followed and comma-separated by its longitude."
   643	}
   644	
   645	func (l location) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   646		where := args[0]
   647		coords := strings.Split(where, ",")
   648		if len(coords) != 4 {
   649			return nil, fmt.Errorf("got %d coordinates for location area, expected 4", len(coords))
   650		}
   651		asFloat := make([]float64, 4)
   652		for k, v := range coords {
   653			coo, err := strconv.ParseFloat(v, 64)
   654			if err != nil {
   655				return nil, fmt.Errorf("could not convert location area coordinate as a float: %v", err)
   656			}
   657			asFloat[k] = coo
   658		}
   659		rects := []geocode.Rect{
   660			{
   661				NorthEast: geocode.LatLong{
   662					Lat:  asFloat[0],
   663					Long: asFloat[3],
   664				},
   665				SouthWest: geocode.LatLong{
   666					Lat:  asFloat[2],
   667					Long: asFloat[1],
   668				},
   669			},
   670		}
   671		return locationPredicate(ctx, rects)
   672	}
   673	
   674	type hasLocation struct {
   675		matchEqual
   676	}
   677	
   678	func newHasLocation() keyword {
   679		return hasLocation{"has:location"}
   680	}
   681	
   682	func (h hasLocation) Description() string {
   683		return "matches images and permanodes that have a location (GPSLatitude\n" +
   684			"and GPSLongitude can be retrieved from the image's EXIF tags)."
   685	}
   686	
   687	func (h hasLocation) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   688		return &Constraint{
   689			Permanode: &PermanodeConstraint{
   690				Location: &LocationConstraint{
   691					Any: true,
   692				},
   693			},
   694		}, nil
   695	}
   696	
   697	// NamedSearch lets you use the search aliases you defined with SetNamed from the search handler.
   698	type namedSearch struct {
   699		matchPrefix
   700		sh *Handler
   701	}
   702	
   703	func newNamedSearch(sh *Handler) keyword {
   704		return namedSearch{newMatchPrefix("named"), sh}
   705	}
   706	
   707	func (n namedSearch) Description() string {
   708		return "Uses substitution of a predefined search. Set with $searchRoot/camli/search/setnamed?name=foo&substitute=attr:bar:baz" +
   709			"\nSee what the substitute is with $searchRoot/camli/search/getnamed?named=foo"
   710	}
   711	
   712	func (n namedSearch) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   713		return n.namedConstraint(args[0])
   714	}
   715	
   716	func (n namedSearch) namedConstraint(name string) (*Constraint, error) {
   717		subst, err := n.sh.getNamed(context.TODO(), name)
   718		if err != nil {
   719			return nil, err
   720		}
   721		return evalSearchInput(subst)
   722	}
   723	
   724	// Helpers
   725	
   726	func permWithAttr(attr, val string) *Constraint {
   727		c := &Constraint{
   728			Permanode: &PermanodeConstraint{
   729				Attr:       attr,
   730				SkipHidden: true,
   731			},
   732		}
   733		if val == "" {
   734			c.Permanode.ValueMatches = &StringConstraint{Empty: true}
   735		} else {
   736			c.Permanode.Value = val
   737		}
   738		return c
   739	}
   740	
   741	func permWithAttrSubstr(attr string, c *StringConstraint) *Constraint {
   742		return &Constraint{
   743			Permanode: &PermanodeConstraint{
   744				Attr:         attr,
   745				ValueMatches: c,
   746			},
   747		}
   748	}
   749	
   750	func permOfFile(fc *FileConstraint) *Constraint {
   751		return &Constraint{
   752			Permanode: &PermanodeConstraint{
   753				Attr:       nodeattr.CamliContent,
   754				ValueInSet: &Constraint{File: fc},
   755			},
   756		}
   757	}
   758	
   759	func whRatio(fc *FloatConstraint) *Constraint {
   760		return permOfFile(&FileConstraint{
   761			IsImage: true,
   762			WHRatio: fc,
   763		})
   764	}
   765	
   766	func parseWHExpression(expr string) (min, max string, err error) {
   767		if m := whRangeExpr.FindStringSubmatch(expr); m != nil {
   768			return m[1], m[2], nil
   769		}
   770		if m := whValueExpr.FindStringSubmatch(expr); m != nil {
   771			return m[1], m[1], nil
   772		}
   773		return "", "", fmt.Errorf("Unable to parse %q as range, wanted something like 480-1024, 480-, -1024 or 1024", expr)
   774	}
   775	
   776	func parseTimePrefix(when string) (time.Time, error) {
   777		if len(when) < len(base) {
   778			when += base[len(when):]
   779		}
   780		return time.Parse(time.RFC3339, when)
   781	}
   782	
   783	func whIntConstraint(mins, maxs string) *IntConstraint {
   784		ic := &IntConstraint{}
   785		if mins != "" {
   786			if mins == "0" {
   787				ic.ZeroMin = true
   788			} else {
   789				n, _ := strconv.Atoi(mins)
   790				ic.Min = int64(n)
   791			}
   792		}
   793		if maxs != "" {
   794			if maxs == "0" {
   795				ic.ZeroMax = true
   796			} else {
   797				n, _ := strconv.Atoi(maxs)
   798				ic.Max = int64(n)
   799			}
   800		}
   801		return ic
   802	}
   803	
   804	func mimeFromFormat(v string) (string, error) {
   805		if strings.Contains(v, "/") {
   806			return v, nil
   807		}
   808		switch v {
   809		case "jpg", "jpeg":
   810			return "image/jpeg", nil
   811		case "gif":
   812			return "image/gif", nil
   813		case "png":
   814			return "image/png", nil
   815		case "pdf":
   816			return "application/pdf", nil // RFC 3778
   817		}
   818		return "", fmt.Errorf("Unknown format: %s", v)
   819	}
   820	
   821	// Custom predicates
   822	
   823	type isPost struct {
   824		matchEqual
   825	}
   826	
   827	func newIsPost() keyword {
   828		return isPost{"is:post"}
   829	}
   830	
   831	func (k isPost) Description() string {
   832		return "matches tweets, status updates, blog posts, etc"
   833	}
   834	
   835	func (k isPost) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   836		return &Constraint{
   837			Permanode: &PermanodeConstraint{
   838				Attr:  nodeattr.Type,
   839				Value: "twitter.com:tweet",
   840			},
   841		}, nil
   842	}
   843	
   844	type isLike struct {
   845		matchEqual
   846	}
   847	
   848	func newIsLike() keyword {
   849		return isLike{"is:like"}
   850	}
   851	
   852	func (k isLike) Description() string {
   853		return "matches liked tweets"
   854	}
   855	
   856	func (k isLike) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   857		return &Constraint{
   858			Permanode: &PermanodeConstraint{
   859				Attr:  nodeattr.Type,
   860				Value: "twitter.com:like",
   861			},
   862		}, nil
   863	}
   864	
   865	type isCheckin struct {
   866		matchEqual
   867	}
   868	
   869	func newIsCheckin() keyword {
   870		return isCheckin{"is:checkin"}
   871	}
   872	
   873	func (k isCheckin) Description() string {
   874		return "matches location check-ins (foursquare, etc)"
   875	}
   876	
   877	func (k isCheckin) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   878		return &Constraint{
   879			Permanode: &PermanodeConstraint{
   880				Attr:  nodeattr.Type,
   881				Value: "foursquare.com:checkin",
   882			},
   883		}, nil
   884	}
   885	
   886	type isUntagged struct {
   887		matchEqual
   888	}
   889	
   890	func newIsUntagged() keyword {
   891		return isUntagged{"is:untagged"}
   892	}
   893	
   894	func (k isUntagged) Description() string {
   895		return "matches untagged permanodes"
   896	}
   897	
   898	func (k isUntagged) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   899		return &Constraint{
   900			// Note: we can't just match the Empty string constraint for the tag attribute,
   901			// because we actually want to match the absence of any tag attribute, hence below.
   902			Logical: &LogicalConstraint{
   903				Op: "not",
   904				A: &Constraint{
   905					Permanode: &PermanodeConstraint{
   906						Attr:       "tag",
   907						SkipHidden: true,
   908						ValueMatches: &StringConstraint{
   909							ByteLength: &IntConstraint{
   910								Min: 1,
   911							},
   912						},
   913					},
   914				},
   915			},
   916		}, nil
   917	}
   918	
   919	type filename struct {
   920		matchPrefix
   921	}
   922	
   923	func newFilename() keyword {
   924		return filename{newMatchPrefix("filename")}
   925	}
   926	
   927	func (fn filename) Description() string {
   928		return "Match filename, case sensitively. Supports optional '*' wildcard at beginning, end, or both."
   929	}
   930	
   931	func (fn filename) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   932		arg := args[0]
   933		switch {
   934		case !strings.Contains(arg, "*"):
   935			return permOfFile(&FileConstraint{FileName: &StringConstraint{Equals: arg}}), nil
   936		case strings.HasPrefix(arg, "*") && !strings.Contains(arg[1:], "*"):
   937			suffix := arg[1:]
   938			return permOfFile(&FileConstraint{FileName: &StringConstraint{HasSuffix: suffix}}), nil
   939		case strings.HasSuffix(arg, "*") && !strings.Contains(arg[:len(arg)-1], "*"):
   940			prefix := arg[:len(arg)-1]
   941			return permOfFile(&FileConstraint{FileName: &StringConstraint{
   942				HasPrefix: prefix,
   943			}}), nil
   944		case strings.HasSuffix(arg, "*") && strings.HasPrefix(arg, "*") && !strings.Contains(arg[1:len(arg)-1], "*"):
   945			sub := arg[1 : len(arg)-1]
   946			return permOfFile(&FileConstraint{FileName: &StringConstraint{Contains: sub}}), nil
   947		}
   948		return nil, errors.New("unsupported glob wildcard in filename search predicate")
   949	}
Website layout inspired by memcached.
Content by the authors.