O TEMA DO FÓRUM ESTÁ EM MANUTENÇÃO. FEEDBACKS AQUI: ACESSAR

MBS - Database

Iniciado por Brandt, 19/01/2015 às 14:09

19/01/2015 às 14:09 Última edição: 19/01/2015 às 17:32 por Masked
MBS - Database
por Masked




Características


O script traz um módulo de banco de dados simples, com tabelas, colunas e entradas nas quais você pode armazenar valores (no momento, apenas textos)
O script também permite que os arquivos de banco de dados sejam encriptados.

Instruções


No script, nos comentários em verde


Script


#==============================================================================
# MBS - Database
#------------------------------------------------------------------------------
# por Masked
#==============================================================================
#==============================================================================
# Instruções
#------------------------------------------------------------------------------
# Para criar um banco de dados, use o seguinte comando:
# MBS::MDB.create_database("nome") # nome = Nome do banco de dados
#
# Para criar uma tabela no banco de dados, você pode, ao criar o banco de dados
# criar um bloco 'do...end' e criar as tabelas que quiser a partir da 
# referência passada nele (entre '|'):
# MBS::MDB.create_database('database') do |database|
#   database.create_table('tabela')
# end
#
# Para criar uma coluna na tabela use o bloco 'do...end' gerado pelo 
# create_table:
# MBS::MDB.create_database('database') do |database|
#   database.create_table('tabela') do |table|
#     table.create_column('coluna')
#   end
# end
#
# Para criar uma entrada, faça como com as colunas, porém chamando o método
# add_entry:
# MBS::MDB.create_database('database') do |database|
#   database.create_table('tabela') do |table|
#     table.create_column('coluna1')
#     table.create_column('coluna2')
#     table.add_entry('entrada1', 'valor da coluna1', 'valor da coluna2')
#   end
# end
#
# Para salvar os bancos de dados criados:
# MBS::MDB.save
#
# Para carregar os bancos de dados salvos:
# MBS::MDB.load
#
# Para obter o valor de uma coluna em uma entrada:
# MBS::MDB.get('database.tabel.coluna1.entrada1.coluna1') #=> valor da coluna1
#
# Para selecionar uma lista de entradas que atendam a uma condição:
# MBS::MDB.select do |values|
#   values.column1 == '1ª coluna' && (
#   values.column2 == '2ª coluna' || values.column3 == '3ª coluna')
# end
#
# A condição dentro do select é uma condição normal em Ruby, 'values' é o nome 
# da variável que terá os valores das colunas da entrada e as colunas são 
# acessadas através de atributos de 'values' com o nome delas 
# 
# Ex.: 
# Para pegar todas as entradas com o valor da coluna 'coluna_ex' igual a 'oi':
# MBS::MDB.select do |values|
#   values.coluna_ex == 'oi'
# end
#------------------------------------------------------------------------------
# Obs.: Os nomes dos bancos de dados/tabelas/colunas/entradas não podem conter
# caracteres especiais ou espaço (podendo ser formados com a-z, 0-9 e _)
#==============================================================================
($imported ||= {})[:mbs_database] = true
module MBS
  module MDB
#==============================================================================
# Configurações
#==============================================================================

    # Nome da pasta onde ficarão os arquivos de banco de dados
    FOLDER    = 'Database'
    
    # Extensão usada nos arquivos de banco de dados
    EXTENSION = 'rvdb2'
    
    # Caso queira que o banco de dados seja encriptado, mude para true, se não
    # deixe como false
    ENCRYPT = false
    
    # Chave de encriptação
    CRYPT_KEY = 'maçã'
    
#==============================================================================
# Fim das Configurações
#==============================================================================
  end
end
#==============================================================================
# >> MBS
#==============================================================================
module MBS
  #==========================================================================
  # >> TriangleCrypt
  #--------------------------------------------------------------------------
  # Módulo de encriptação de texto usando números triangulares
  #==========================================================================
  module TriangleCrypt
    
    extend self
    
    #------------------------------------------------------------------------
    # > Geração do número triangular de número X
    #    number  :  Número a ser transformado em triangular
    #------------------------------------------------------------------------
    def tri(n)
      return (n + n ** 2) / 2
    end

    #------------------------------------------------------------------------
    # > Reversão do processo feito pelo método 'tri'
    #    number  :  Número triangular
    #------------------------------------------------------------------------
    def atri(n)
      return ((1 + n * 8) ** 0.5) / 2
    end
    
    #------------------------------------------------------------------------
    # > Encriptação
    #    text  : Texto a ser encriptado
    #    key   : Chave usada na encriptação
    #------------------------------------------------------------------------
    def encrypt(text, key)
      tchars = text.unpack('S*C*').collect {|c| c}
      kchars = key.unpack('U*').collect {|c| tri(c)}
      array = []
      tchars.each_with_index do |char, i|
        array << tri(char + kchars[i % kchars.size])
      end
      array.pack('V*S*C*')
    end
    
    #------------------------------------------------------------------------
    # > Encriptação
    #    text  : Texto a ser encriptado
    #    key   : Chave usada na encriptação
    #------------------------------------------------------------------------
    def decrypt(text, key)
      tchars = text.unpack('V*S*C*').collect {|c| c}
      kchars = key.unpack('U*').collect {|c| tri(c)}
      array = []
      tchars.each_with_index do |char, i|
        array << atri(char) - kchars[i % kchars.size]
      end
      array.pack('S*C*')
    end
    
    private :tri, :atri
    
  end
  
  #============================================================================
  # >> MDB
  #============================================================================
  module MDB
    LOG = []
    CHARS = /[a-z0-9_]/
    @databases = {}

#==============================================================================
# ERROS
#==============================================================================

    #==========================================================================
    # ** DatabaseError
    #--------------------------------------------------------------------------
    # Esta é a classe genérica dos erros de banco de dados
    #==========================================================================
    class DatabaseError < RuntimeError
      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(msg)
        super
        MDB::LOG << msg
      end
    end

    #==========================================================================
    # ** NoDatabaseError
    #--------------------------------------------------------------------------
    # Esta é a classe do erro causado quando um banco de dados chamado não
    # existe
    #==========================================================================
    class NoDatabaseError < DatabaseError
      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(db)
        super("Banco de dados inexistente: `#{db}'")
      end
    end

    #==========================================================================
    # ** NoTableDatabaseError
    #--------------------------------------------------------------------------
    # Esta é a classe do erro causado quando uma tabela chamada não existe no
    # banco de dados
    #==========================================================================
    class NoTableDatabaseError < DatabaseError
      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(db, tb)
        super("[DATABASE `#{db}'] Tabela inexistente: `#{tb}'")
      end
    end

    #==========================================================================
    # ** NoColumnDatabaseError
    #--------------------------------------------------------------------------
    # Esta é a classe do erro causado quando uma coluna chamada não existe na
    # tabela
    #==========================================================================
    class NoColumnDatabaseError < DatabaseError
      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(db, tb, cl)
        super("[DATABASE `#{db}', TABLE `#{tb}'] Coluna inexistente: `#{cl}'")
      end
    end

    #==========================================================================
    # ** NoEntryDatabaseError
    #--------------------------------------------------------------------------
    # Esta é a classe do erro causado quando uma entrada chamada não existe na
    # tabela
    #==========================================================================
    class NoEntryDatabaseError < DatabaseError
      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(db, tb, en)
        super("[DATABASE `#{db}', TABLE `#{tb}'] Entrada inexistente: `#{en}'")
      end
    end

    #==========================================================================
    # ** NoValueDatabaseError
    #--------------------------------------------------------------------------
    # Esta é a classe do erro causado quando um valor chamado não existe na
    # entrada
    #==========================================================================
    class NoValueDatabaseError < DatabaseError
      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(db, tb, en, i)
        super("[DATABASE `#{db}', TABLE `#{tb}', ENTRY `#{en}'] Valor no índice #{i} inexistente")
      end
    end

    #==========================================================================
    # ** ValueNumberDatabaseError
    #--------------------------------------------------------------------------
    # Esta é a classe do erro causado quando o número de valores passados
    # é diferente do número de valores esperado
    #==========================================================================
    class ValueNumberDatabaseError < DatabaseError
      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(db, tb, en)
        super("[DATABASE `#{db}', TABLE `#{tb}', ENTRY `#{en}'] O número de colunas recebido não bate com o número esperado")
      end
    end

    #==========================================================================
    # ** DBPathDatabaseError
    #--------------------------------------------------------------------------
    # Esta é a classe do erro gerado quando um endereço no banco de dados
    # passado é inválido
    #==========================================================================
    class DBPathDatabaseError < DatabaseError
      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(path)
        super("O caminho `#{path}' não pôde ser encontrado no banco de dados")
      end
    end

#==============================================================================
# ESTRUTURAS
#==============================================================================

#==========================================================================
# ** Database
#--------------------------------------------------------------------------
# Esta é a classe dos bancos de dados
#==========================================================================
    class Database
      #------------------------------------------------------------------------
      # * Definição dos atributos legíveis
      #------------------------------------------------------------------------
      attr_reader :name, :tables

      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(name)
        if name =~ /^#{CHARS}+$/
          @name = name
        else
          raise DatabaseError.new("O nome do banco de dados deve ser composto apenas por letras, números ou underline (a-z, 0-9 e _)")
        end
        @tables = {}
      end

      #------------------------------------------------------------------------
      # * Criação de uma tabela no banco de dados
      #------------------------------------------------------------------------
      def create_table(name)
        @tables[name] = Table.new(name, self)
        yield @tables[name]
      end

      #------------------------------------------------------------------------
      # * Remoção de uma tabela do banco de dados
      #------------------------------------------------------------------------
      def remove_table(name)
        if @tables[name]
          @tables.delete(name)
        else
          raise NoTableDatabaseError.new(@name, name)
        end
      end

      #------------------------------------------------------------------------
      # * Aquisição de uma tabela no banco de dados
      #------------------------------------------------------------------------
      def table(name)
        if @tables[name]
          return @tables[name]
        else
          raise NoTableDatabaseError.new(@name, name)
        end
      end
    end

    #==========================================================================
    # ** Table
    #==========================================================================
    class Table
      #------------------------------------------------------------------------
      # * Definição dos atributos legíveis
      #------------------------------------------------------------------------
      attr_reader :name, :entries, :columns, :parent

      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(name, parent)
        if name =~ /^#{CHARS}+$/
          @name = name
        else
          raise DatabaseError.new("[DATABASE `#{parent.parent.name}'] O nome da tabela deve ser composto apenas por letras, números ou underline (a-z, 0-9 e _)")
        end
        @parent = parent
        @columns = []
        @entries = {}
      end

      #------------------------------------------------------------------------
      # * Criação de uma coluna na tabela
      #------------------------------------------------------------------------
      def create_column(name)
        @columns << name unless @columns.include?(name)
        @entries.each do |entry|
          entry.values << ''
        end
      end

      #------------------------------------------------------------------------
      # * Remoção de uma coluna da tabela
      #------------------------------------------------------------------------
      def remove_column(name)
        if @columns[name]
          @entries.each do |entry|
            entry.delete_value(@columns.index(name))
          end
          @columns.delete(name)
        else
          raise NoColumnDatabaseError.new(@name, @parent.name, name)
        end
      end

      #------------------------------------------------------------------------
      # * Adição de uma nova entrada na tabela
      #------------------------------------------------------------------------
      def add_entry(name, *values)
        if values.size == @columns.size
          @entries[name] = Entry.new(name, values, self)
        else
          raise ValueNumberDatabaseError.new(@parent.name, @name, name)
        end
      end
      
      #------------------------------------------------------------------------
      # * Aquisição de uma entrada a partir do nome dela
      #------------------------------------------------------------------------
      def entry(name)
        if @entries[name]
          return @entries[name]
        else
          raise NoEntryDatabaseError.new(@parent.name, @name, name)
        end
      end
    end
    
    #==========================================================================
    # ** Entry
    #==========================================================================
    class Entry
      #------------------------------------------------------------------------
      # * Definição dos atributos legíveis
      #------------------------------------------------------------------------
      attr_reader :name, :parent, :values

      #------------------------------------------------------------------------
      # * Inicialização do objeto
      #------------------------------------------------------------------------
      def initialize(name, values, parent)
        if name =~ /^#{CHARS}+$/
          @name = name
        else
          raise DatabaseError.new("[DATABASE `#{parent.parent.name}', TABLE `#{parent.name}'] O nome da entrada deve ser composto apenas por letras, números ou underline (a-z, 0-9 e _)")
        end
        @parent = parent
        @values = Values.new(self, *values)
      end

      #------------------------------------------------------------------------
      # * Adição de um valor à lista de valores da entrada
      #------------------------------------------------------------------------
      def add_value(value)
        @values << value
      end

      #------------------------------------------------------------------------
      # * Remoção de um valor no índice X da lista de valores da entrada
      #------------------------------------------------------------------------
      def remove_value(index)
        if @values[index]
          @values.delete_at index
        else
          raise NoValueDatabaseError.new(@parent.parent.name, @parent.name, @name, index)
        end
      end
      
      #------------------------------------------------------------------------
      # * Aquisição do valor na coluna 'cl' da entrada
      #------------------------------------------------------------------------
      def value(cl)
        if i = @parent.columns.index(cl)
          return @values[i]
        else
          raise NoColumnDatabaseError.new(@parent.parent.name, @parent.name, cl)
        end
      end      
    end
    
    #==========================================================================
    # ** Entry::Values
    #==========================================================================
    class Entry::Values
      
      attr_reader :keys
      
      def initialize(parent, *args)
        @keys = []
        @parent = parent
        args.each_with_index do |a, i|
          @keys[i] = parent.parent.columns[i]
          add_value(@keys[i], a)
        end
      end
      
      def add_value(key, v)
        instance_eval("@#{key} = #{v.inspect}; def #{key}; @#{key}; end")
      end
      
      def <<(v)
        add_value(parent.parent.columns[@keys.size], v)
      end
      
      def delete_at(i)
        eval("@#{keys[i]} = nil")
      end
      
      def [](index)
        return eval "@#{@keys[index]}"
      end
      
      def each
        @keys.size.times do |i|
          yield self[i]
        end
      end
      
    end
#==============================================================================
# MÉTODOS
#==============================================================================

    #==========================================================================
    # <> DBPath
    #--------------------------------------------------------------------------
    # Esta estrutura é usada para controlar os caminhos no banco de dados
    #==========================================================================
    DBPath = Struct.new(:database, :table, :entry, :column) do
      #------------------------------------------------------------------------
      # * Inicialização da estrutura
      #------------------------------------------------------------------------
      def initialize(str)
        split = str.split('.')
        if split.size == 4
          self.database = split[0]
          self.table    = split[1]
          self.entry    = split[2]
          self.column   = split[3]
        else
          raise DBPathDatabaseError.new(str)
        end
      end
    end

    #--------------------------------------------------------------------------
    # * Criação de um banco de dados
    #--------------------------------------------------------------------------
    def self.create_database(name)
      @databases[name] = Database.new(name)
      yield @databases[name]
      @databases[name]
    end
    
    #--------------------------------------------------------------------------
    # * Aquisição de um valor no banco de dados
    #--------------------------------------------------------------------------
    def self.get(path)
      path = DBPath.new(path)
      if db = @databases[path.database]
        return db.table(path.table).entry(path.entry).value(path.column)
      else
        raise NoDatabaseError.new(path.database)
      end
    end
    
    #--------------------------------------------------------------------------
    # * Salvamento dos bancos de dados em arquivos
    #--------------------------------------------------------------------------
    def self.save
      Dir.mkdir(FOLDER) unless FileTest.directory?(FOLDER)
      @databases.each do |name, db|
        File.open("#{FOLDER}/#{name}#{EXTENSION.empty? ? '' : ".#{EXTENSION}"}", 'wb') do |file|
          
          txt = ""
          
          txt += "MDB100ACE[#{name}]\r\n"
          
          db.tables.each do |ke, tb|
            
            
            
            txt += "\tTABLE[#{ke}](#{tb.columns.to_s[1...-1]})\r\n"
            
            tb.entries.each do |k, en|
              txt += "\t\tENTRY[#{k}]\r\n"
              
              tb.columns.each do |cl|
                txt += "\t\t\t#{cl}=#{en.value(cl).inspect}\r\n"
              end
              
              txt += "\t\tENDENTRY\r\n"
            end
            
            txt += "\tENDTABLE\r\n"
          end
          
          txt += 'EOF'
          
          file.write(ENCRYPT ? encrypt(txt) : txt)
        end
      end
    end
    
    #--------------------------------------------------------------------------
    # * Carregamento dos bancos de dados a partir de arquivos
    #--------------------------------------------------------------------------
    def self.load
      return unless FileTest.directory?(FOLDER)
      Dir.entries(FOLDER).each do |filename|
        next unless FileTest.file? "#{FOLDER}/#{filename}"
        str = File.open("#{FOLDER}/#{filename}", 'rb') {|file| ENCRYPT ? decrypt(file.read) : file.read}
        db = nil
        tb = nil
        cls = []
        en = nil
        vls = {}
        tbls = []
        ents = []
        str.each_line do |line|
          if line =~ /^\s*MDB100ACE\[(\S+)\]\s*$/i
            unless db
              db = $1
            else
              raise DatabaseError.new("Não é possível criar um banco de dados dentro de outro")
            end
          elsif line =~ /^\s*TABLE\[(.+)\]\((.+)\)\s*$/i
            if !tb && db
              tb = $1
              cls = eval("[#{$2}]")
            elsif tb
              raise DatabaseError.new("Não é possível criar uma tabela dentro de outra")
            else
              raise DatabaseError.new("Não é possível criar uma tabela fora de um banco de dados")
            end
          elsif line =~ /^\s*ENTRY\[(.+)\]\s*$/i
            if !en && tb
              en = $1
            elsif en
              raise DatabaseError.new("Não é possível criar uma entrada dentro de outra")
            else
              raise DatabaseError.new("Não é possível criar uma entrada fora de uma tabela")
            end
          elsif line =~ /^\s*([^"]+)=(".+")\s*$/i
            if (i = cls.index($1)) && en
              (vls[en] ||= [])[i] = eval($2)
            elsif en
              raise NoColumnDatabaseError(db, tb, $1)
            else
              raise DatabaseError.new("Não é possível criar um valor fora de uma entrada")
            end
          elsif line =~ /^\s*ENDTABLE\s*$/i
            if tb
              tbls << tb
            else
              raise DatabaseError.new("Não é possível fechar uma tabela antes de abrí-la")
            end
            tb = nil
          elsif line =~ /^\s*ENDENTRY\s*$/i
            if en
              ents << en
            else
              raise DatabaseError.new("Não é possível fechar uma tabela antes de abrí-la")
            end
            en = nil
          elsif line =~ /^\s*EOF[\s\0]*$/i
            if db
              create_database(db) do |database|
                tbls.each do |k|
                  database.create_table(k) do |table|
                    cls.each {|cl| table.create_column(cl)}
                    ents.each {|en| table.add_entry(en, *vls[en])}
                  end
                end
              end
            else
              raise DatabaseError.new("Não é possível terminar o arquivo antes de criar um banco de dados")
            end
            return
          end
        end
      end
    end
    
    #--------------------------------------------------------------------------
    # * Seleção de entradas que tenham os valores X
    #--------------------------------------------------------------------------
    def self.select
      list = []
      databases.values.each do |db|
        db.tables.values.each do |tb|
          list += tb.entries.values.select do |en|
            yield en.values
          end
        end
      end
      return list
    end
    
    #--------------------------------------------------------------------------
    # * Aquisição da lista de bancos de dados
    #--------------------------------------------------------------------------
    def self.databases
      return @databases
    end
    
    #--------------------------------------------------------------------------
    # * Aquisição de um banco de dados a partir de seu nome
    #--------------------------------------------------------------------------
    def self.database(name)
      return @databases[name]
    end
    
    #--------------------------------------------------------------------------
    # * Encriptação
    #--------------------------------------------------------------------------
    def self.encrypt(txt)
      TriangleCrypt.encrypt(txt, CRYPT_KEY)
    end
    
    #--------------------------------------------------------------------------
    # * Encriptação
    #--------------------------------------------------------------------------
    def self.decrypt(txt)
      TriangleCrypt.decrypt(txt, CRYPT_KEY)
    end
  end
end
#==============================================================================
# >> DataManager
#==============================================================================
module DataManager
  #============================================================================
  # alias DataManager
  #============================================================================
  class << self
    alias mbsdblddb load_database
    alias mbsdbsvgm save_game
  end
  #----------------------------------------------------------------------------
  # * Carregamento do Banco de Dados
  #----------------------------------------------------------------------------
  def self.load_database
    mbsdblddb
    MBS::MDB.load
  end
  #----------------------------------------------------------------------------
  # * Salvar Jogo
  #     index : índice
  #----------------------------------------------------------------------------
  def self.save_game(index)
    mbsdbsvgm(index)
    MBS::MDB.save
  end
end



Créditos


- a mim, por criar
~ Masked

Ele fica no mesmo "formato" que os outros arquivos em "Data"? Ou só possui a mesma extensão com outro formato?

Se entendi direito (se ele funciona igual ao banco de dados do RPG Maker) então não, na verdade acho que você entendeu errado o script, o que ele faz é criar um sistema parecido (e bem simplificado) com um banco de dados SQL, diferente do que o RM faz que é serializar um objeto Ruby no arquivo (Marshal.dump) pra ser carregado depois. Se você quiser dá até pra montar o banco de dados direto pelo arquivo já que ele é só um arquivo de texto normal, por exemplo:
MDB100ACE[database]
    TABLE[tabela]("coluna1", "coluna2")
        ENTRY[entrada]
            coluna1 = "texto da coluna 1"
            coluna2 = "texto da coluna 2"
        ENDENTRY
    ENDTABLE
EOF


Você poderia escrever isso em uma tabela se quisesse:


  nome da entrada 
coluna1
coluna2
entrada 
  texto da coluna 1    texto da coluna 2 
~ Masked