sourcecamAxonPlugin::Etc.fan

//
// Copyright (c) 2009, SkyFoundry LLC
// All Rights Reserved
//
// History:
//   24 Jun 09  Brian Frank  Create
//

**
** Etc provides folio related utility methods.
**
@Js
const class Etc
{

//////////////////////////////////////////////////////////////////////////
// Dict
//////////////////////////////////////////////////////////////////////////

  **
  ** Get the emtpy Dict instance.
  **
  static Dict emptyDict() { EmptyDict.val }

  **
  ** Empty Str:Obj? map
  **
  @NoDoc static const Str:Obj? emptyTags := [:]

  **
  ** Make a Dict instance where 'val' is one of the following:
  **   - Dict: return 'val'
  **   - null: return `emptyDict`
  **   - Str[]: dictionary of key/Marker value pairs
  **   - Str:Obj?: wrap map as Dict
  **
  static Dict makeDict(Obj? val)
  {
    if (val == null) return emptyDict
    if (val is Dict) return val
    if (val is List)
    {
      tags := Str:Obj[:]
      ((List)val).each |Str key| { tags[key] = Marker.val }
      return factory.fromMap(tags)
    }
    Str:Obj? map := val
    return map.isEmpty ? emptyDict : factory.fromMap(map)
  }

  private static const EtcDictFactory factory
  static
  {
    try
      factory = Type.find("dict::DictFactory").make
    catch
      factory = EtcDictFactory()
  }

  **
  ** Make a list of Dict instances using `makeDict`.
  **
  static Dict[] makeDicts(Obj?[] maps)
  {
    maps.map |map -> Dict| { makeDict(map) }
  }

  **
  ** Get a read/write list of the dict's name keys.
  **
  static Str[] dictNames(Dict d)
  {
    names := Str[,]
    d.each |v, n| { names.add(n) }
    return names
  }

  **
  ** Given a list of dictionaries, find all the common names
  ** used.  Return the names in standard sorted order.
  **
  static Str[] dictsNames(Dict[] dicts)
  {
    Str:Str map := Str:Str[:] { ordered = true }
    hasId := false
    hasMod := false
    dicts.each |dict|
    {
      dict.each |v, n|
      {
        if (n == "id")  { hasId  = true; return }
        if (n == "mod") { hasMod = true; return }
        map[n] = n
      }
    }
    list := map.vals.sort
    if (hasId)  list.insert(0, "id")
    if (hasMod) list.add("mod")
    return list
  }

  **
  ** Get all the non-null values mapped by a dictionary.
  **
  static Obj?[] dictVals(Dict d)
  {
    vals := Obj?[,]
    d.each |v, n| { vals.add(v) }
    return vals
  }

  **
  ** Convert a Dict to a read/write map.  This method is expensive,
  ** when possible you should instead use `Dict.each`.
  **
  static Str:Obj? dictToMap(Dict d)
  {
    map := Str:Obj?[:]
    d.each |v, n| { map[n] = v }
    return map
  }

  **
  ** Return if the given value is one of the scalar
  ** values supported by dictions and grids.
  **
  static Bool isDictVal(Obj? val)
  {
    val == null || Kind.fromType(val.typeof, false) != null
  }

  **
  ** Apply the given map function to each name/value pair
  ** to construct a new Dict.
  **
  static Dict dictMap(Dict d, |Obj? v, Str n->Obj?| f)
  {
    map := Str:Obj?[:]
    d.each |v, n| { map[n] = f(v, n) }
    return makeDict(map)
  }

  **
  ** Apply the given map function to each name/value pair
  ** to construct a new Dict.
  **
  static Dict dictFindAll(Dict d, |Obj? v, Str n->Obj?| f)
  {
    map := Str:Obj?[:]
    d.each |v, n| { if (f(v, n)) map[n] = v }
    return makeDict(map)
  }


  **
  ** Add/set all the name/value pairs in a with those defined
  ** in b.  If b defines a remove value then that name/value is
  ** removed from a.  The b parameter may be any value
  ** accepted by `makeDict`
  **
  static Dict dictMerge(Dict a, Obj? b)
  {
    if (b == null) return a
    tags := dictToMap(a)
    if (b is Dict)
    {
      bd := (Dict)b
      if (bd.isEmpty) return a
      bd.each |v, n|
      {
        if (v === Remove.val) tags.remove(n)
        else tags[n] = v
      }
    }
    else
    {
      bm := (Str:Obj?)b
      if (bm.isEmpty) return a
      bm.each |v, n|
      {
        if (v === Remove.val) tags.remove(n)
        else tags[n] = v
      }
    }
    return makeDict(tags)
  }

  **
  ** Set a name/val pair in an existing dict.
  **
  static Dict dictSet(Dict d, Str name, Obj? val)
  {
    map := Str:Obj?[:]
    if (d is Row) map.ordered = true
    d.each |v, n| { map[n] = v }
    map[name] = val
    return MapDict(map)
  }

  **
  ** Set a name/val pair in an existing dict.
  **
  static Dict dictRemove(Dict d, Str name)
  {
    if (d.missing(name)) return d
    map := Str:Obj?[:]
    d.each |v, n| { map[n] = v }
    map.remove(name)
    return map.isEmpty ? emptyDict : MapDict(map)
  }

//////////////////////////////////////////////////////////////////////////
// Dis
//////////////////////////////////////////////////////////////////////////

  **
  ** Given a dic, attempt to find the best display string:
  **   1. 'disMacro' tag returns `macro` using dict as scope
  **   2. 'dis' tag
  **   3. 'name' tag
  **   4. 'tag' tag
  **   5. 'id' tag
  **   6. default
  **
  static Str? dictToDis(Dict dict, Str? def := "")
  {
    disMacro := dict.get("disMacro", null) as Str
    if (disMacro != null) return macro(disMacro, dict)

    Obj? d
    d = dict.get("dis", null);  if (d != null) return d.toStr
    d = dict.get("name", null); if (d != null) return d.toStr
    d = dict.get("tag", null);  if (d != null) return d.toStr
    id := dict.get("id", null) as Ref; if (id != null) return id.dis
    return def
  }

  **
  ** Get a relative display name.  If the child display name
  ** starts with the parent, then we can strip that as the
  ** common suffix.
  **
  static Str relDis(Str parent, Str child)
  {
    // we could really improve efficiency of this
    p := parent.split
    c := child.split
    m := p.size.min(c.size)
    i := 0
    while (i < m && p[i] == c[i]) ++i
    if (i == 0 || i >= c.size) return child
    return c[i..-1].join(" ")
  }

  **
  ** Given two display strings, return 1, 0, or -1 if a is less
  ** than, equal to, or greater than b.  The comparison is case
  ** insensitive and takes into account trailing digits so that a
  ** dis str such as "Foo-10" is greater than "Foo-2".
  **
  static Int compareDis(Str a, Str b)
  {
    // handle empty strings
    if (a.isEmpty) return b.isEmpty ? 0 : -1
    if (b.isEmpty) return 1

    // check first chars as quick optimization
    a0 := a[0].lower
    b0 := b[0].lower
    if (a0 != b0) return a0 <=> b0

    // check if we don't have trailing digits,
    // then use normal locale compare
    if (!a[-1].isDigit || !b[-1].isDigit) return a.localeCompare(b)

    // find first index of digits
    adi := a.size; while(adi>0 && a[adi-1].isDigit) --adi
    bdi := b.size; while(bdi>0 && b[bdi-1].isDigit) --bdi

    // check if prefixes are equal
    if (adi != bdi) return a.localeCompare(b)
    for (i:=0; i<adi; ++i)
      if (a[i].lower != b[i].lower) return a[i] <=> b[i]

    // prefixes are equal, compare by digits
    return a[adi..-1].toInt <=> b[bdi..-1].toInt
  }

  **
  ** Process macro pattern with given scope of variable name/value pairs.
  ** The pattern is a Unicode string with embedded expressions:
  **  - '$tag': resolve tag name from scope
  **  - '${tag}': resolve tag name from scope
  **  - '$<pod::key>': localization key
  **
  ** If a tag resolves to Ref, then we use Ref.dis for string.
  **
  static Str macro(Str pattern, Dict scope)
  {
    try
      return Macro(pattern, scope).apply
    catch (Err e)
      return pattern
  }

//////////////////////////////////////////////////////////////////////////
// Names
//////////////////////////////////////////////////////////////////////////

  **
  ** Return if the given string is a legal tag name:
  **   - first char must be ASCII lower case
  **     letter: 'a' - 'z'
  **   - rest of chars must be ASCII letter or
  **     digit: 'a' - 'z', 'A' - 'Z', '0' - '9', or '_'
  **
  static Bool isTagName(Str n)
  {
    if (n.isEmpty || !n[0].isLower) return false
    return n.all |c| { c.isAlphaNum || c == '_' }
  }

   **
   ** Take an arbitrary string ane convert into a safe tag name.
   **
   static Str toTagName(Str n)
   {
     if (n.isEmpty) throw ArgErr("string is empty")
     n = n.fromDisplayName
     buf := StrBuf()
     n.each |ch|
     {
       if (ch.isAlphaNum)
       {
         if (buf.isEmpty)
         {
           if (ch.isDigit) buf.addChar('v').addChar(ch)
           else if (ch.isUpper) buf.addChar(ch.lower)
           else buf.addChar(ch)
         }
         else buf.addChar(ch)
       }
     }
     if (buf.isEmpty) return "v"
     return buf.toStr
   }

  **
  ** Get the localized string for the given tag name for the
  ** current locale. See `docSkySpark::Localization#tags`.
  **
  static Str tagToLocale(Str name)
  {
    pod := Pod.find("proj")
    locale := Locale.cur
    props := Env.cur.props(pod, `tags/${locale.lang}.props`, Duration.maxVal)
    return props[name] ?: name
  }

//////////////////////////////////////////////////////////////////////////
// Grids
//////////////////////////////////////////////////////////////////////////

  **
  ** Given an arbitrary object, translate it to a Grid suitable
  ** for serizliation with Zinc:
  **   - if grid just return it
  **   - if row in grid of size, return row.grid
  **   - if scalar return 1x1 grid
  **   - if dict return grid where dict is only
  **   - if list of dict return grid where each dict is row
  **   - if list of non-dicts, return one col grid with rows for each item
  **   - if non-zinc type return grid with cols val, type
  **
  static Grid toGrid(Obj? val)
  {
    // if already a Grid
    if (val is Grid) return (Grid)val

    // if a Row in a single row Grid
    if (val is Row)
    {
      grid := ((Row)val).grid
      try
        if (grid.size == 1) return grid
      catch {}
    }

    // if value is a Dict map to a 1 row grid
    if (val is Dict) return makeDictGrid(null, val)

    // if value is a list
    if (val is List)
    {
      // if list is all dicts, turn into real NxN grid
      list := (List)val
      if (list.all { it is Dict }) return makeDictsGrid(null, val)

      // otherwise just turn it into a 1 column grid
      grid := ZincGrid(emptyDict, [ZincCol("val")])
      list.each |v| { grid.addRow([toCell(v)]) }
      return grid
    }

    // scalar translate to 1x1 Grid
    grid := ZincGrid(emptyDict, [ZincCol("val")])
    grid.addRow([toCell(val)])
    return grid
  }

  private static Obj? toCell(Obj? val)
  {
    if (isDictVal(val)) return val
    return "$val.toStr [$val.typeof]"
  }

  **
  ** Construct an empty grid with just the given grid level meta-data.
  ** The meta parameter can be any `makeDict` value.
  **
  static Grid makeEmptyGrid(Obj? meta := null)
  {
    ZincGrid(makeDict(meta), [ZincCol("empty")])
  }

  **
  ** Construct a grid for an error response.
  **
  static Grid makeErrGrid(Err e, Obj? meta := null)
  {
    // figure out trace
    trace := e.traceToStr
    if (e.typeof.field("axonTrace", false) != null)
    {
      trace = "ERROR: $e.msg\n\n" + e->axonTrace + "\n" + trace
    }

    // core tags
    tags := [
      "err":Marker.val,
      "dis": e.toStr,
      "errTrace": trace,
      "errType":e.typeof.qname
    ]

    // additional tags
    if (meta != null) makeDict(meta).each |v, n| { tags[n] = v }

    return makeEmptyGrid(tags)
  }

  **
  ** Convenience for `makeDictGrid`
  **
  static Grid makeMapGrid(Obj? meta, Str:Obj? row)
  {
    makeDictGrid(meta, makeDict(row))
  }

  **
  ** Convenience for `makeDictsGrid`
  **
  static Grid makeMapsGrid(Obj? meta, [Str:Obj?][] rows)
  {
    makeDictsGrid(meta, makeDicts(rows))
  }

  **
  ** Construct a grid for a Dict row.
  ** The meta parameter can be any `makeDict` value.
  **
  static Grid makeDictGrid(Obj? meta, Dict row)
  {
    cols  := ZincCol[,]
    cells := Obj?[,]
    if (row.has("id")) { cols.add(ZincCol("id")); cells.add(row["id"]) }
    row.each |v, n|
    {
      if (n == "id" || n == "mod") return
      cols.add(ZincCol(n))
      cells.add(v)
    }
    if (row.has("mod")) { cols.add(ZincCol("mod")); cells.add(row["mod"]) }
    if (cols.isEmpty) return makeEmptyGrid(meta)
    grid := ZincGrid(makeDict(meta), cols)
    grid.addRow(cells)
    return grid
  }

  **
  ** Construct a grid for a list of Dict rows.
  ** The meta parameter can be any `makeDict` value.
  **
  static Grid makeDictsGrid(Obj? meta, Dict[] rows)
  {
    // boundary cases
    if (rows.isEmpty) return makeEmptyGrid(meta)
    if (rows.size == 1) return makeDictGrid(meta, rows.first)

    // just brute force it for now

    // first pass finds all the unique columns
    colNames := dictsNames(rows)
    if (colNames.isEmpty) throw ArgErr("cols are empty")
    ZincCol[] cols := colNames.map |n,i->ZincCol| { ZincCol(n) }

    // map rows
    return ZincGrid(makeDict(meta), cols).addDictRows(rows)
  }

  **
  ** Construct a grid with one column for a list.  The meta
  ** and colMeta parameters can be any `makeDict` value.
  **
  static Grid makeListGrid(Obj? meta, Str colName, Obj? colMeta, Obj?[] rows)
  {
    grid := ZincGrid(makeDict(meta), [ZincCol(colName, colMeta)])
    rows.each |v| { grid.addRow([v]) }
    return grid
  }

  **
  ** Construct a grid for a list of rows, where each row is
  ** a list of cells.  The meta and colMetas parameters can
  ** be any `makeDict` value.
  **
  static Grid makeListsGrid(Obj? meta, Str[] colNames, Obj?[]? colMetas, Obj?[][] rows)
  {
    cols := colNames.map |n, i->ZincCol| { ZincCol(n, colMetas?.get(i)) }
    grid := ZincGrid(makeDict(meta), cols)
    rows.each |row| { grid.addRow(row) }
    return grid
  }

  **
  ** Given an existing grid, return a new grid which uses a callback
  ** to provide the display string for cells and columns. If the cb
  ** returns null, then normal display formatting is applied.
  ** If the callback is invoked with a null row then return display
  ** value for column, otherwise return display value for cell.
  **
  ** TODO: this functionality has been deprecated with 2.0 with removal
  **   of cell dis/meta.  Use of this grid will no longer encode to
  **   Zinc correctly
  **
  @Deprecated
  @NoDoc
  static Grid makeDisValGrid(Grid grid, |Col,Row?->Str?| cb)
  {
    grid
  }

}

**************************************************************************
** DictFactory
**************************************************************************

@Js
@NoDoc
const class EtcDictFactory
{
  virtual Dict fromMap(Str:Obj? map) { MapDict(map) }
}