github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/dsref/version_info.go (about)

     1  package dsref
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/qri-io/dataset"
    10  )
    11  
    12  // VersionInfo is an aggregation of fields from a dataset version for caching &
    13  // listing purposes. VersionInfos are typically used when showing a list of
    14  // datasets or a list of dataset versions ("qri list"). Fields on VersionInfo
    15  // are focused on being the minimum set of values required to drive user
    16  // interfaces that list datasets.
    17  //
    18  // VersionInfos can also describe dataset versions that are being created or
    19  // failed to create. In these cases the calculated VersionInfo.Path value must
    20  // always equal the empty string.
    21  //
    22  // If any fields are added to this struct, keep it in sync with:
    23  //   dscache/def.fbs       dscache
    24  //   dscache/fill_info.go  func fillInfoForDatasets
    25  //   repo/ref/convert.go   func ConvertToVersionInfo
    26  // If you are considering making major changes to VersionInfo, read this
    27  // synopsis first:
    28  //   https://github.com/qri-io/qri/pull/1641#issuecomment-778521313
    29  type VersionInfo struct {
    30  	//
    31  	// Key as a stable identifier
    32  	//
    33  	// InitID is derived from the logbook for the dataset
    34  	InitID string `json:"initID,omitempty"`
    35  	//
    36  	// Fields from dsref.Ref
    37  	//
    38  	// Username of dataset owner
    39  	Username string `json:"username,omitempty"`
    40  	// ProfileID of dataset owner
    41  	ProfileID string `json:"profileID,omitempty"`
    42  	// Unique name reference for this dataset
    43  	Name string `json:"name,omitempty"`
    44  	// Content-addressed path for this dataset
    45  	Path string `json:"path,omitempty"`
    46  	//
    47  	// State about the dataset that can change
    48  	//
    49  	// If true, this dataset has published versions
    50  	Published bool `json:"published,omitempty"`
    51  	// If true, this reference doesn't exist locally. Only makes sense if path is set, as this
    52  	// flag refers to specific versions, not to entire dataset histories.
    53  	Foreign bool `json:"foreign,omitempty"`
    54  	//
    55  	// Meta fields
    56  	//
    57  	// Title from the meta structure
    58  	MetaTitle string `json:"metaTitle,omitempty"`
    59  	// List of themes from the meta structure, comma-separated list
    60  	ThemeList string `json:"themeList,omitempty"`
    61  	//
    62  	// Structure fields
    63  	//
    64  	// Size of the body in bytes
    65  	BodySize int `json:"bodySize,omitempty"`
    66  	// Num of rows in the body
    67  	BodyRows int `json:"bodyRows,omitempty"`
    68  	// Format of the body, such as "csv" or "json"
    69  	BodyFormat string `json:"bodyFormat,omitempty"`
    70  	// Number of errors from the structure
    71  	NumErrors int `json:"numErrors,omitempty"`
    72  	//
    73  	// Commit fields
    74  	//
    75  	// Timestamp field from the commit
    76  	CommitTime time.Time `json:"commitTime,omitempty"`
    77  	// Title field from the commit
    78  	CommitTitle string `json:"commitTitle,omitempty"`
    79  	// Message field from the commit
    80  	CommitMessage string `json:"commitMessage,omitempty"`
    81  	//
    82  	//
    83  	// Workflow fields
    84  	//
    85  	WorkflowID                 string `json:"workflowID,omitempty"`
    86  	WorkflowTriggerDescription string `json:"workflowtriggerDescription,omitempty"`
    87  	//
    88  	// Run Fields
    89  	//
    90  	// RunID is derived from from either the Commit.RunID, field or the runID of a
    91  	// failed run. In the latter case the Path value will be empty
    92  	RunID string `json:"runID,omitempty"`
    93  	// RunStatus is a string version of the run.Status enumeration. This value
    94  	// will always be one of:
    95  	//    ""|"waiting"|"running"|"succeeded"|"failed"|"unchanged"|"skipped"
    96  	// RunStatus is not stored on a dataset version, and instead must come from
    97  	// either run state or a cache of run state
    98  	// it's of type string to follow the "plain old data" pattern
    99  	RunStatus string `json:"runStatus,omitempty"`
   100  	// RunDuration is how long the run took/has currently taken in nanoseconds
   101  	// default value of 0 means no duration data is available.
   102  	// RunDuration is not stored on a dataset version, and instead must come from
   103  	// either run state or logbook
   104  	RunDuration int64 `json:"runDuration,omitempty"`
   105  	// RunStart is the start time of the run. It is not stored on a dataset version
   106  	// and instead must come from either run state or logbook
   107  	RunStart *time.Time `json:"runStart,omitempty"`
   108  	//
   109  	//
   110  	// Aggregate Fields
   111  	// TODO (ramfox): These fields are only temporarily living on `VersionInfo`.
   112  	// They are needed by the frontend to display "details" about the head of
   113  	// of the dataset. When we get more user feedback and settle what info
   114  	// users want about their datasets, these fields may move to a new struct
   115  	// store, or subsystem.
   116  	// These fields are not derived from any `dataset.Dataset` fields.
   117  	// These fields should only be used in the `collection` package.
   118  	//
   119  	// RunCount is the number of times this dataset's transform has been run
   120  	RunCount int `json:"runCount,omitempty"`
   121  	// CommitCount is the number of commits in this dataset's history
   122  	CommitCount int `json:"commitCount,omitempty"`
   123  	// DownloadCount is the number of times this dataset has been directly
   124  	// downloaded from this Qri node
   125  	DownloadCount int `json:"downloadCount,omitempty"`
   126  	// FollowerCount is the number of followers this dataset has on this Qri node
   127  	FollowerCount int `json:"followerCount,omitempty"`
   128  	// OpenIssueCount is the number of open issues this dataset has on this
   129  	// Qri node
   130  	OpenIssueCount int `json:"openIssueCount,omitempty"`
   131  }
   132  
   133  // NewVersionInfoFromRef creates a sparse-populated VersionInfo from a dsref.Ref
   134  func NewVersionInfoFromRef(ref Ref) VersionInfo {
   135  	return VersionInfo{
   136  		InitID:    ref.InitID,
   137  		Username:  ref.Username,
   138  		ProfileID: ref.ProfileID,
   139  		Name:      ref.Name,
   140  		Path:      ref.Path,
   141  	}
   142  }
   143  
   144  // SimpleRef returns a simple dsref.Ref
   145  func (v VersionInfo) SimpleRef() Ref {
   146  	return Ref{
   147  		InitID:    v.InitID,
   148  		Username:  v.Username,
   149  		ProfileID: v.ProfileID,
   150  		Name:      v.Name,
   151  		Path:      v.Path,
   152  	}
   153  }
   154  
   155  // Alias returns the alias components of a Ref as a string
   156  func (v *VersionInfo) Alias() string {
   157  	s := v.Username
   158  	if v.Name != "" {
   159  		s += "/" + v.Name
   160  	}
   161  	return s
   162  }
   163  
   164  // ConvertDatasetToVersionInfo assigns values form a dataset to a VersionInfo
   165  // This function is a shim while we work on building up dscache as a store of
   166  // VersionInfo.
   167  //
   168  // Deprecated: Don't use this function for new code. Instead reference a
   169  // VersionInfo that is stored somewhere, or write a function that builds a
   170  // VersionInfo without needing a dataset
   171  func ConvertDatasetToVersionInfo(ds *dataset.Dataset) VersionInfo {
   172  	vi := VersionInfo{
   173  		InitID:    ds.ID,
   174  		Username:  ds.Peername,
   175  		ProfileID: ds.ProfileID,
   176  		Name:      ds.Name,
   177  		Path:      ds.Path,
   178  	}
   179  	if ds.Commit != nil {
   180  		vi.CommitTime = ds.Commit.Timestamp
   181  		vi.CommitTitle = ds.Commit.Title
   182  		vi.CommitMessage = ds.Commit.Message
   183  		vi.RunID = ds.Commit.RunID
   184  	}
   185  	if ds.Meta != nil {
   186  		vi.MetaTitle = ds.Meta.Title
   187  		if ds.Meta.Theme != nil {
   188  			vi.ThemeList = strings.Join(ds.Meta.Theme, ",")
   189  		}
   190  	}
   191  
   192  	if ds.Structure != nil {
   193  		vi.BodyFormat = ds.Structure.Format
   194  		vi.BodySize = ds.Structure.Length
   195  		vi.BodyRows = ds.Structure.Entries
   196  		vi.NumErrors = ds.Structure.ErrCount
   197  	}
   198  
   199  	return vi
   200  }
   201  
   202  // ConvertVersionInfoToDataset builds up a dataset from all the relevant
   203  // VersionInfo fields.
   204  //
   205  // Deprecated: This function is needed only for supporting Search functionality.
   206  // Do not add new callers if possible.
   207  func ConvertVersionInfoToDataset(info *VersionInfo) *dataset.Dataset {
   208  	return &dataset.Dataset{
   209  		Peername:  info.Username,
   210  		ProfileID: info.ProfileID,
   211  		Name:      info.Name,
   212  		Path:      info.Path,
   213  		Commit: &dataset.Commit{
   214  			Timestamp: info.CommitTime,
   215  			Title:     info.CommitTitle,
   216  			Message:   info.CommitMessage,
   217  			RunID:     info.RunID,
   218  		},
   219  		Meta: &dataset.Meta{
   220  			Title: info.MetaTitle,
   221  		},
   222  		Structure: &dataset.Structure{
   223  			Format:   info.BodyFormat,
   224  			Length:   info.BodySize,
   225  			Entries:  info.BodyRows,
   226  			ErrCount: info.NumErrors,
   227  		},
   228  	}
   229  }
   230  
   231  type lessFunc func(a, b *VersionInfo) bool
   232  
   233  func newLessFunc(key string) (lessFunc, error) {
   234  	switch key {
   235  	case "name":
   236  		return func(a, b *VersionInfo) bool {
   237  			return (a.Username < b.Username || a.Name < b.Name)
   238  		}, nil
   239  	case "size":
   240  		return func(a, b *VersionInfo) bool { return a.BodySize < b.BodySize }, nil
   241  	}
   242  
   243  	return nil, fmt.Errorf("unrecognized sorting key %q", key)
   244  }
   245  
   246  // VersionInfoAggregator sorts slices of VersionInfos according to a provided
   247  // string configuration. Call its Sort method to sort the data
   248  // TODO(b5): add support for filtering
   249  type VersionInfoAggregator struct {
   250  	infos []VersionInfo
   251  	less  []lessFunc
   252  }
   253  
   254  // Sort sorts the argument slice according to the less functions passed to OrderedBy.
   255  func (agg *VersionInfoAggregator) Sort(infos []VersionInfo) {
   256  	agg.infos = infos
   257  	sort.Sort(agg)
   258  }
   259  
   260  // NewVersionInfoAggregator returns a Sorter that sorts using the less functions
   261  // in order.
   262  func NewVersionInfoAggregator(orderBy []string) (*VersionInfoAggregator, error) {
   263  	less := []lessFunc{}
   264  	for _, o := range orderBy {
   265  		fn, err := newLessFunc(o)
   266  		if err != nil {
   267  			return nil, err
   268  		}
   269  		less = append(less, fn)
   270  	}
   271  
   272  	return &VersionInfoAggregator{
   273  		less: less,
   274  	}, nil
   275  }
   276  
   277  // Len is part of sort.Interface.
   278  func (agg *VersionInfoAggregator) Len() int {
   279  	return len(agg.infos)
   280  }
   281  
   282  // Swap is part of sort.Interface.
   283  func (agg *VersionInfoAggregator) Swap(i, j int) {
   284  	agg.infos[i], agg.infos[j] = agg.infos[j], agg.infos[i]
   285  }
   286  
   287  func (agg *VersionInfoAggregator) Less(i, j int) bool {
   288  	p, q := &agg.infos[i], &agg.infos[j]
   289  	// Try all but the last comparison.
   290  	var k int
   291  	for k = 0; k < len(agg.less)-1; k++ {
   292  		less := agg.less[k]
   293  		switch {
   294  		case less(p, q):
   295  			// p < q, so we have a decision.
   296  			return true
   297  		case less(q, p):
   298  			// p > q, so we have a decision.
   299  			return false
   300  		}
   301  		// p == q; try the next comparison.
   302  	}
   303  	// All comparisons to here said "equal", so just return whatever
   304  	// the final comparison reports.
   305  	return agg.less[k](p, q)
   306  }