diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..02d2b8f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.replit b/.replit new file mode 100644 index 0000000..ece3def --- /dev/null +++ b/.replit @@ -0,0 +1,2 @@ +language = "ruby" +run = "bundle exec rake" \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index bfef2d0..5f81d25 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,8 @@ +require: rubocop-rake + AllCops: - TargetRubyVersion: 2.4 + TargetRubyVersion: 2.5 + NewCops: enable Style/StringLiterals: Enabled: true @@ -9,5 +12,18 @@ Style/StringLiteralsInInterpolation: Enabled: true EnforcedStyle: double_quotes +Layout/EndOfLine: + Enabled: false + +Naming/AccessorMethodName: + Enabled: false + +Lint/ScriptPermission: + Enabled: false + Layout/LineLength: Max: 120 + +Lint/SuppressedException: + Exclude: + - Rakefile diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..46b70e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog][Keep a Changelog] and this project adheres to [Semantic Versioning][Semantic Versioning]. + +## [Unreleased] + +--- + +## [Released] + +--- + + +[Keep a Changelog]: https://keepachangelog.com/ +[Semantic Versioning]: https://semver.org/ + + +[Unreleased]: https://github.com/janlindblom/ruby-replitdb/compare/v1.0.0...HEAD +[Released]: https://github.com/janlindblom/ruby-replitdb/releases +[0.0.2]: https://github.com/janlindblom/ruby-replitdb/compare/v0.0.1..v0.0.2 +[0.0.1]: https://github.com/janlindblom/ruby-replitdb/releases/v0.0.1 diff --git a/Gemfile b/Gemfile index 91947f1..419c280 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,3 @@ source "https://rubygems.org" # Specify your gem's dependencies in ruby-replitdb.gemspec gemspec - -gem "rake", "~> 13.0" - -gem "rubocop", "~> 1.7" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..5181300 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,70 @@ +PATH + remote: . + specs: + replitdb (0.0.1) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + coderay (1.1.3) + diff-lcs (1.4.4) + method_source (1.0.0) + parallel (1.20.1) + parser (3.0.1.0) + ast (~> 2.4.1) + pry (0.14.1) + coderay (~> 1.1) + method_source (~> 1.0) + rainbow (3.0.0) + rake (13.0.3) + regexp_parser (2.1.1) + rexml (3.2.5) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.2) + rubocop (1.13.0) + parallel (~> 1.10) + parser (>= 3.0.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.2.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.4.1) + parser (>= 2.7.1.5) + rubocop-rake (0.5.1) + rubocop + rubocop-rspec (2.3.0) + rubocop (~> 1.0) + rubocop-ast (>= 1.1.0) + ruby-progressbar (1.11.0) + unicode-display_width (2.0.0) + yard (0.9.26) + +PLATFORMS + x64-mingw32 + +DEPENDENCIES + pry (~> 0.14) + rake (~> 13.0) + replitdb! + rspec (~> 3.10) + rubocop (~> 1.7) + rubocop-rake (~> 0.5) + rubocop-rspec (~> 2.3) + yard (~> 0.9) + +BUNDLED WITH + 2.2.5 diff --git a/README.md b/README.md index a34cd31..5506ffb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# ReplitDb +# Replit Database client -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ruby/replitdb`. To experiment with that code, run `bin/console` for an interactive prompt. +Replit Database client is a simple way to use Replit Database in your Ruby repls. -TODO: Delete this and the text above, and describe your gem +Based on the [Node.js Replit Database client](https://github.com/replit/database-node). ## Installation @@ -12,17 +12,53 @@ Add this line to your application's Gemfile: gem 'replitdb' ``` -And then execute: +## Usage + +Require it in your code: - $ bundle install +```ruby +require 'replit' +client = Replit::Database::Client.new +client.set "key", "value" +key = client.get "key" +puts key +``` -Or install it yourself as: +### Library documentation - $ gem install replitdb +#### Constructor -## Usage +```ruby +Replit::Database::Client.new(custom_url) +``` + +Constructor takes a custom database URL as an optional argument. + +#### Functions + +```ruby +get(key, {raw: false}) +``` + +Gets the value of a key from the database, specifying passing the optional `raw: true` option returns the raw value stored, otherwise it will be deserialized as JSON into a Ruby object. + +```ruby +set(key, value) +``` + +Sets the value of a key in the database. + +```ruby +delete(key) +``` + +Deletes `key` from database. + +```ruby +list(prefix="") +``` -TODO: Write usage instructions here +List all of the keys, or if prefix is defined all of the keys starting with `prefix`. ## Development diff --git a/Rakefile b/Rakefile index 1924143..232ae67 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,21 @@ # frozen_string_literal: true require "bundler/gem_tasks" -require "rubocop/rake_task" -RuboCop::RakeTask.new +begin + require "rubocop/rake_task" + require "yard" + require "rspec/core/rake_task" -task default: :rubocop + RSpec::Core::RakeTask.new(:spec) + + RuboCop::RakeTask.new + + YARD::Rake::YardocTask.new do |t| + t.files = ["lib/**/*.rb", "spec/**/*.rb"] + t.stats_options = ["--list-undoc"] + end + + task default: :rubocop +rescue LoadError +end diff --git a/bin/console b/bin/console index 627ef53..4eaddb5 100644 --- a/bin/console +++ b/bin/console @@ -2,7 +2,7 @@ # frozen_string_literal: true require "bundler/setup" -require "ruby/replitdb" +require "replit/database" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. diff --git a/lib/replit.rb b/lib/replit.rb new file mode 100644 index 0000000..6ec1f0a --- /dev/null +++ b/lib/replit.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require_relative "replit/database" + +# +# The Replit module. +# +# @author Jan Lindblom +# @version 0.1.0 +# +module Replit +end diff --git a/lib/replit/database.rb b/lib/replit/database.rb new file mode 100644 index 0000000..383e67c --- /dev/null +++ b/lib/replit/database.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "database/client" + +module Replit + # + # Replit Database module. + # + # @author Jan Lindblom + # @version 0.1.0 + # + module Database + # + # Thrown if there is a syntax error. + # + class SyntaxError < StandardError; end + + # + # Thrown if there is a configuration error. + # + class ConfigurationError < StandardError; end + end +end diff --git a/lib/replit/database/client.rb b/lib/replit/database/client.rb new file mode 100644 index 0000000..b7398ca --- /dev/null +++ b/lib/replit/database/client.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require "net/http" +require "uri" +require "json" +require "cgi" + +module Replit + module Database + # + # The Replit Database Client. + # + class Client + # + # Create a new ReplitDb::Client. + # + # @param [String] custom_url optional custom URL + # + def initialize(custom_url = nil) + @database_url = ENV["REPLIT_DB_URL"] if ENV["REPLIT_DB_URL"] + @database_url = custom_url if custom_url + end + + # + # Gets a key. + # + # @param [String, Symbol] key Key. + # @param [Hash] options Options Hash. + # @option options [Boolean] :raw Makes it so that we return the raw string value. Default is false. + # + # @return [String,Object] the stored value. + # + def get(key, options = { raw: false }) + verify_connection_url + + raw_value = Net::HTTP.get(URI("#{@database_url}/#{key}")) + return nil if raw_value.empty? + return raw_value if options[:raw] + + json_parse(raw_value, key) + end + + # + # Sets a key. + # + # @param [String, Symbol] key Key. + # @param [Object] value Value. + # @param [Hash] options Options Hash. + # @option options [Boolean] :raw Makes it so that we store the raw string value. Default is false. + # + def set(key, value, options = { raw: false }) + verify_connection_url + + json_value = options[:raw] ? value : value.to_json + payload = "#{CGI.escape(key.to_s)}=#{CGI.escape(json_value)}" + Net::HTTP.post(URI(@database_url), payload) + end + + # + # Deletes a key. + # + # @param [String] key Key. + # + def delete(key) + verify_connection_url + + Net::HTTP.delete(URI("#{@database_url}/#{key}")) + end + + # + # List key starting with a prefix or list all. + # + # @param [String] prefix Filter keys starting with prefix. + # + # @return [Array] keys in database. + # + def list(prefix = "") + verify_connection_url + + response = + Net::HTTP.get( + URI("#{@database_url}?encode=true&prefix=#{CGI.escape(prefix)}") + ) + return [] if response.empty? + + response.split("\n").map { |l| CGI.unescape(l) } + end + + # + # Clears the database. + # + def empty + verify_connection_url + + list.each { |key| delete key } + end + + # + # Get all key/value pairs and return as an object. + # + # @return [Hash] Hash with all objects in database. + # + def get_all + verify_connection_url + + output = {} + list.each { |key| output[key.to_sym] = get key } + output + end + + # + # Sets the entire database through an object. + # + # @param [Hash] obj The object. + # + def set_all(obj = {}) + verify_connection_url + + obj.each_key { |key| set(key, obj[key]) } + end + + # + # Delete multiple entries by keys + # + # @param [Array] keys Keys. + # + def delete_multiple(keys = []) + verify_connection_url + + keys.each { |key| delete key } + end + + private + + def verify_connection_url + error = Replit::Database::ConfigurationError.new "Missing database connection url" + raise error unless @database_url + raise error if @database_url.empty? + end + + def json_parse(string, key) + JSON.parse(string, { symbolize_names: true }) + rescue JSON::ParserError + raise Replit::Database::SyntaxError, "Failed to parse value of #{ + key + }, try passing a raw option to get the raw value" + end + end + end +end diff --git a/lib/replit/database/version.rb b/lib/replit/database/version.rb new file mode 100644 index 0000000..fc5f0c0 --- /dev/null +++ b/lib/replit/database/version.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Replit + module Database + # @return [String] Current version. + VERSION = "0.1.0" + end +end diff --git a/lib/replit_db.rb b/lib/replit_db.rb deleted file mode 100644 index bf8d052..0000000 --- a/lib/replit_db.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require_relative "replit_db/version" - -module ReplitDb - class Error < StandardError; end - # Your code goes here... -end diff --git a/lib/replit_db/version.rb b/lib/replit_db/version.rb deleted file mode 100644 index e7c4e4e..0000000 --- a/lib/replit_db/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module ReplitDb - VERSION = "0.1.0" -end diff --git a/replit_db.gemspec b/replit_db.gemspec deleted file mode 100644 index c33d823..0000000 --- a/replit_db.gemspec +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require_relative "lib/replitdb/version" - -Gem::Specification.new do |spec| - spec.name = "replitdb" - spec.version = ReplitDb::VERSION - spec.authors = ["Jan Lindblom"] - spec.email = ["janlindblom@fastmail.fm"] - - spec.summary = "TODO: Write a short summary, because RubyGems requires one." - spec.description = "TODO: Write a longer description or delete this line." - spec.homepage = "TODO: Put your gem's website or public repo URL here." - spec.license = "MIT" - spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0") - - spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" - - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." - - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path(__dir__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } - end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] - - # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" - - # For more information and examples about making a new gem, checkout our - # guide at: https://bundler.io/guides/creating_gem.html -end diff --git a/replitdb.gemspec b/replitdb.gemspec new file mode 100644 index 0000000..406732a --- /dev/null +++ b/replitdb.gemspec @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "lib/replit/database/version" + +Gem::Specification.new do |spec| + spec.name = "replitdb" + spec.version = Replit::Database::VERSION + spec.authors = ["Jan Lindblom"] + spec.email = ["janlindblom@fastmail.fm"] + + spec.summary = "A gem to access Replit Databases." + spec.homepage = "https://github.com/janlindblom/ruby-replitdb" + spec.license = "MIT" + spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0") + + spec.metadata["allowed_push_host"] = "https://rubygems.org" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/janlindblom/ruby-replitdb" + spec.metadata["changelog_uri"] = "https://github.com/janlindblom/ruby-replitdb/blob/main/CHANGELOG.md" + + spec.files = Dir.chdir(File.expand_path(__dir__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|.github)/}) } + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_development_dependency "pry", "~> 0.14" + spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rspec", "~> 3.10" + spec.add_development_dependency "rubocop", "~> 1.7" + spec.add_development_dependency "rubocop-rake", "~> 0.5" + spec.add_development_dependency "rubocop-rspec", "~> 2.3" + spec.add_development_dependency "yard", "~> 0.9" +end diff --git a/spec/replit_spec.rb b/spec/replit_spec.rb new file mode 100644 index 0000000..176cda2 --- /dev/null +++ b/spec/replit_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "replit" + +RSpec.describe Replit do + describe Replit::Database do + it "has a version defined" do + expect(Replit::Database::VERSION).not_to be_nil + end + end +end + +RSpec.describe Replit::Database::Client do + context "without a defined connection URL" do + before :all do + @client = Replit::Database::Client.new("") + end + + it "will raise a ConfigurationError" do + expect { @client.get("dummy") }.to raise_error Replit::Database::ConfigurationError + expect { @client.set("dummy", "value") }.to raise_error Replit::Database::ConfigurationError + expect { @client.delete("dummy") }.to raise_error Replit::Database::ConfigurationError + end + end +end