github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/project/namespace.go (about)

     1  package project
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  
     7  	"github.com/ActiveState/cli/internal/fileutils"
     8  	"github.com/ActiveState/cli/internal/locale"
     9  	"github.com/ActiveState/cli/pkg/projectfile"
    10  	"github.com/go-openapi/strfmt"
    11  )
    12  
    13  // NamespaceRegex matches the org and project name in a namespace, eg. org/project
    14  const NamespaceRegex = `^([\w-_]+)\/([\w-_\.]+)(?:#([-a-fA-F0-9]*))?$`
    15  
    16  // ProjectRegex matches the project name for a namespace with omitted org.
    17  const ProjectRegex = `^([\w-_\.]+)(?:#([-a-fA-F0-9]*))?$`
    18  
    19  // Namespaced represents a project namespace of the form <org/project>
    20  type Namespaced struct {
    21  	Owner          string
    22  	Project        string
    23  	CommitID       *strfmt.UUID
    24  	AllowOmitOwner bool
    25  }
    26  
    27  type ConfigAble interface {
    28  	projectfile.ConfigGetter
    29  }
    30  
    31  func NewNamespace(owner, project, commitID string) *Namespaced {
    32  	ns := &Namespaced{
    33  		Owner:   owner,
    34  		Project: project,
    35  	}
    36  	if commitID != "" {
    37  		commitUUID := strfmt.UUID(commitID)
    38  		ns.CommitID = &commitUUID
    39  	}
    40  	return ns
    41  }
    42  
    43  // Set implements the captain argmarshaler interface.
    44  func (ns *Namespaced) Set(v string) error {
    45  	if ns == nil {
    46  		return fmt.Errorf("cannot set nil value")
    47  	}
    48  
    49  	parsedNs, err := ParseNamespace(v)
    50  	if err != nil {
    51  		if !ns.AllowOmitOwner {
    52  			return err
    53  		}
    54  		parsedNs, err = ParseProjectNoOwner(v)
    55  		if err != nil {
    56  			return err
    57  		}
    58  	}
    59  
    60  	*ns = *parsedNs
    61  	return nil
    62  }
    63  
    64  // String implements the fmt.Stringer interface.
    65  func (ns *Namespaced) String() string {
    66  	if ns == nil {
    67  		return ""
    68  	}
    69  
    70  	var sep string
    71  	if ns.IsValid() {
    72  		sep = "/"
    73  	}
    74  	return fmt.Sprintf("%s%s%s", ns.Owner, sep, ns.Project)
    75  }
    76  
    77  // Type returns the human readable type name of Namespaced.
    78  func (ns *Namespaced) Type() string {
    79  	return "namespace"
    80  }
    81  
    82  // IsValid returns whether or not the namespace is set sufficiently.
    83  func (ns *Namespaced) IsValid() bool {
    84  	return ns != nil && ns.Project != ""
    85  }
    86  
    87  // Validate returns a failure if the namespace is not valid.
    88  func (ns *Namespaced) Validate() error {
    89  	if ns == nil || !ns.IsValid() {
    90  		return locale.NewInputError("err_invalid_namespace", "", ns.String())
    91  	}
    92  	return nil
    93  }
    94  
    95  // ParseNamespace returns a valid project namespace
    96  func ParseNamespace(raw string) (*Namespaced, error) {
    97  	rx := regexp.MustCompile(NamespaceRegex)
    98  	groups := rx.FindStringSubmatch(raw)
    99  	if len(groups) < 3 {
   100  		return nil, locale.NewInputError("err_invalid_namespace", "", raw)
   101  	}
   102  
   103  	names := Namespaced{
   104  		Owner:   groups[1],
   105  		Project: groups[2],
   106  	}
   107  
   108  	if len(groups) > 3 && len(groups[3]) > 0 {
   109  		uuidString := groups[3]
   110  		if !strfmt.IsUUID(uuidString) {
   111  			return nil, locale.NewInputError("err_invalid_commit_id", "", uuidString)
   112  		}
   113  		uuid := strfmt.UUID(uuidString)
   114  		names.CommitID = &uuid
   115  	}
   116  
   117  	return &names, nil
   118  }
   119  
   120  func ParseProjectNoOwner(raw string) (*Namespaced, error) {
   121  	rx := regexp.MustCompile(ProjectRegex)
   122  	groups := rx.FindStringSubmatch(raw)
   123  	if len(groups) < 2 {
   124  		return nil, locale.NewInputError("err_invalid_project_name", "", raw)
   125  	}
   126  
   127  	names := Namespaced{
   128  		Project:        groups[1],
   129  		AllowOmitOwner: true,
   130  	}
   131  
   132  	if len(groups) > 2 && len(groups[2]) > 0 {
   133  		uuidString := groups[2]
   134  		if !strfmt.IsUUID(uuidString) {
   135  			return nil, locale.NewInputError("err_invalid_commit_id", "", uuidString)
   136  		}
   137  		uuid := strfmt.UUID(uuidString)
   138  		names.CommitID = &uuid
   139  	}
   140  
   141  	return &names, nil
   142  }
   143  
   144  // NameSpaceForConfig returns a valid project namespace.
   145  // This version prefers to create a namespace from a configFile if it exists
   146  func NameSpaceForConfig(configFile string) *Namespaced {
   147  	if !fileutils.FileExists(configFile) {
   148  		return nil
   149  	}
   150  
   151  	prj, err := FromPath(configFile)
   152  	if err != nil {
   153  		return nil
   154  	}
   155  
   156  	names := Namespaced{
   157  		Owner:   prj.Owner(),
   158  		Project: prj.Name(),
   159  	}
   160  
   161  	commitID := strfmt.UUID(prj.LegacyCommitID()) // Not using localcommit due to import cycle. See anti-pattern comment in localcommit pkg.
   162  	if commitID != "" {
   163  		names.CommitID = &commitID
   164  	}
   165  
   166  	return &names
   167  }