github.com/haagen/force@v0.19.6-0.20140911230915-22addd930b34/display.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  )
     9  
    10  var BatchInfoTemplate = `
    11  Id 			%s
    12  JobId 			%s
    13  State 			%s
    14  CreatedDate 		%s
    15  SystemModstamp 		%s
    16  NumberRecordsProcessed  %d
    17  `
    18  
    19  func DisplayBatchList(batchInfos []BatchInfo) {
    20  
    21  	for i, batchInfo := range batchInfos {
    22  		fmt.Printf("Batch %d", i)
    23  		DisplayBatchInfo(batchInfo)
    24  		fmt.Println()
    25  	}
    26  }
    27  
    28  func DisplayBatchInfo(batchInfo BatchInfo) {
    29  
    30  	fmt.Printf(BatchInfoTemplate, batchInfo.Id, batchInfo.JobId, batchInfo.State,
    31  		batchInfo.CreatedDate, batchInfo.SystemModstamp,
    32  		batchInfo.NumberRecordsProcessed)
    33  }
    34  
    35  func DisplayJobInfo(jobInfo JobInfo) {
    36  	var msg = `
    37  Id				%s
    38  State 				%s
    39  Operation			%s
    40  Object 				%s
    41  Api Version 			%s
    42  
    43  Created By Id 			%s
    44  Created Date 			%s
    45  System Mod Stamp		%s
    46  Content Type 			%s
    47  Concurrency Mode 		%s
    48  
    49  Number Batches Queued 		%d
    50  Number Batches In Progress	%d
    51  Number Batches Completed 	%d
    52  Number Batches Failed 		%d
    53  Number Batches Total 		%d
    54  Number Records Processed 	%d
    55  Number Retries 			%d
    56  
    57  Number Records Failed 		%d
    58  Total Processing Time 		%d
    59  Api Active Processing Time 	%d
    60  Apex Processing Time 		%d
    61  `
    62  	fmt.Printf(msg, jobInfo.Id, jobInfo.State, jobInfo.Operation, jobInfo.Object, jobInfo.ApiVersion,
    63  		jobInfo.CreatedById, jobInfo.CreatedDate, jobInfo.SystemModStamp,
    64  		jobInfo.ContentType, jobInfo.ConcurrencyMode,
    65  		jobInfo.NumberBatchesQueued, jobInfo.NumberBatchesInProgress,
    66  		jobInfo.NumberBatchesCompleted, jobInfo.NumberBatchesFailed,
    67  		jobInfo.NumberBatchesTotal, jobInfo.NumberRecordsProcessed,
    68  		jobInfo.NumberRetries,
    69  		jobInfo.NumberRecordsFailed, jobInfo.TotalProcessingTime,
    70  		jobInfo.ApiActiveProcessingTime, jobInfo.ApexProcessingTime)
    71  }
    72  
    73  func DisplayForceSobjects(sobjects []ForceSobject) {
    74  	names := make([]string, len(sobjects))
    75  	for i, sobject := range sobjects {
    76  		names[i] = sobject["name"].(string)
    77  	}
    78  	sort.Strings(names)
    79  	for _, name := range names {
    80  		fmt.Println(name)
    81  	}
    82  }
    83  
    84  func DisplayForceRecordsf(records []ForceRecord, format string) {
    85  	switch format {
    86  	case "csv":
    87  		fmt.Println(RenderForceRecordsCSV(records, format))
    88  	default:
    89  		fmt.Printf("Format %s not supported\n\n", format)
    90  	}
    91  }
    92  
    93  func DisplayForceRecords(result ForceQueryResult) {
    94  	if len(result.Records) > 0 {
    95  		fmt.Print(RenderForceRecords(result.Records))
    96  	}
    97  	fmt.Println(fmt.Sprintf(" (%d records)", result.TotalSize))
    98  }
    99  
   100  func recordColumns(records []ForceRecord) (columns []string) {
   101  	for _, record := range records {
   102  		for key, _ := range record {
   103  			found := false
   104  			for _, column := range columns {
   105  				if column == key {
   106  					found = true
   107  					break
   108  				}
   109  			}
   110  			if !found {
   111  				columns = append(columns, key)
   112  			}
   113  		}
   114  	}
   115  	return
   116  }
   117  
   118  func coerceForceRecords(uncoerced []map[string]interface{}) (records []ForceRecord) {
   119  	records = make([]ForceRecord, len(uncoerced))
   120  	for i, record := range uncoerced {
   121  		records[i] = ForceRecord(record)
   122  	}
   123  	return
   124  }
   125  
   126  func columnLengths(records []ForceRecord, prefix string) (lengths map[string]int) {
   127  	lengths = make(map[string]int)
   128  
   129  	columns := recordColumns(records)
   130  	for _, column := range columns {
   131  		lengths[fmt.Sprintf("%s.%s", prefix, column)] = len(column) + 2
   132  	}
   133  
   134  	for _, record := range records {
   135  		for column, value := range record {
   136  			key := fmt.Sprintf("%s.%s", prefix, column)
   137  			length := 0
   138  			switch value := value.(type) {
   139  			case []ForceRecord:
   140  				lens := columnLengths(value, key)
   141  				for k, l := range lens {
   142  					length += l
   143  					if l > lengths[k] {
   144  						lengths[k] = l
   145  					}
   146  				}
   147  				length += len(lens) - 1
   148  			default:
   149  				if value == nil {
   150  					length = len(" (null) ")
   151  				} else {
   152  					length = len(fmt.Sprintf(" %v ", value))
   153  				}
   154  			}
   155  			if length > lengths[key] {
   156  				lengths[key] = length
   157  			}
   158  		}
   159  	}
   160  	return
   161  }
   162  
   163  func recordHeader(columns []string, lengths map[string]int, prefix string) (out string) {
   164  	headers := make([]string, len(columns))
   165  	for i, column := range columns {
   166  		key := fmt.Sprintf("%s.%s", prefix, column)
   167  		headers[i] = fmt.Sprintf(fmt.Sprintf(" %%-%ds ", lengths[key]-2), column)
   168  	}
   169  	out = strings.Join(headers, "|")
   170  	return
   171  }
   172  
   173  func recordSeparator(columns []string, lengths map[string]int, prefix string) (out string) {
   174  	separators := make([]string, len(columns))
   175  	for i, column := range columns {
   176  		key := fmt.Sprintf("%s.%s", prefix, column)
   177  		separators[i] = strings.Repeat("-", lengths[key])
   178  	}
   179  	out = strings.Join(separators, "+")
   180  	return
   181  }
   182  
   183  func recordRow(record ForceRecord, columns []string, lengths map[string]int, prefix string) (out string) {
   184  	values := make([]string, len(columns))
   185  	for i, column := range columns {
   186  		value := record[column]
   187  		switch value := value.(type) {
   188  		case []ForceRecord:
   189  			values[i] = strings.TrimSuffix(renderForceRecords(value, fmt.Sprintf("%s.%s", prefix, column), lengths), "\n")
   190  		default:
   191  			if value == nil {
   192  				values[i] = fmt.Sprintf(fmt.Sprintf(" %%-%ds ", lengths[column]-2), "(null)")
   193  			} else {
   194  				values[i] = fmt.Sprintf(fmt.Sprintf(" %%-%dv ", lengths[column]-2), value)
   195  			}
   196  		}
   197  	}
   198  	maxrows := 1
   199  	for _, value := range values {
   200  		rows := len(strings.Split(value, "\n"))
   201  		if rows > maxrows {
   202  			maxrows = rows
   203  		}
   204  	}
   205  	rows := make([]string, maxrows)
   206  	for i := 0; i < maxrows; i++ {
   207  		rowvalues := make([]string, len(columns))
   208  		for j, column := range columns {
   209  			key := fmt.Sprintf("%s.%s", prefix, column)
   210  			parts := strings.Split(values[j], "\n")
   211  			if i < len(parts) {
   212  				rowvalues[j] = fmt.Sprintf(fmt.Sprintf("%%-%ds", lengths[key]), parts[i])
   213  			} else {
   214  				rowvalues[j] = strings.Repeat(" ", lengths[key])
   215  			}
   216  		}
   217  		rows[i] = strings.Join(rowvalues, "|")
   218  	}
   219  	out = strings.Join(rows, "\n")
   220  	return
   221  }
   222  
   223  // returns first index of a given string
   224  func StringSlicePos(slice []string, value string) int {
   225  	for p, v := range slice {
   226  		if v == value {
   227  			return p
   228  		}
   229  	}
   230  	return -1
   231  }
   232  
   233  // returns true if a slice contains given string
   234  func StringSliceContains(slice []string, value string) bool {
   235  	return StringSlicePos(slice, value) > -1
   236  }
   237  
   238  func RenderForceRecordsCSV(records []ForceRecord, format string) string {
   239  	var out bytes.Buffer
   240  
   241  	var keys []string
   242  	var flattenedRecords []map[string]interface{}
   243  	for _, record := range records {
   244  		flattenedRecord := flattenForceRecord(record)
   245  		flattenedRecords = append(flattenedRecords, flattenedRecord)
   246  		for key, _ := range flattenedRecord {
   247  			if !StringSliceContains(keys, key) {
   248  				keys = append(keys, key)
   249  			}
   250  		}
   251  	}
   252  	//keys = RemoveTransientRelationships(keys)
   253  	f, _ := ActiveCredentials()
   254  	if len(records) > 0 {
   255  		lengths := make([]int, len(keys))
   256  		outKeys := make([]string, len(keys))
   257  		for i, key := range keys {
   258  			lengths[i] = len(key)
   259  			if strings.HasSuffix(key, "__c") && f.Namespace != "" {
   260  				outKeys[i] = fmt.Sprintf(`%%%`, f.Namespace, "__", key)
   261  			} else {
   262  				outKeys[i] = key
   263  			}
   264  		}
   265  
   266  		formatter_parts := make([]string, len(outKeys))
   267  		for i, length := range lengths {
   268  			formatter_parts[i] = fmt.Sprintf(`"%%-%ds"`, length)
   269  		}
   270  
   271  		formatter := strings.Join(formatter_parts, `,`)
   272  		out.WriteString(fmt.Sprintf(formatter+"\n", StringSliceToInterfaceSlice(outKeys)...))
   273  		for _, record := range flattenedRecords {
   274  			values := make([][]string, len(keys))
   275  			for i, key := range keys {
   276  				values[i] = strings.Split(fmt.Sprintf(`%v`, record[key]), `\n`)
   277  			}
   278  
   279  			maxLines := 0
   280  			for _, value := range values {
   281  				lines := len(value)
   282  				if lines > maxLines {
   283  					maxLines = lines
   284  				}
   285  			}
   286  
   287  			for li := 0; li < maxLines; li++ {
   288  				line := make([]string, len(values))
   289  				for i, value := range values {
   290  					if len(value) > li {
   291  						line[i] = strings.Replace(value[li], `"`, `'`, -1)
   292  					}
   293  				}
   294  				out.WriteString(fmt.Sprintf(formatter+"\n", StringSliceToInterfaceSlice(line)...))
   295  			}
   296  		}
   297  	}
   298  	return out.String()
   299  	return ""
   300  }
   301  
   302  func flattenForceRecord(record ForceRecord) (flattened ForceRecord) {
   303  	flattened = make(ForceRecord)
   304  	for key, value := range record {
   305  		if key == "attributes" {
   306  			continue
   307  		}
   308  		switch value := value.(type) {
   309  		case map[string]interface{}:
   310  			if value["records"] != nil {
   311  				unflattened := value["records"].([]interface{})
   312  				subflattened := make([]ForceRecord, len(unflattened))
   313  				for i, record := range unflattened {
   314  					subflattened[i] = (map[string]interface{})(flattenForceRecord(ForceRecord(record.(map[string]interface{}))))
   315  				}
   316  				flattened[key] = subflattened
   317  			} else {
   318  				for k, v := range flattenForceRecord(value) {
   319  					flattened[fmt.Sprintf("%s.%s", key, k)] = v
   320  				}
   321  			}
   322  		default:
   323  			flattened[key] = value
   324  		}
   325  	}
   326  	return
   327  }
   328  
   329  func recordsHaveSubRows(records []ForceRecord) bool {
   330  	for _, record := range records {
   331  		for _, value := range record {
   332  			switch value := value.(type) {
   333  			case []ForceRecord:
   334  				if len(value) > 0 {
   335  					return true
   336  				}
   337  			}
   338  		}
   339  	}
   340  	return false
   341  }
   342  
   343  func renderForceRecords(records []ForceRecord, prefix string, lengths map[string]int) string {
   344  	var out bytes.Buffer
   345  
   346  	columns := recordColumns(records)
   347  
   348  	out.WriteString(recordHeader(columns, lengths, prefix) + "\n")
   349  	out.WriteString(recordSeparator(columns, lengths, prefix) + "\n")
   350  
   351  	for _, record := range records {
   352  		out.WriteString(recordRow(record, columns, lengths, prefix) + "\n")
   353  		if recordsHaveSubRows(records) {
   354  			out.WriteString(recordSeparator(columns, lengths, prefix) + "\n")
   355  		}
   356  	}
   357  
   358  	return out.String()
   359  }
   360  
   361  func RenderForceRecords(records []ForceRecord) string {
   362  	flattened := make([]ForceRecord, len(records))
   363  	for i, record := range records {
   364  		flattened[i] = flattenForceRecord(record)
   365  	}
   366  	lengths := columnLengths(flattened, "")
   367  	return renderForceRecords(flattened, "", lengths)
   368  }
   369  
   370  func DisplayForceRecord(record ForceRecord) {
   371  	DisplayInterfaceMap(record, 0)
   372  }
   373  
   374  func DisplayInterfaceMap(object map[string]interface{}, indent int) {
   375  	keys := make([]string, len(object))
   376  	i := 0
   377  	for key, _ := range object {
   378  		keys[i] = key
   379  		i++
   380  	}
   381  	sort.Strings(keys)
   382  	for _, key := range keys {
   383  		for i := 0; i < indent; i++ {
   384  			fmt.Printf("  ")
   385  		}
   386  		fmt.Printf("%s: ", key)
   387  		switch v := object[key].(type) {
   388  		case map[string]interface{}:
   389  			fmt.Printf("\n")
   390  			DisplayInterfaceMap(v, indent+1)
   391  		default:
   392  			fmt.Printf("%v\n", v)
   393  		}
   394  	}
   395  }
   396  
   397  func StringSliceToInterfaceSlice(s []string) (i []interface{}) {
   398  	for _, str := range s {
   399  		i = append(i, interface{}(str))
   400  	}
   401  	return
   402  }
   403  
   404  type ForceSobjectFields []interface{}
   405  
   406  func DisplayForceSobject(sobject ForceSobject) {
   407  	fields := ForceSobjectFields(sobject["fields"].([]interface{}))
   408  	sort.Sort(fields)
   409  	for _, f := range fields {
   410  		field := f.(map[string]interface{})
   411  		switch field["type"] {
   412  		case "picklist":
   413  			var values []string
   414  			for _, value := range field["picklistValues"].([]interface{}) {
   415  				values = append(values, value.(map[string]interface{})["value"].(string))
   416  			}
   417  			fmt.Printf("%s: %s (%s)\n", field["name"], field["type"], strings.Join(values, ", "))
   418  		case "reference":
   419  			var refs []string
   420  			for _, ref := range field["referenceTo"].([]interface{}) {
   421  				refs = append(refs, ref.(string))
   422  			}
   423  			fmt.Printf("%s: %s (%s)\n", field["name"], field["type"], strings.Join(refs, ", "))
   424  		default:
   425  			fmt.Printf("%s: %s\n", field["name"], field["type"])
   426  		}
   427  	}
   428  }
   429  
   430  func DisplayFieldTypes() {
   431  	var msg = `
   432    text/string            (length = 255)
   433    textarea               (length = 255)
   434    longtextarea           (length = 32768, visibleLines = 5)
   435    richtextarea           (length = 32768, visibleLines = 5)
   436    checkbox/bool/boolean  (defaultValue = false)
   437    datetime               ()
   438    float/double/currency  (length = 16, precision = 2)
   439    number/int             (length = 18, precision = 0)
   440    autonumber             (displayFormat = "AN {00000}", startingNumber = 0)
   441    geolocation            (displayLocationInDecimal = true, scale = 5)
   442    lookup                 (will be prompted for Object and label)
   443    masterdetail           (will be prompted for Object and label)
   444  
   445    *To create a formula field add a formula argument to the command.
   446    force field create <objectname> <fieldName>:text formula:'LOWER("HEY MAN")'
   447    `
   448  	fmt.Println(msg)
   449  }
   450  
   451  func DisplayFieldDetails(fieldType string) {
   452  	var msg = ``
   453  	switch fieldType {
   454  	case "text", "string":
   455  		msg = DisplayTextFieldDetails()
   456  		break
   457  	case "textarea":
   458  		msg = DisplayTextAreaFieldDetails()
   459  		break
   460  	case "longtextarea":
   461  		msg = DisplayLongTextAreaFieldDetails()
   462  		break
   463  	case "richtextarea":
   464  		msg = DisplayRichTextAreaFieldDetails()
   465  		break
   466  	case "checkbox", "bool", "boolean":
   467  		msg = DisplayCheckboxFieldDetails()
   468  		break
   469  	case "datetime":
   470  		msg = DisplayDatetimeFieldDetails()
   471  		break
   472  	case "float", "double", "currency":
   473  		if fieldType == "currency" {
   474  			msg = DisplayCurrencyFieldDetails()
   475  		} else {
   476  			msg = DisplayDoubleFieldDetails()
   477  		}
   478  		break
   479  	case "number", "int":
   480  		msg = DisplayDoubleFieldDetails()
   481  		break
   482  	case "autonumber":
   483  		msg = DisplayAutonumberFieldDetails()
   484  		break
   485  	case "geolocation":
   486  		msg = DisplayGeolocationFieldDetails()
   487  		break
   488  	case "lookup":
   489  		msg = DisplayLookupFieldDetails()
   490  		break
   491  	case "masterdetail":
   492  		msg = DisplayMasterDetailFieldDetails()
   493  		break
   494  	default:
   495  		msg = `
   496    Sorry, that is not a valid field type.
   497  `
   498  	}
   499  	fmt.Printf(msg + "\n")
   500  }
   501  
   502  func DisplayTextFieldDetails() (message string) {
   503  	return fmt.Sprintf(`
   504    Allows users to enter any combination of letters and numbers.
   505  
   506      %s
   507        label            - defaults to name
   508        length           - defaults to 255
   509        name
   510    
   511      %s
   512        description
   513        helptext
   514        required         - defaults to false
   515        unique           - defaults to false
   516        caseSensistive   - defaults to false
   517        externalId       - defaults to false
   518        defaultValue
   519        formula          - defaultValue must be blask
   520        formulaTreatBlanksAs  - defaults to "BlankAsZero"
   521  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   522  }
   523  func DisplayTextAreaFieldDetails() (message string) {
   524  	return fmt.Sprintf(`
   525    Allows users to enter up to 255 characters on separate lines.
   526  
   527      %s
   528        label            - defaults to name
   529        name
   530    
   531      %s
   532        description
   533        helptext
   534        required         - defaults to false
   535        defaultValue
   536  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   537  }
   538  func DisplayLongTextAreaFieldDetails() (message string) {
   539  	return fmt.Sprintf(`
   540    Allows users to enter up to 32,768 characters on separate lines.
   541  
   542      %s
   543        label            - defaults to name
   544        length           - defaults to 32,768
   545        name
   546        visibleLines     - defaults to 3
   547    
   548      %s
   549        description
   550        helptext
   551        defaultValue
   552  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   553  }
   554  func DisplayRichTextAreaFieldDetails() (message string) {
   555  	return fmt.Sprintf(`
   556    Allows users to enter formatted text, add images and links. Up to 32,768 characters on separate lines.
   557  
   558      %s
   559        label            - defaults to name
   560        length           - defaults to 32,768
   561        name
   562        visibleLines     - defaults to 25
   563    
   564      %s
   565        description
   566        helptext
   567  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   568  }
   569  func DisplayCheckboxFieldDetails() (message string) {
   570  	return fmt.Sprintf(`
   571    Allows users to select a True (checked) or False (unchecked) value.
   572  
   573      %s
   574        label            - defaults to name
   575        name
   576    
   577      %s
   578        description
   579        helptext
   580        defaultValue     - defaults to unchecked or false
   581        formula          - defaultValue must be blask
   582        formulaTreatBlanksAs  - defaults to "BlankAsZero"
   583  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   584  }
   585  func DisplayDatetimeFieldDetails() (message string) {
   586  	return fmt.Sprintf(`
   587    Allows users to enter a date and time.
   588  
   589      %s
   590        label            - defaults to name
   591        name
   592    
   593      %s
   594        description
   595        helptext
   596        defaultValue     
   597        required         - defaults to false
   598        formula          - defaultValue must be blask
   599        formulaTreatBlanksAs  - defaults to "BlankAsZero"
   600  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   601  }
   602  func DisplayDoubleFieldDetails() (message string) {
   603  	return fmt.Sprintf(`
   604    Allows users to enter any number. Leading zeros are removed.
   605  
   606      %s
   607        label            - defaults to name
   608        length           - defaults to 18
   609        name
   610        precision        - decimal places (defaults to 0)
   611        scale            - digits left of decimal (defaults to 18)
   612    
   613      %s
   614        description
   615        helptext
   616        required         - defaults to false
   617        unique           - defaults to false
   618        externalId       - defaults to false 
   619        defaultValue
   620        formula          - defaultValue must be blask
   621        formulaTreatBlanksAs  - defaults to "BlankAsZero"
   622  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   623  }
   624  func DisplayCurrencyFieldDetails() (message string) {
   625  	return fmt.Sprintf(`
   626    Allows users to enter a dollar or other currency amount and automatically formats the field as a currency amount.
   627  
   628      %s
   629        label            - defaults to name
   630        length           - defaults to 18
   631        name
   632        precision        - decimal places (defaults to 0)
   633        scale            - digits left of decimal (defaults to 18)
   634    
   635      %s
   636        description
   637        helptext
   638        required         - defaults to false
   639        defaultValue
   640        formula          - defaultValue must be blask
   641        formulaTreatBlanksAs  - defaults to "BlankAsZero"
   642  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   643  }
   644  func DisplayAutonumberFieldDetails() (message string) {
   645  	return fmt.Sprintf(`
   646    A system-generated sequence number that uses a display format you define. The number is automatically incremented for each new record.
   647  
   648      %s
   649        label            - defaults to name
   650        name
   651        displayFormat    - defaults to "AN-{00000}"
   652        startingNumber   - defaults to 0
   653    
   654      %s
   655        description
   656        helptext
   657        externalId       - defaults to false
   658  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   659  }
   660  func DisplayGeolocationFieldDetails() (message string) {
   661  	return fmt.Sprintf(`
   662     Allows users to define locations.
   663  
   664      %s
   665        label                       - defaults to name
   666        name
   667        DisplayLocationInDecimal    - defaults false
   668        scale                       - defaults to 5 (number of decimals to the right)
   669    
   670      %s
   671        description
   672        helptext
   673        required                    - defaults to false
   674  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   675  }
   676  func DisplayLookupFieldDetails() (message string) {
   677  	return fmt.Sprintf(`
   678     Creates a relationship that links this object to another object.
   679  
   680      %s
   681        label            - defaults to name
   682        name
   683        referenceTo      - Name of related object
   684        relationshipName - defaults to referenceTo value
   685  
   686      %s
   687        description
   688        helptext
   689        required         - defaults to false
   690        relationShipLabel
   691  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   692  }
   693  func DisplayMasterDetailFieldDetails() (message string) {
   694  	return fmt.Sprintf(`
   695     Creates a special type of parent-child relationship between this object (the child, or "detail") and another object (the parent, or "master") where:
   696       The relationship field is required on all detail records.
   697       The ownership and sharing of a detail record are determined by the master record.
   698       When a user deletes the master record, all detail records are deleted.
   699       You can create rollup summary fields on the master record to summarize the detail records.
   700  
   701      %s
   702        label            - defaults to name
   703        name
   704        referenceTo      - Name of related object
   705        relationshipName - defaults to referenceTo value
   706  
   707      %s
   708        description
   709        helptext
   710        required         - defaults to false
   711        relationShipLabel
   712  `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m")
   713  }