git.s-ol.nu mmm / 9632233 mmm / mmmfs / fileder.moon
9632233

Tree @9632233 (Download .tar.gz)

fileder.moon @9632233raw · history · blame

require = relative ..., 1
import get_conversions, apply_conversions from require '.conversion'

-- split filename into dirname + basename
dir_base = (path) ->
  dir, base = path\match '(.-)([^/]-)$'
  if dir and #dir > 0
    dir = dir\sub 1, #dir - 1

  dir, base

-- Key of a Fileder Facet
-- contains:
-- * @name - key name or '' for main content
-- * @type - type string (type -> type -> type)
class Key
  -- instantiate from table w/ keys described above
  -- or string like '@name: @type' (name optional)
  new: (opts, second) =>
    if 'string' == type second
      @name, @type = (opts or ''), second
    elseif 'string' == type opts
      @name, @type = opts\match '^([%w-_]+): *(.+)$'
      if not @name
        @name = ''
        @type = opts
    elseif 'table' == type opts
      @name = opts.name
      @type = opts.type
      @original = opts.original
      @filename = opts.filename
    else
      error "wrong argument type: #{type opts}, #{type second}"

  -- format as a string (see constructor)
  tostring: =>
    if @name == ''
      @type
    else
      "#{@name}: #{@type}"

  __tostring: => @tostring!

-- Fileder itself
-- contains:
-- * @facets - Facet Map (Key to Value)
-- * @children - Children Array
class Fileder
  new: (@store, @path='') =>
    @loaded = false

    -- lazy-load children,
    -- allow indexing by name as well as numeric index,
    -- automatically mount children on insert
    @children = setmetatable {}, {
      __len: (t) ->
        @load! unless @loaded
        rawlen t

      __ipairs: (t) ->
        @load! unless @loaded
        ipairs t

      __index: (t, k) ->
        @load! unless @loaded

        if 'string' == type k
          @walk "#{@path}/#{k}"
        else
          rawget t, k

      __newindex: (t, k, child) ->
        rawset t, k, child

        if @path == '/'
          child\mount '/'
        elseif @path
          child\mount @path .. '/'
    }

    -- lazy-load facets,
    -- allow indexing by name as well as numeric index,
    -- automatically mount children on insert

    -- we need to store the presence of facets separately from the actual (cached) value,
    -- because we want to lazily load the tree (index) *and* facet contents.
    -- @facet_keys maps from key-strings to Key instances ('canonical Key instances')
    -- @facets maps from canonical keys to cached values and lazy-loads.
    -- both maps automatically rewrite __index and __newindex for both Key instances and strings.
    @facet_keys = setmetatable {}, {
      __pairs: (t) ->
        @load! unless @loaded
        next, t

      __index: (t, k) ->
        canonical = rawget t, tostring k
        canonical or= Key k
        canonical

      __newindex: (t, k, v) ->
        k = Key k
        rawset t, (tostring k), v
    }
    @facets = setmetatable {}, {
      __index: (t, k) ->
        @load! unless @loaded

        -- get canonical Key instance
        k = @facet_keys[k]

        -- if cached, return
        if v = rawget t, k
          return v

        with v = @store\load_facet @path, k.name, k.type
          rawset t, k, v

      __pairs: (t) ->
        @load! unless @loaded

        -- force cache all facets
        for k, v in pairs @facet_keys
          t[v]

        pairs @facets

      __newindex: (t, k, v) ->
        -- get canonical Key instance
        k = @facet_keys[k]

        rawset t, k, v

        v = k unless v == nil
        rawset @facet_keys, k, v
    }

  load: =>
    @loaded = true

    for path in @store\list_fileders_in @path
      table.insert @children, Fileder @store, path

    for name, type in @store\list_facets @path
      key = Key name, type
      @facet_keys[key] = key

    _, name = dir_base @path
    @facets['name: alpha'] = name

  -- -- instantiate from facets and children tables
  -- -- or mix in one table (numeric keys are children, remainder facets)
  -- -- facet-keys are passed to Key constructor
  -- new: (facets, children) =>
  --   if not children
  --     children = for i, child in ipairs facets
  --       facets[i] = nil
  --       child

  --   -- automatically mount children on insert
  --   @children = setmetatable {}, {
  --     __index: (t, k) ->
  --       return rawget t, k unless 'string' == type k

  --       @walk "#{@path}/#{k}"

  --     __newindex: (t, k, child) ->
  --       rawset t, k, child
  --       if @path == '/'
  --         child\mount '/'
  --       elseif @path
  --         child\mount @path .. '/'
  --   }

  --   -- copy children
  --   for i, child in ipairs children
  --     @children[i] = child

  --   -- automatically reify string keys on insert
  --   @facets = setmetatable {}, __newindex: (t, key, v) ->
  --     rawset t, (Key key), v

  --   -- copy facets
  --   for k, v in pairs facets
  --     @facets[k] = v

  -- recursively walk to and return the fileder with @path == path
  -- * path - the path to walk to
  walk: (path) =>
    -- fix relative paths
    return @ if path == ''
    path = "#{@path}/#{path}" if '/' != path\sub 1, 1

    -- early-out if we are outside of the path already
    return unless @path == path\sub 1, #@path

    -- gotcha
    return @ if #path == #@path

    for child in *@children
      if match = child\walk path
        return match

  -- recursively mount fileder and children at path
  -- * path - the path to mount at
  -- * mount_as - dont append own name to path
  mount: (path, mount_as) =>
    if not mount_as
      path ..= @gett 'name: alpha'

    assert not @path or @path == path, "mounted twice: #{@path} and now #{path}"

    @path = path

    for child in *@children
      child\mount @path .. '/'

  -- recursively iterate all children (coroutine)
  -- * depth - depth to stop after; 1 = yield only self (default: infinite)
  iterate: (depth=0) =>
    coroutine.yield @
    return if depth == 1

    for child in *@children
      child\iterate depth - 1

  -- get all facet names (list)
  get_facets: =>
    names = {}
    for str, key in pairs @facet_keys
      names[key.name] = true

    [name for name in pairs names]

  -- get an index table, listing path, facets and children
  -- optionally get recursive index
  get_index: (recursive=false) =>
    {
      path: @path
      facets: [key for str, key in pairs @facet_keys]
      children: if recursive
        [child\get_index true for child in *@children]
      else
        [{ :path } for { :path } in *@children]
    }

  -- check whether a facet is directly available
  has: (...) =>
    want = Key ...

    @facet_keys[want]

  -- check whether any facet with that name exists
  has_facet: (want) =>
    for str, key in pairs @facet_keys
      continue if key.original

      if key.name == want
        return key

  -- find facet and type according to criteria, nil if no value or conversion path
  -- * ... - arguments like Key
  find: (...) =>
    want = Key ...

    -- filter facets by name
    matching = [ key for str, key in pairs @facet_keys when key.name == want.name ]
    return unless #matching > 0

    -- get shortest conversion path
    shortest_path, start = get_conversions want.type, [ key.type for key in *matching ]

    if start
      for key in *matching
        if key.type == start
          return key, shortest_path

      error "couldn't find key after resolution?"

  -- get and convert facet according to criteria, nil if no value or conversion path
  -- * ... - arguments like Key
  get: (...) =>
    want = Key ...

    -- find matching key and shortest conversion path
    key, conversions = @find want

    if key
      value = apply_conversions conversions, @facets[key], @, key
      value, key

  -- like @get, throw if no value or conversion path
  gett: (...) =>
    want = Key ...

    value, key = @get want
    assert value, "#{@} doesn't have value for '#{want}'"
    value, key

  __tostring: => "Fileder:#{@path}"

{
  :Key
  :Fileder
  :dir_base
}