├── .debt_ceiling.rb ├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGLELOG ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bench.rb ├── bin ├── console ├── gkv └── setup ├── example ├── Gemfile ├── README.md └── server.rb ├── gkv.gemspec ├── lib ├── gkv.rb └── gkv │ ├── database.rb │ ├── git.rb │ └── version.rb └── spec ├── gkv_spec.rb └── spec_helper.rb /.debt_ceiling.rb: -------------------------------------------------------------------------------- 1 | DebtCeiling.configure do |c| 2 | c.whitelist = %w(bin lib bench example) 3 | c.max_debt_per_module = 30 4 | c.debt_ceiling = 75 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.1 4 | before_install: gem install bundler -v 1.10.3 5 | 6 | -------------------------------------------------------------------------------- /CHANGLELOG: -------------------------------------------------------------------------------- 1 | v0.2.8 2 | Remove shell injection vulnerability 3 | 4 | v0.2.7 5 | Major refactors, no API changes 6 | 7 | v0.2.6 8 | add `all_versions` method 9 | 10 | v0.2.5 11 | Revert to stdin piping for object hashing 12 | 13 | v0.2.4 14 | Switch `all` method to use hashrocket syntax for pre-Ruby 2.2.x compatibility 15 | 16 | v0.2.3 17 | Add operator overloading for set/get sytax 18 | 19 | v0.2.2 20 | Full type system. All bugs related to hashes from 0.2.0 and 0.2.1 fixed. 21 | 22 | v0.1.x 23 | Set strings. Get strings 24 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # [DWTFYW](http://www.wtfpl.net/) 2 | # Also, don't be a dick. 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in gkv.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: "bundle exec rspec" do 2 | require "guard/rspec/dsl" 3 | dsl = Guard::RSpec::Dsl.new(self) 4 | 5 | # RSpec files 6 | rspec = dsl.rspec 7 | watch(rspec.spec_helper) { rspec.spec_dir } 8 | watch(rspec.spec_support) { rspec.spec_dir } 9 | watch(rspec.spec_files) 10 | 11 | # Ruby files 12 | ruby = dsl.ruby 13 | dsl.watch_spec_files_for(ruby.lib_files) 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gkv 2 | 3 | [![Join the chat at https://gitter.im/ybur-yug/gkv](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ybur-yug/gkv?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [![Gem Version](https://badge.fury.io/rb/gkv.svg)](http://badge.fury.io/rb/gkv) 6 | [![Build Status](https://travis-ci.org/ybur-yug/gkv.svg?branch=master)](https://travis-ci.org/ybur-yug/gkv) 7 | 8 | 9 | Gkv is a simple git wrapper that allows you to use it as a kv store 10 | 11 | ![proof in our pudding](http://i.imgur.com/EKdt7oR.png) 12 | 13 | The documentation says thats what it does. So why not yo? 14 | 15 | ## Installation 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | ```ruby 20 | gem 'gkv' 21 | ``` 22 | 23 | And then execute: 24 | 25 | $ bundle 26 | 27 | Or install it yourself as: 28 | 29 | $ gem install gkv 30 | 31 | ## API 32 | Types are implicitly understood, and are automatically set/loaded. Only symbols are excluded. 33 | There are 4 main functions: 34 | 35 | ### Set 36 | 37 | db[*key*] = *value* 38 | 39 | ```ruby 40 | db = Gkv::Database.new 41 | db['Pants'] = 'red leather' 42 | # => 'red leather' 43 | ``` 44 | This allows a shorthand notation using operator overloading to set without invoking `set` directly. 45 | 46 | 47 | set(*key*, *value*) 48 | 49 | ```ruby 50 | db = Gkv::Database.new 51 | db.set('key', '12') 52 | # => 'key' 53 | db.set('test', 12) 54 | # => 'test' 55 | ``` 56 | 57 | ### Get 58 | 59 | db[*key*] 60 | 61 | ```ruby 62 | db = Gkv::Database.new 63 | db['Pants'] 64 | # => 'red leather' 65 | ``` 66 | This allows a shorthand notation using operator overloading to get without invoking `get` directly. 67 | 68 | 69 | get(*key*) 70 | 71 | ```ruby 72 | db = Gkv::Database.new 73 | db.set('apples', '10') 74 | # => 'apples' 75 | db.get('apples') 76 | # => '10' 77 | ``` 78 | 79 | The type is inferred from when you initially set the value. Note that saving the string `'1'` will 80 | return the integer `1` due to the naive nature of the implementation. Hashes, arrays and booleans 81 | behave as expected when saved. 82 | 83 | ### Get Version 84 | 85 | get_version(*version*, *key*) 86 | 87 | ```ruby 88 | db = Gkv::Database.new 89 | db.set('apples', '20') 90 | # => 'apples' 91 | db.set('apples', '50') 92 | # => 'apples' 93 | db.get_version(1, 'apples') 94 | # => '20' 95 | db.get_version(2, 'apples') 96 | # => '50' 97 | ``` 98 | 99 | ### All 100 | 101 | all 102 | 103 | ```ruby 104 | db.set('apples', 20.0) 105 | db.set('ants', 'pants') 106 | db.set('things', {}) 107 | db.all 108 | # =>[{ 'apples': 20.0 }, { 'ants': 'pants'}, { 'things': {} }] 109 | ``` 110 | 111 | all_versions(*key*) 112 | 113 | ```ruby 114 | db.set('apples', 5) 115 | db.set('apples', 'pants') 116 | db.set('apples', 5.0) 117 | db.all_versions('apples') 118 | # => [ 5, 'pants', 5.0] 119 | ``` 120 | 121 | ### Destroy 122 | 123 | ```ruby 124 | db.set('apples', 20.0) 125 | db.set('ants', 'pants') 126 | db.set('things', {}) 127 | db.destroy! 128 | db['apples'] 129 | # => KeyError 130 | db['ants'] 131 | # => KeyError 132 | db['things'] 133 | # => KeyError 134 | ``` 135 | 136 | ## Usage 137 | 138 | ```ruby 139 | db = Gkv::Database.new 140 | 141 | db.set('Apples', '10') 142 | # => 'Apples' 143 | db.get('Apples') 144 | # => '10' 145 | 146 | # update some values 147 | db.set('Apples', '12') 148 | # => 'Apples' 149 | db.get('Apples') 150 | # => '12' 151 | db.get_version(1, 'Apples') 152 | #=> '10' 153 | 154 | # keys that do not exist return KeyError 155 | db.get('magic') 156 | # => KeyError 157 | ``` 158 | 159 | ## Development 160 | 161 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the 162 | tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 163 | 164 | It is a typical bundle install to get things running. We use Guard for testing. To initialize it simply 165 | run 166 | 167 | `$ guard` 168 | 169 | And you will be good to go! 170 | 171 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, 172 | update the version number in `version.rb`, and then run `bundle exec rake release`, which will create 173 | a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 174 | 175 | ### Dev Roadmap 176 | #### 0.3 177 | - Remote synchronization & Backup [] 178 | - Persistance & Dump Loading [x] 179 | - Stop wrapping git via it's CLI [] 180 | 181 | ## Contributing 182 | Feel free to check out the gitter room and ask whats on the agenda. 183 | 184 | Bug reports and pull requests are welcome on GitHub at https://github.com/ybur-yug/gkv. 185 | 186 | ## License 187 | 188 | See [this](http://www.wtfpl.net/about/). 189 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | require 'mutant' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task :fuzz do 9 | status = Mutant::CLI.run(%w[--use rspec --require gkv --use rspec Gkv::GitFunctions]) 10 | 11 | if status.nonzero? 12 | puts 'Mutant task was not successful' 13 | else 14 | puts 'Mutant task was successful' 15 | end 16 | end 17 | 18 | task :default => [ :spec, :fuzz ] 19 | -------------------------------------------------------------------------------- /bench.rb: -------------------------------------------------------------------------------- 1 | require 'gkv' 2 | require 'benchmark' 3 | 4 | gkv = Gkv::Database.new 5 | 6 | n = 1000 7 | 8 | Benchmark.bm do |x| 9 | x.report("set:\t\t") { n.times { gkv.set('Apple', 10) } } 10 | x.report("hash set:\t") { n.times { gkv['Apple'] = 10 } } 11 | x.report("get:\t\t") { n.times { gkv.get('Apple') } } 12 | x.report("hash get:\t") { n.times { gkv['Apple'] } } 13 | end 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'gkv' 5 | 6 | DB = Gkv::Database.new 7 | 8 | require 'irb' 9 | IRB.start 10 | -------------------------------------------------------------------------------- /bin/gkv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gkv' 4 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gem 'sinatra' 5 | gem 'gkv' 6 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Setup 4 | Start the server 5 | 6 | $ bundle 7 | $ ruby server.rb 8 | 9 | With this, in another terminal start IRB: 10 | 11 | ```ruby 12 | require 'net/http' 13 | require 'uri' 14 | 15 | set_uri = URI('http://localhost:4567/set') 16 | get_uri = URI('http://localhost:4567/get') 17 | 18 | set_params = { :key => 'your_key', :value => 'your value' } 19 | get_params = { :key => 'your_key' } 20 | set_uri.query = URI.encode_www_form(set_params) 21 | get_uri.query = URI.encode_www_form(set_params) 22 | 23 | set_resp = Net::HTTP.get(set_uri) 24 | get_resp = Net::HTTP.get(get_uri) 25 | puts set_resp 26 | # => {"key_set":"your_key"} 27 | puts get_resp 28 | # => your value 29 | ``` 30 | -------------------------------------------------------------------------------- /example/server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | require 'gkv' 4 | 5 | DB = Gkv::Database.new 6 | 7 | get '/' do 8 | { all_items: DB.all }.to_json 9 | end 10 | 11 | get '/set' do 12 | begin 13 | key, value = params.fetch('key'), params.fetch('value') 14 | DB.set(key, value) 15 | { key_set: key }.to_json 16 | rescue KeyError 17 | { error: 'error setting key/value pair, please send key and value params' }.to_json 18 | end 19 | end 20 | 21 | get '/get' do 22 | begin 23 | key = params.fetch('key') 24 | DB.get(key) 25 | rescue KeyError 26 | { error: 'error retrieving key. it either does not exist or you did not send a parameter `key`' }.to_json 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /gkv.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'gkv/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "gkv" 8 | spec.version = Gkv::VERSION 9 | spec.authors = ["="] 10 | spec.email = ["="] 11 | 12 | spec.summary = %q{Git as a key:value store.} 13 | spec.description = %q{Utilize a git repository as a trivially simple kv store.} 14 | spec.homepage = "https://www.github.com/ybur-yug/gkv" 15 | spec.license = "MIT" 16 | 17 | # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or 18 | # delete this section to allow pushing this gem to any host. 19 | 20 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 21 | spec.bindir = "bin" 22 | spec.executables = "gkv" 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_development_dependency "bundler", "~> 1.10" 26 | spec.add_development_dependency "rake", "~> 10.0" 27 | spec.add_development_dependency "rspec" 28 | spec.add_development_dependency "mutant", "~> 0.8.0" 29 | spec.add_development_dependency "mutant-rspec", "~> 0.8.0" 30 | spec.add_development_dependency "pry" 31 | spec.add_development_dependency "debt_ceiling" 32 | spec.add_development_dependency "guard-rspec" 33 | end 34 | -------------------------------------------------------------------------------- /lib/gkv.rb: -------------------------------------------------------------------------------- 1 | require 'gkv/version' 2 | require 'gkv/git' 3 | require 'gkv/database' 4 | 5 | module Gkv 6 | end 7 | -------------------------------------------------------------------------------- /lib/gkv/database.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | $ITEMS = {} 4 | 5 | module Gkv 6 | class Database 7 | include Gkv::DbFunctions 8 | 9 | def initialize 10 | `git init` 11 | @git = Gkv::GitFunctions 12 | end 13 | 14 | def []=(key, value) 15 | set(key, value) 16 | end 17 | 18 | def set(key, value) 19 | update_items(key, YAML.dump(value)) 20 | key 21 | end 22 | 23 | def [](key) 24 | get(key) 25 | end 26 | 27 | def get_version(version, key) 28 | YAML.load(@git.cat_file($ITEMS[key][version.to_i - 1])) 29 | end 30 | 31 | def all_versions(key) 32 | $ITEMS[key].map { |hash| YAML.load(@git.cat_file(hash)) } 33 | end 34 | 35 | def get(key) 36 | if $ITEMS.keys.include?(key) 37 | YAML.load(@git.cat_file($ITEMS[key].last)) 38 | else 39 | raise KeyError 40 | end 41 | end 42 | 43 | def all 44 | $ITEMS.keys.map { |key| 45 | { "#{key}" => YAML.load(@git.cat_file($ITEMS[key].last)) } 46 | } 47 | end 48 | 49 | def save 50 | hash = @git.hash_object(YAML.dump($ITEMS)) 51 | File.open(".database", 'w') { |f| f.write(hash) } 52 | hash 53 | end 54 | 55 | def load(hash) 56 | begin 57 | $ITEMS = YAML.load(@git.cat_file(File.open('.database').read)) 58 | rescue 59 | $ITEMS 60 | end 61 | end 62 | 63 | def destroy! 64 | $ITEMS = {} 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/gkv/git.rb: -------------------------------------------------------------------------------- 1 | require "open3" 2 | 3 | module Gkv 4 | module GitFunctions 5 | extend self 6 | 7 | def hash_object(data) 8 | Open3.popen3("git hash-object -w --stdin") do |stdin, stdout, stderr| 9 | stdin.write data 10 | stdin.close 11 | stdout.read.strip 12 | end 13 | end 14 | 15 | def cat_file(hash) 16 | `git cat-file -p #{hash}` 17 | end 18 | end 19 | 20 | module DbFunctions 21 | def update_items(key, value) 22 | if $ITEMS.keys.include? key 23 | history = $ITEMS[key] 24 | history << Gkv::GitFunctions.hash_object(value) 25 | $ITEMS[key] = history 26 | else 27 | $ITEMS[key] = [Gkv::GitFunctions.hash_object(value)] 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/gkv/version.rb: -------------------------------------------------------------------------------- 1 | module Gkv 2 | VERSION = '0.2.8' 3 | end 4 | -------------------------------------------------------------------------------- /spec/gkv_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Gkv do 4 | let(:db) { Gkv::Database.new } 5 | 6 | before(:each) { clear_db } 7 | 8 | context "as a gem" do 9 | it 'has a version number' do 10 | expect(Gkv::VERSION).not_to be nil 11 | end 12 | end 13 | 14 | context "git functions" do 15 | it 'hash does not have excess newlines' do 16 | data = Gkv::GitFunctions::hash_object("hello") 17 | expect(data).to eq data.strip 18 | end 19 | end 20 | 21 | context "on set" do 22 | it 'sets a key' do 23 | load_db([{ 'Apples' => 10 }]) 24 | expect(db.get('Apples')).to eq 10 25 | end 26 | 27 | it 'sets a key with hash syntax' do 28 | db['Apples'] = 10 29 | expect(db['Apples']).to eq 10 30 | end 31 | 32 | it 'modifies a key' do 33 | load_db([{ 'Apples' => 12 }, { 'Apples' => 10 }]) 34 | expect(db.get('Apples')).to eq 10 35 | end 36 | 37 | it 'modifies a key with hash syntax' do 38 | db['Apples'] = 12 39 | db['Apples'] = 10 40 | expect(db['Apples']).to eq 10 41 | end 42 | 43 | it 'keeps a history of a keys values' do 44 | load_db([{ 'Pants' => 10 }, { 'Pants' => 'oats' }]) 45 | expect(db.get_version(1, 'Pants')).to eq 10 46 | expect(db.get_version(2, 'Pants')).to eq 'oats' 47 | end 48 | 49 | it 'can set values containing quotes' do 50 | db['TestingQuotes'] = %(single ' and double " quote) 51 | expect(db['TestingQuotes']).to eq %(single ' and double " quote) 52 | end 53 | 54 | it 'can return a float type' do 55 | load_db([{ 'Pants' => 10.0 }]) 56 | expect(db.get('Pants')).to eq 10.0 57 | end 58 | 59 | it 'can return an integer type' do 60 | load_db([{ 'Pants' => 10 }]) 61 | expect(db.get('Pants')).to eq 10 62 | end 63 | 64 | context "when saving hashes" do 65 | it 'deals with ruby 2 hashes' do 66 | load_db([{ 'stuff' => { key: "value" } }]) 67 | expect(db.get('stuff')).to be_an_instance_of Hash 68 | expect(db.get('stuff')).to eq({ key: 'value' }) 69 | end 70 | 71 | it 'deals with old hash syntax' do 72 | load_db([{ 'stuff' => { :key => "value" } }]) 73 | expect(db.get('stuff')).to be_an_instance_of Hash 74 | expect(db.get('stuff')).to eq({ key: 'value' }) 75 | end 76 | 77 | it 'can set hashes in a list' do 78 | load_db([{ 'stuff' => [{ :key => "value" }, { :stuff => "value" }] }]) 79 | expect(db.get('stuff')).to eq [{ key: 'value' }, { stuff: 'value' }] 80 | end 81 | end 82 | 83 | it 'can return an array type' do 84 | load_db([{ 'favorites' => ['pants', 'lack of pants', 'party pants'] }]) 85 | expect(db.get('favorites')).to eq ['pants', 'lack of pants', 'party pants'] 86 | end 87 | 88 | it 'can return a bool type' do 89 | load_db([{ 'stuff' => true }]) 90 | expect(db.get('stuff')).to eq true 91 | end 92 | 93 | it 'does not freak out when it sees quotes' do 94 | load_db([ { 'stuff" 7' => 'pants' }]) 95 | expect(db.get('stuff" 7')).to eq 'pants' 96 | end 97 | end 98 | 99 | context "on get" do 100 | it 'returns the key when a key is set' do 101 | expect(db.set('Apples', '10')).to eq 'Apples' 102 | end 103 | 104 | it 'can get all stored items' do 105 | load_db([{ 'ants' => 10 }, { 'bob' => 'pants' }, { 'cants' => 10 } ]) 106 | expect(db.all).to eq [{ 'ants' => 10 }, { 'bob' => 'pants' }, { 'cants' => 10 }] 107 | end 108 | 109 | it 'can get all versions of a given item' do 110 | load_db([{ 'ants' => 10 }, { 'ants' => 'pants' }, { 'ants' => 10 } ]) 111 | expect(db.all_versions('ants')).to eq [10, 'pants', 10] 112 | end 113 | end 114 | 115 | context "on save" do 116 | it 'returns a hash' do 117 | load_db([{ 'hello' => 'world' }]) 118 | expect(db.save).to be_an_instance_of String 119 | end 120 | 121 | # it 'saves the hash to a dotfile for persistance' do 122 | # load_db([{ 'hello' => 'world' }]) 123 | # db.save 124 | # expect(`ls -a`.include?(".database")).to be true 125 | # end 126 | 127 | it 'can be loaded given a hash' do 128 | load_db([{ 'hello' => 'world' }]) 129 | db.save 130 | clear_db 131 | db.load(File.open('.database').read) 132 | expect(db.get('hello')).to eq 'world' 133 | expect($ITEMS.keys).to eq ['hello'] 134 | end 135 | end 136 | 137 | context 'on destruction' do 138 | it 'can destroy itself' do 139 | load_db([{ 'hello' => 'world' }]) 140 | db.destroy! 141 | expect($ITEMS == {}).to eq true 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'gkv' 3 | require 'debt_ceiling' 4 | 5 | RSpec.configure do |config| 6 | config.after(:all) { DebtCeiling.audit } 7 | end 8 | 9 | def clear_db 10 | $ITEMS = {} 11 | end 12 | 13 | def load_db(kv_list) 14 | kv_list.each do |kv| 15 | db.set(kv.keys.first, kv.values.first) 16 | end 17 | end 18 | --------------------------------------------------------------------------------