kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/languageserver/pathmap/mapper.go (about)

     1  /*
     2   * Copyright 2017 The Kythe Authors. All rights reserved.
     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  // Package pathmap provides utilities for matching and generating paths
    18  // based on a pattern string.
    19  package pathmap // import "kythe.io/kythe/go/languageserver/pathmap"
    20  
    21  import (
    22  	"fmt"
    23  	"net/url"
    24  	"path/filepath"
    25  	"regexp"
    26  	"strings"
    27  )
    28  
    29  // Mapper provides path parsing and generating with named segments
    30  // See NewMapper for details on construction
    31  type Mapper struct {
    32  	// Regexp used for parsing strings to variables
    33  	re *regexp.Regexp
    34  	// Array of segments used for generating strings from variables
    35  	seg []segment
    36  }
    37  
    38  // NewMapper produces a Mapper object from a pattern string.
    39  // Patterns strings are paths that have named segments that are extracted during
    40  // parsing and populated during generation.
    41  // Example:
    42  //
    43  //			m := NewMapper("/dir/:segment/home/:rest*")
    44  //	 	s, err := m.Parse("/dir/foo/home/hello/world")	// {"segment": "foo", "rest": "hello/world"}, nil
    45  //			p := m.Generate(s) 								// /dir/foo/home/hello/world
    46  func NewMapper(pat string) (*Mapper, error) {
    47  	// All handling below assumes unix-style '/' separated paths
    48  	pat = filepath.ToSlash(pat)
    49  
    50  	// This is a sanity check to ensure that the path is not malformed.
    51  	// By prepending the / we stop url from getting confused if there's
    52  	// a leading colon
    53  	u, err := url.Parse("/" + pat)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("pattern (%s) is not a valid path: '%v'", pat, err)
    56  	}
    57  	if u.EscapedPath() != u.Path {
    58  		return nil, fmt.Errorf("pattern (%s) is requires escaping", pat)
    59  	}
    60  
    61  	var (
    62  		segments []segment
    63  		patReg   = "^"
    64  	)
    65  
    66  	for i, seg := range strings.Split(pat, "/") {
    67  		// A slash must precede any segment after the first
    68  		if i != 0 {
    69  			patReg += "/"
    70  			segments = append(segments, staticsegment("/"))
    71  		}
    72  
    73  		// If the segment is empty there's nothing else to do
    74  		if seg == "" {
    75  			continue
    76  		}
    77  
    78  		// If the segment starts with a ':' then it is a named segment
    79  		if seg[0] == ':' {
    80  			var segname string
    81  			if seg[len(seg)-1] == '*' {
    82  				segname = seg[1 : len(seg)-1]
    83  				patReg += fmt.Sprintf("(?P<%s>(?:[^/]+/)*(?:[^/]+))", segname)
    84  			} else {
    85  				segname = seg[1:]
    86  				patReg += fmt.Sprintf("(?P<%s>(?:[^/]+))", segname)
    87  			}
    88  			segments = append(segments, varsegment(segname))
    89  		} else {
    90  			segments = append(segments, staticsegment(seg))
    91  			patReg += regexp.QuoteMeta(seg)
    92  		}
    93  	}
    94  	patReg += "$"
    95  
    96  	re, err := regexp.Compile(patReg)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("error compiling regex for pattern (%s):\nRegex: %s\nError: %v", pat, patReg, err)
    99  	}
   100  
   101  	return &Mapper{
   102  		re:  re,
   103  		seg: segments,
   104  	}, nil
   105  }
   106  
   107  // Parse extracts named segments from the path provided
   108  // All segments must appear in the path
   109  func (m Mapper) Parse(path string) (map[string]string, error) {
   110  	// The regex is based on unix-style paths so we need to convert
   111  	path = filepath.ToSlash(path)
   112  
   113  	match := m.re.FindStringSubmatch(path)
   114  	if len(match) == 0 {
   115  		return nil, fmt.Errorf("path (%s) did not match regex (%v)", path, m.re)
   116  	}
   117  
   118  	out := make(map[string]string)
   119  	// The first SubexpName is "" so we skip it
   120  	for i, v := range m.re.SubexpNames()[1:] {
   121  		// The first match is the full string so add 1
   122  		out[v] = match[i+1]
   123  	}
   124  	return out, nil
   125  }
   126  
   127  // Generate produces a path from a map of segment values.
   128  // All required values must be present
   129  func (m Mapper) Generate(vars map[string]string) (string, error) {
   130  	var gen string
   131  	for _, s := range m.seg {
   132  		seg, err := s.str(vars)
   133  		if err != nil {
   134  			return "", err
   135  		}
   136  		gen += seg
   137  	}
   138  	// Convert back to local path format at the end
   139  	return filepath.FromSlash(gen), nil
   140  }
   141  
   142  type segment interface {
   143  	str(map[string]string) (string, error)
   144  }
   145  
   146  type staticsegment string
   147  
   148  func (s staticsegment) str(map[string]string) (string, error) {
   149  	return string(s), nil
   150  }
   151  
   152  type varsegment string
   153  
   154  func (v varsegment) str(p map[string]string) (string, error) {
   155  	if s, ok := p[string(v)]; ok {
   156  		return s, nil
   157  	}
   158  	return "", fmt.Errorf("path generation failure. Missing key: %s", v)
   159  }