mattruggio/teton

View on GitHub
lib/teton/stores/memory.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module Teton
  module Stores
    # Plugs in a light-weight store that can be used for modeling other stores.
    class Memory
      CREATED_AT_KEY = 'created_at'
      IDS_KEY        = 'ids'
      INDICES_KEY    = 'indices'
      META_KEY       = 'meta'
      DATA_KEY       = 'data'
      UPDATED_AT_KEY = 'updated_at'

      attr_reader :store

      def initialize(store = {})
        @store = store || {}
      end

      # Main Object API

      def set(key, data)
        store_pointer = insert_traverse_to_last(key)

        upsert(store_pointer, key.last_part, data)

        self
      end

      def get(key, limit: nil, skip: nil)
        store_pointer = traverse_to_last(key)

        return unless store_pointer

        if key.resource?
          entries(key, store_pointer, key.last_part, limit: limit, skip: skip)
        else
          entry(key, store_pointer, key.last_part)
        end
      end

      def del(key)
        store_pointer = traverse_to_last(key)

        return self unless store_pointer

        store_pointer.delete(key.last_part)

        self
      end

      def count(key)
        store_pointer = traverse_to_last(key)

        return 0 unless store_pointer

        if key.resource?
          (store_pointer.dig(key.last_part, IDS_KEY) || {}).keys.length
        else
          store_pointer.dig(key.last_part, DATA_KEY) ? 1 : 0
        end
      end

      # Persistence API

      def load!(path)
        from_json!(File.read(path))
      end

      def save!(path)
        dir = File.dirname(path)

        FileUtils.mkdir_p(dir)

        File.write(path, to_json)

        self
      end

      def from_json!(json)
        @store = JSON.parse(json)

        self
      end

      def to_json(*_args)
        store.to_json
      end

      private

      def upsert(pointer, part, data)
        pointer[part] = record_prototype unless pointer.key?(part)

        pointer[part][DATA_KEY]                 = data
        pointer[part][META_KEY][UPDATED_AT_KEY] = Time.now.utc

        nil
      end

      def insert_traverse(index, pointer, part)
        if index.even?
          # index
          pointer[part] = { IDS_KEY => {} } unless pointer.key?(part)

          pointer[part][IDS_KEY]
        else
          # id
          pointer[part] = record_prototype unless pointer.key?(part)

          pointer[part][INDICES_KEY]
        end
      end

      def traverse(index, pointer, part)
        if index.even?
          # index
          pointer.dig(part, IDS_KEY)
        else
          # id
          pointer.dig(part, INDICES_KEY)
        end
      end

      def insert_traverse_to_last(key)
        store_pointer = store

        key.traverse do |key_pointer|
          store_pointer = insert_traverse(key_pointer.index, store_pointer, key_pointer.value) if key_pointer.not_last?
        end

        store_pointer
      end

      def traverse_to_last(key)
        store_pointer = store

        key.traverse do |key_pointer|
          break unless store_pointer

          store_pointer = traverse(key_pointer.index, store_pointer, key_pointer.value) if key_pointer.not_last?
        end

        store_pointer
      end

      def entry(key, pointer, part)
        data = pointer.dig(part, DATA_KEY)
        meta = pointer.dig(part, META_KEY)

        return unless data

        Entry.new(
          key.to_s,
          data: data,
          created_at: meta[CREATED_AT_KEY],
          updated_at: meta[UPDATED_AT_KEY]
        )
      end

      def entries(key, pointer, part, limit: nil, skip: nil)
        pointer = pointer.dig(part, IDS_KEY)

        return [] unless pointer

        start_index   = skip || 0
        end_index     = limit ? (start_index + limit - 1) : -1
        selected_keys = pointer.keys[start_index..end_index] || []

        selected_keys.map do |selected_key|
          value = pointer[selected_key]

          Entry.new(
            key.to_s(selected_key),
            data: value[DATA_KEY],
            created_at: value[META_KEY][CREATED_AT_KEY],
            updated_at: value[META_KEY][UPDATED_AT_KEY]
          )
        end
      end

      def record_prototype
        {
          DATA_KEY => {},
          INDICES_KEY => {},
          META_KEY => {
            CREATED_AT_KEY => Time.now.utc,
            UPDATED_AT_KEY => Time.now.utc
          }
        }
      end
    end
  end
end