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	
   132		// Location predicates
   133		registerKeyword(newHasLocation())
   134		registerKeyword(newNamedLocation())
   135		registerKeyword(newLocation())
   136	
   137		// People predicates
   138		registerKeyword(newWith())
   139	}
   140	
   141	// Helper implementation for mixing into keyword implementations
   142	// that match the full keyword, i.e. 'is:pano'
   143	type matchEqual string
   144	
   145	func (me matchEqual) Name() string {
   146		return string(me)
   147	}
   148	
   149	func (me matchEqual) Match(a atom) (bool, error) {
   150		return string(me) == a.String(), nil
   151	}
   152	
   153	// Helper implementation for mixing into keyword implementations
   154	// that match only the beginning of the keyword, and get their parameters from
   155	// the rest, i.e. 'width:' for searches like 'width:100-200'.
   156	type matchPrefix struct {
   157		prefix string
   158		count  int
   159	}
   160	
   161	func newMatchPrefix(p string) matchPrefix {
   162		return matchPrefix{prefix: p, count: 1}
   163	}
   164	
   165	func (mp matchPrefix) Name() string {
   166		return mp.prefix
   167	}
   168	func (mp matchPrefix) Match(a atom) (bool, error) {
   169		if mp.prefix == a.predicate {
   170			if len(a.args) != mp.count {
   171				return true, fmt.Errorf("Wrong number of arguments for %q, given %d, expected %d", mp.prefix, len(a.args), mp.count)
   172			}
   173			return true, nil
   174		}
   175		return false, nil
   176	}
   177	
   178	// Core predicates
   179	
   180	type after struct {
   181		matchPrefix
   182	}
   183	
   184	func newAfter() keyword {
   185		return after{newMatchPrefix("after")}
   186	}
   187	
   188	func (a after) Description() string {
   189		return "date format is RFC3339, but can be shortened as required.\n" +
   190			"i.e. 2011-01-01 is Jan 1 of year 2011 and \"2011\" means the same."
   191	}
   192	
   193	func (a after) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   194		t, err := parseTimePrefix(args[0])
   195		if err != nil {
   196			return nil, err
   197		}
   198		tc := &TimeConstraint{}
   199		tc.After = types.Time3339(t)
   200		c := &Constraint{
   201			Permanode: &PermanodeConstraint{
   202				Time: tc,
   203			},
   204		}
   205		return c, nil
   206	}
   207	
   208	type before struct {
   209		matchPrefix
   210	}
   211	
   212	func newBefore() keyword {
   213		return before{newMatchPrefix("before")}
   214	}
   215	
   216	func (b before) Description() string {
   217		return "date format is RFC3339, but can be shortened as required.\n" +
   218			"i.e. 2011-01-01 is Jan 1 of year 2011 and \"2011\" means the same."
   219	}
   220	
   221	func (b before) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   222		t, err := parseTimePrefix(args[0])
   223		if err != nil {
   224			return nil, err
   225		}
   226		tc := &TimeConstraint{}
   227		tc.Before = types.Time3339(t)
   228		c := &Constraint{
   229			Permanode: &PermanodeConstraint{
   230				Time: tc,
   231			},
   232		}
   233		return c, nil
   234	}
   235	
   236	type attribute struct {
   237		matchPrefix
   238	}
   239	
   240	func newAttribute() keyword {
   241		return attribute{matchPrefix{"attr", 2}}
   242	}
   243	
   244	func (a attribute) Description() string {
   245		return "match on attribute. Use attr:foo:bar to match nodes having their foo\n" +
   246			"attribute set to bar or attr:foo:~bar to do a substring\n" +
   247			"case-insensitive search for 'bar' in attribute foo"
   248	}
   249	
   250	func (a attribute) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   251		c := permWithAttr(args[0], args[1])
   252		if strings.HasPrefix(args[1], "~") {
   253			// Substring. Hack. Figure out better way to do this.
   254			c.Permanode.Value = ""
   255			c.Permanode.ValueMatches = &StringConstraint{
   256				Contains:        args[1][1:],
   257				CaseInsensitive: true,
   258			}
   259		}
   260		return c, nil
   261	}
   262	
   263	type childrenOf struct {
   264		matchPrefix
   265	}
   266	
   267	func newChildrenOf() keyword {
   268		return childrenOf{newMatchPrefix("childrenof")}
   269	}
   270	
   271	func (k childrenOf) Description() string {
   272		return "Find child permanodes of a parent permanode (or prefix of a parent\n" +
   273			"permanode): childrenof:sha1-527cf12 Only matches permanodes currently."
   274	}
   275	
   276	func (k childrenOf) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   277		c := &Constraint{
   278			Permanode: &PermanodeConstraint{
   279				Relation: &RelationConstraint{
   280					Relation: "parent",
   281					Any: &Constraint{
   282						BlobRefPrefix: args[0],
   283					},
   284				},
   285			},
   286		}
   287		return c, nil
   288	}
   289	
   290	type parentOf struct {
   291		matchPrefix
   292	}
   293	
   294	func newParentOf() keyword {
   295		return parentOf{newMatchPrefix("parentof")}
   296	}
   297	
   298	func (k parentOf) Description() string {
   299		return "Find parent permanodes of a child permanode (or prefix of a child\n" +
   300			"permanode): parentof:sha1-527cf12 Only matches permanodes currently."
   301	}
   302	
   303	func (k parentOf) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   304		c := &Constraint{
   305			Permanode: &PermanodeConstraint{
   306				Relation: &RelationConstraint{
   307					Relation: "child",
   308					Any: &Constraint{
   309						BlobRefPrefix: args[0],
   310					},
   311				},
   312			},
   313		}
   314		return c, nil
   315	}
   316	
   317	type format struct {
   318		matchPrefix
   319	}
   320	
   321	func newFormat() keyword {
   322		return format{newMatchPrefix("format")}
   323	}
   324	
   325	func (f format) Description() string {
   326		return "file's format (or MIME-type) such as jpg, pdf, tiff."
   327	}
   328	
   329	func (f format) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   330		mimeType, err := mimeFromFormat(args[0])
   331		if err != nil {
   332			return nil, err
   333		}
   334		c := permOfFile(&FileConstraint{
   335			MIMEType: &StringConstraint{
   336				Equals: mimeType,
   337			},
   338		})
   339		return c, nil
   340	}
   341	
   342	type tag struct {
   343		matchPrefix
   344	}
   345	
   346	func newTag() keyword {
   347		return tag{newMatchPrefix("tag")}
   348	}
   349	
   350	func (t tag) Description() string {
   351		return "match on a tag"
   352	}
   353	
   354	func (t tag) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   355		return permWithAttr("tag", args[0]), nil
   356	}
   357	
   358	type with struct {
   359		matchPrefix
   360	}
   361	
   362	func newWith() keyword {
   363		return with{newMatchPrefix("with")}
   364	}
   365	
   366	func (w with) Description() string {
   367		return "match people containing substring in their first or last name"
   368	}
   369	
   370	func (w with) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   371		// TODO(katepek): write a query optimizer or a separate matcher
   372		c := &Constraint{
   373			// TODO(katepek): Does this work with repeated values for "with"?
   374			// Select all permanodes where attribute "with" points to permanodes with the foursquare person type
   375			// and with first or last name partially matching the query string
   376			Permanode: &PermanodeConstraint{
   377				Attr: "with",
   378				ValueInSet: andConst(
   379					&Constraint{
   380						Permanode: &PermanodeConstraint{
   381							Attr:  nodeattr.Type,
   382							Value: "foursquare.com:person",
   383						},
   384					},
   385					orConst(
   386						permWithAttrSubstr(nodeattr.GivenName, &StringConstraint{
   387							Contains:        args[0],
   388							CaseInsensitive: true,
   389						}),
   390						permWithAttrSubstr(nodeattr.FamilyName, &StringConstraint{
   391							Contains:        args[0],
   392							CaseInsensitive: true,
   393						}),
   394					),
   395				),
   396			},
   397		}
   398		return c, nil
   399	}
   400	
   401	type title struct {
   402		matchPrefix
   403	}
   404	
   405	func newTitle() keyword {
   406		return title{newMatchPrefix("title")}
   407	}
   408	
   409	func (t title) Description() string {
   410		return "match nodes containing substring in their title"
   411	}
   412	
   413	func (t title) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   414		c := &Constraint{
   415			Permanode: &PermanodeConstraint{
   416				Attr:       nodeattr.Title,
   417				SkipHidden: true,
   418				ValueMatches: &StringConstraint{
   419					Contains:        args[0],
   420					CaseInsensitive: true,
   421				},
   422			},
   423		}
   424		return c, nil
   425	}
   426	
   427	type ref struct {
   428		matchPrefix
   429	}
   430	
   431	func newRef() keyword {
   432		return ref{newMatchPrefix("ref")}
   433	}
   434	
   435	func (r ref) Description() string {
   436		return "match nodes whose blobRef starts with the given substring"
   437	}
   438	
   439	func (r ref) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   440		return &Constraint{
   441			BlobRefPrefix: args[0],
   442		}, nil
   443	}
   444	
   445	// Image predicates
   446	
   447	type isImage struct {
   448		matchEqual
   449	}
   450	
   451	func newIsImage() keyword {
   452		return isImage{"is:image"}
   453	}
   454	
   455	func (k isImage) Description() string {
   456		return "object is an image"
   457	}
   458	
   459	func (k isImage) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   460		c := &Constraint{
   461			Permanode: &PermanodeConstraint{
   462				Attr: nodeattr.CamliContent,
   463				ValueInSet: &Constraint{
   464					File: &FileConstraint{
   465						IsImage: true,
   466					},
   467				},
   468			},
   469		}
   470		return c, nil
   471	}
   472	
   473	type isLandscape struct {
   474		matchEqual
   475	}
   476	
   477	func newIsLandscape() keyword {
   478		return isLandscape{"is:landscape"}
   479	}
   480	
   481	func (k isLandscape) Description() string {
   482		return "the image has a landscape aspect"
   483	}
   484	
   485	func (k isLandscape) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   486		return whRatio(&FloatConstraint{Min: 1.0}), nil
   487	}
   488	
   489	type isPano struct {
   490		matchEqual
   491	}
   492	
   493	func newIsPano() keyword {
   494		return isPano{"is:pano"}
   495	}
   496	
   497	func (k isPano) Description() string {
   498		return "the image's aspect ratio is over 2 - panorama picture."
   499	}
   500	
   501	func (k isPano) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   502		return whRatio(&FloatConstraint{Min: 2.0}), nil
   503	}
   504	
   505	type isPortait struct {
   506		matchEqual
   507	}
   508	
   509	func newIsPortait() keyword {
   510		return isPortait{"is:portrait"}
   511	}
   512	
   513	func (k isPortait) Description() string {
   514		return "the image has a portrait aspect"
   515	}
   516	
   517	func (k isPortait) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   518		return whRatio(&FloatConstraint{Max: 1.0}), nil
   519	}
   520	
   521	type width struct {
   522		matchPrefix
   523	}
   524	
   525	func newWidth() keyword {
   526		return width{newMatchPrefix("width")}
   527	}
   528	
   529	func (w width) Description() string {
   530		return "use width:min-max to match images having a width of at least min\n" +
   531			"and at most max. Use width:min- to specify only an underbound and\n" +
   532			"width:-max to specify only an upperbound.\n" +
   533			"Exact matches should use width:640 "
   534	}
   535	
   536	func (w width) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   537		mins, maxs, err := parseWHExpression(args[0])
   538		if err != nil {
   539			return nil, err
   540		}
   541		c := permOfFile(&FileConstraint{
   542			IsImage: true,
   543			Width:   whIntConstraint(mins, maxs),
   544		})
   545		return c, nil
   546	}
   547	
   548	type height struct {
   549		matchPrefix
   550	}
   551	
   552	func newHeight() keyword {
   553		return height{newMatchPrefix("height")}
   554	}
   555	
   556	func (h height) Description() string {
   557		return "use height:min-max to match images having a height of at least min\n" +
   558			"and at most max. Use height:min- to specify only an underbound and\n" +
   559			"height:-max to specify only an upperbound.\n" +
   560			"Exact matches should use height:480"
   561	}
   562	
   563	func (h height) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   564		mins, maxs, err := parseWHExpression(args[0])
   565		if err != nil {
   566			return nil, err
   567		}
   568		c := permOfFile(&FileConstraint{
   569			IsImage: true,
   570			Height:  whIntConstraint(mins, maxs),
   571		})
   572		return c, nil
   573	}
   574	
   575	// Location predicates
   576	
   577	// namedLocation matches e.g. `loc:Paris` or `loc:"New York, New York"` queries.
   578	type namedLocation struct {
   579		matchPrefix
   580	}
   581	
   582	func newNamedLocation() keyword {
   583		return namedLocation{newMatchPrefix("loc")}
   584	}
   585	
   586	func (l namedLocation) Description() string {
   587		return "matches images and permanodes having a location near\n" +
   588			"the specified location.  Locations are resolved using\n" +
   589			"maps.googleapis.com. For example: loc:\"new york, new york\" "
   590	}
   591	
   592	func locationPredicate(ctx context.Context, rects []geocode.Rect) (*Constraint, error) {
   593		var c *Constraint
   594		for i, rect := range rects {
   595			loc := &LocationConstraint{
   596				West:  rect.SouthWest.Long,
   597				East:  rect.NorthEast.Long,
   598				North: rect.NorthEast.Lat,
   599				South: rect.SouthWest.Lat,
   600			}
   601			permLoc := &Constraint{
   602				Permanode: &PermanodeConstraint{
   603					Location: loc,
   604				},
   605			}
   606			if i == 0 {
   607				c = permLoc
   608			} else {
   609				c = orConst(c, permLoc)
   610			}
   611		}
   612		return c, nil
   613	}
   614	
   615	func (l namedLocation) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   616		where := args[0]
   617		rects, err := geocode.Lookup(ctx, where)
   618		if err != nil {
   619			return nil, err
   620		}
   621		if len(rects) == 0 {
   622			return nil, fmt.Errorf("No location found for %q", where)
   623		}
   624		return locationPredicate(ctx, rects)
   625	}
   626	
   627	// location matches "locrect:N,W,S,E" queries.
   628	type location struct {
   629		matchPrefix
   630	}
   631	
   632	func newLocation() keyword {
   633		return location{newMatchPrefix("locrect")}
   634	}
   635	
   636	func (l location) Description() string {
   637		return "matches images and permanodes having a location within\n" +
   638			"the specified location area. The area is defined by its\n " +
   639			"North-West corner, followed and comma-separated by its\n " +
   640			"South-East corner. Each corner is defined by its latitude,\n " +
   641			"followed and comma-separated by its longitude."
   642	}
   643	
   644	func (l location) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   645		where := args[0]
   646		coords := strings.Split(where, ",")
   647		if len(coords) != 4 {
   648			return nil, fmt.Errorf("got %d coordinates for location area, expected 4", len(coords))
   649		}
   650		asFloat := make([]float64, 4)
   651		for k, v := range coords {
   652			coo, err := strconv.ParseFloat(v, 64)
   653			if err != nil {
   654				return nil, fmt.Errorf("could not convert location area coordinate as a float: %v", err)
   655			}
   656			asFloat[k] = coo
   657		}
   658		rects := []geocode.Rect{
   659			{
   660				NorthEast: geocode.LatLong{
   661					Lat:  asFloat[0],
   662					Long: asFloat[3],
   663				},
   664				SouthWest: geocode.LatLong{
   665					Lat:  asFloat[2],
   666					Long: asFloat[1],
   667				},
   668			},
   669		}
   670		return locationPredicate(ctx, rects)
   671	}
   672	
   673	type hasLocation struct {
   674		matchEqual
   675	}
   676	
   677	func newHasLocation() keyword {
   678		return hasLocation{"has:location"}
   679	}
   680	
   681	func (h hasLocation) Description() string {
   682		return "matches images and permanodes that have a location (GPSLatitude\n" +
   683			"and GPSLongitude can be retrieved from the image's EXIF tags)."
   684	}
   685	
   686	func (h hasLocation) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   687		return &Constraint{
   688			Permanode: &PermanodeConstraint{
   689				Location: &LocationConstraint{
   690					Any: true,
   691				},
   692			},
   693		}, nil
   694	}
   695	
   696	// NamedSearch lets you use the search aliases you defined with SetNamed from the search handler.
   697	type namedSearch struct {
   698		matchPrefix
   699		sh *Handler
   700	}
   701	
   702	func newNamedSearch(sh *Handler) keyword {
   703		return namedSearch{newMatchPrefix("named"), sh}
   704	}
   705	
   706	func (n namedSearch) Description() string {
   707		return "Uses substitution of a predefined search. Set with $searchRoot/camli/search/setnamed?name=foo&substitute=attr:bar:baz" +
   708			"\nSee what the substitute is with $searchRoot/camli/search/getnamed?named=foo"
   709	}
   710	
   711	func (n namedSearch) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   712		return n.namedConstraint(args[0])
   713	}
   714	
   715	func (n namedSearch) namedConstraint(name string) (*Constraint, error) {
   716		subst, err := n.sh.getNamed(context.TODO(), name)
   717		if err != nil {
   718			return nil, err
   719		}
   720		return evalSearchInput(subst)
   721	}
   722	
   723	// Helpers
   724	
   725	func permWithAttr(attr, val string) *Constraint {
   726		c := &Constraint{
   727			Permanode: &PermanodeConstraint{
   728				Attr:       attr,
   729				SkipHidden: true,
   730			},
   731		}
   732		if val == "" {
   733			c.Permanode.ValueMatches = &StringConstraint{Empty: true}
   734		} else {
   735			c.Permanode.Value = val
   736		}
   737		return c
   738	}
   739	
   740	func permWithAttrSubstr(attr string, c *StringConstraint) *Constraint {
   741		return &Constraint{
   742			Permanode: &PermanodeConstraint{
   743				Attr:         attr,
   744				ValueMatches: c,
   745			},
   746		}
   747	}
   748	
   749	func permOfFile(fc *FileConstraint) *Constraint {
   750		return &Constraint{
   751			Permanode: &PermanodeConstraint{
   752				Attr:       nodeattr.CamliContent,
   753				ValueInSet: &Constraint{File: fc},
   754			},
   755		}
   756	}
   757	
   758	func whRatio(fc *FloatConstraint) *Constraint {
   759		return permOfFile(&FileConstraint{
   760			IsImage: true,
   761			WHRatio: fc,
   762		})
   763	}
   764	
   765	func parseWHExpression(expr string) (min, max string, err error) {
   766		if m := whRangeExpr.FindStringSubmatch(expr); m != nil {
   767			return m[1], m[2], nil
   768		}
   769		if m := whValueExpr.FindStringSubmatch(expr); m != nil {
   770			return m[1], m[1], nil
   771		}
   772		return "", "", fmt.Errorf("Unable to parse %q as range, wanted something like 480-1024, 480-, -1024 or 1024", expr)
   773	}
   774	
   775	func parseTimePrefix(when string) (time.Time, error) {
   776		if len(when) < len(base) {
   777			when += base[len(when):]
   778		}
   779		return time.Parse(time.RFC3339, when)
   780	}
   781	
   782	func whIntConstraint(mins, maxs string) *IntConstraint {
   783		ic := &IntConstraint{}
   784		if mins != "" {
   785			if mins == "0" {
   786				ic.ZeroMin = true
   787			} else {
   788				n, _ := strconv.Atoi(mins)
   789				ic.Min = int64(n)
   790			}
   791		}
   792		if maxs != "" {
   793			if maxs == "0" {
   794				ic.ZeroMax = true
   795			} else {
   796				n, _ := strconv.Atoi(maxs)
   797				ic.Max = int64(n)
   798			}
   799		}
   800		return ic
   801	}
   802	
   803	func mimeFromFormat(v string) (string, error) {
   804		if strings.Contains(v, "/") {
   805			return v, nil
   806		}
   807		switch v {
   808		case "jpg", "jpeg":
   809			return "image/jpeg", nil
   810		case "gif":
   811			return "image/gif", nil
   812		case "png":
   813			return "image/png", nil
   814		case "pdf":
   815			return "application/pdf", nil // RFC 3778
   816		}
   817		return "", fmt.Errorf("Unknown format: %s", v)
   818	}
   819	
   820	// Custom predicates
   821	
   822	type isPost struct {
   823		matchEqual
   824	}
   825	
   826	func newIsPost() keyword {
   827		return isPost{"is:post"}
   828	}
   829	
   830	func (k isPost) Description() string {
   831		return "matches tweets, status updates, blog posts, etc"
   832	}
   833	
   834	func (k isPost) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   835		return &Constraint{
   836			Permanode: &PermanodeConstraint{
   837				Attr:  nodeattr.Type,
   838				Value: "twitter.com:tweet",
   839			},
   840		}, nil
   841	}
   842	
   843	type isLike struct {
   844		matchEqual
   845	}
   846	
   847	func newIsLike() keyword {
   848		return isLike{"is:like"}
   849	}
   850	
   851	func (k isLike) Description() string {
   852		return "matches liked tweets"
   853	}
   854	
   855	func (k isLike) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   856		return &Constraint{
   857			Permanode: &PermanodeConstraint{
   858				Attr:  nodeattr.Type,
   859				Value: "twitter.com:like",
   860			},
   861		}, nil
   862	}
   863	
   864	type isCheckin struct {
   865		matchEqual
   866	}
   867	
   868	func newIsCheckin() keyword {
   869		return isCheckin{"is:checkin"}
   870	}
   871	
   872	func (k isCheckin) Description() string {
   873		return "matches location check-ins (foursquare, etc)"
   874	}
   875	
   876	func (k isCheckin) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   877		return &Constraint{
   878			Permanode: &PermanodeConstraint{
   879				Attr:  nodeattr.Type,
   880				Value: "foursquare.com:checkin",
   881			},
   882		}, nil
   883	}
   884	
   885	type filename struct {
   886		matchPrefix
   887	}
   888	
   889	func newFilename() keyword {
   890		return filename{newMatchPrefix("filename")}
   891	}
   892	
   893	func (fn filename) Description() string {
   894		return "Match filename, case sensitively. Supports optional '*' wildcard at beginning, end, or both."
   895	}
   896	
   897	func (fn filename) Predicate(ctx context.Context, args []string) (*Constraint, error) {
   898		arg := args[0]
   899		switch {
   900		case !strings.Contains(arg, "*"):
   901			return permOfFile(&FileConstraint{FileName: &StringConstraint{Equals: arg}}), nil
   902		case strings.HasPrefix(arg, "*") && !strings.Contains(arg[1:], "*"):
   903			suffix := arg[1:]
   904			return permOfFile(&FileConstraint{FileName: &StringConstraint{HasSuffix: suffix}}), nil
   905		case strings.HasSuffix(arg, "*") && !strings.Contains(arg[:len(arg)-1], "*"):
   906			prefix := arg[:len(arg)-1]
   907			return permOfFile(&FileConstraint{FileName: &StringConstraint{
   908				HasPrefix: prefix,
   909			}}), nil
   910		case strings.HasSuffix(arg, "*") && strings.HasPrefix(arg, "*") && !strings.Contains(arg[1:len(arg)-1], "*"):
   911			sub := arg[1 : len(arg)-1]
   912			return permOfFile(&FileConstraint{FileName: &StringConstraint{Contains: sub}}), nil
   913		}
   914		return nil, errors.New("unsupported glob wildcard in filename search predicate")
   915	}
Website layout inspired by memcached.
Content by the authors.