├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── rejson.rb └── rejson │ ├── client.rb │ ├── path.rb │ └── version.rb ├── rejson-rb.gemspec └── spec ├── rejson_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | .idea/ 11 | .vscode/ 12 | .rspec_status -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - bin/**/* 4 | - coverage/**/* 5 | DisplayCopNames: true 6 | TargetRubyVersion: 2.6 7 | 8 | Style/Documentation: 9 | Enabled: false 10 | 11 | Style/StringLiterals: 12 | EnforcedStyle: double_quotes 13 | 14 | Style/GuardClause: 15 | Enabled: false 16 | 17 | Style/SlicingWithRange: 18 | Enabled: true 19 | 20 | Style/RedundantRegexpEscape: 21 | Enabled: true 22 | 23 | Style/RedundantFetchBlock: 24 | Enabled: true 25 | 26 | Style/HashEachMethods: 27 | Enabled: true 28 | 29 | Style/HashTransformValues: 30 | Enabled: true 31 | 32 | Style/HashTransformKeys: 33 | Enabled: true 34 | 35 | Metrics/CyclomaticComplexity: 36 | Max: 9 37 | 38 | Metrics/PerceivedComplexity: 39 | Max: 9 40 | 41 | Metrics/MethodLength: 42 | Max: 15 43 | 44 | Metrics/ClassLength: 45 | Max: 150 46 | 47 | Metrics/BlockLength: 48 | Exclude: 49 | - spec/* 50 | 51 | Lint/RaiseException: 52 | Enabled: true 53 | 54 | Layout/SpaceAroundMethodCallOperator: 55 | Enabled: true 56 | 57 | Layout/EmptyLinesAroundAttributeAccessor: 58 | Enabled: true 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | 4 | services: docker 5 | cache: bundler 6 | rvm: 7 | - 2.5.5 8 | - 2.6.3 9 | - ruby-head 10 | 11 | before_install: 12 | - gem install bundler -v 2.0.0 13 | - docker pull redislabs/rejson:edge 14 | - docker run -d -p 6379:6379 redislabs/rejson:edge 15 | 16 | install: 17 | # Install dependency gems 18 | - bundle install 19 | 20 | before_script: 21 | - bundle install 22 | 23 | script: 24 | - bundle exec rake rubocop 25 | - bundle exec rspec 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at vachhanihpavan@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in rejson-rb.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 12.0" 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Pavan Vachhani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedisJSON Ruby Client [![Build Status](https://travis-ci.com/vachhanihpavan/rejson-rb.svg?token=x85KXUqPs5qJik1EzpyW&branch=master)](https://travis-ci.com/vachhanihpavan/rejson-rb) [![Gem Version](https://badge.fury.io/rb/rejson-rb.svg)](https://badge.fury.io/rb/rejson-rb) 2 | 3 | rejson-rb is a package that allows storing, updating and querying objects as JSON documents in a [Redis](https://redis.io/) database that is extended with the [ReJSON](https://github.com/RedisJSON/RedisJSON) module. The package extends redis-rb's interface with ReJSON's API, and performs on-the-fly serialization/deserialization of objects to/from JSON. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'rejson-rb' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle install 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install rejson-rb 20 | 21 | ## Usage 22 | 23 | Make sure you have loaded rejson module in `redis.conf` 24 | ```ruby 25 | require 'rejson' 26 | 27 | rcl = Redis.new # Get a redis client 28 | 29 | # Get/Set/Delete keys 30 | obj = { 31 | 'foo': 42, 32 | 'arr': [nil, true, 3.14], 33 | 'truth': { 34 | 'coord': "out there" 35 | } 36 | } 37 | 38 | rcl.json_set("root", Rejson::Path.root_path, obj) 39 | # => "OK" 40 | 41 | rcl.json_set("root", Rejson::Path.new(".foo"), 56) 42 | # => "OK" 43 | 44 | rcl.json_get "root", Rejson::Path.root_path 45 | # => {"foo"=>56, "arr"=>[nil, true, 3.14], "truth"=>{"coord"=>"out there"}} 46 | 47 | rcl.json_del "root", ".truth.coord" 48 | # => 1 49 | 50 | # Use similar to redis-rb client 51 | rj = rcl.pipelined do 52 | rcl.set "foo", "bar" 53 | rcl.json_set "test", ".", "{:foo => 'bar', :baz => 'qux'}" 54 | end 55 | # => ["OK", "OK"] 56 | ``` 57 | 58 | Path to JSON can be passed as `Rejson::Path.new("")` or `Rejson::Path.root_path`. `` syntax can be as mentioned [here](https://oss.redislabs.com/redisjson/path). 59 | 60 | ### Refer project WIKI for more detailed documentation. 61 | 62 | ## Contributing 63 | 64 | Bug reports and pull requests are welcome on GitHub at https://github.com/vachhanihpavan/rejson-rb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/vachhanihpavan/rejson-rb/blob/master/CODE_OF_CONDUCT.md). 65 | 66 | For complete documentation about ReJSON's commands, refer to ReJSON's website. 67 | 68 | ## License 69 | 70 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 71 | 72 | ## Code of Conduct 73 | 74 | Everyone interacting in the Rejson project's codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/vachhanihpavan/rejson-rb/blob/master/CODE_OF_CONDUCT.md).# 75 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rubocop/rake_task" 5 | 6 | RuboCop::RakeTask.new 7 | 8 | begin 9 | require "rspec/core/rake_task" 10 | 11 | RSpec::Core::RakeTask.new(:spec) 12 | 13 | task default: :spec 14 | rescue LoadError 15 | # no rspec available 16 | end 17 | 18 | task default: :spec 19 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "rejson/path" 6 | require "rejson/client" 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | 11 | # (If you use this, don't forget to add pry to your Gemfile!) 12 | # require "pry" 13 | # Pry.start 14 | 15 | require "irb" 16 | IRB.start(__FILE__) 17 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/rejson.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rejson/version" 4 | require "rejson/path" 5 | require "rejson/client" 6 | -------------------------------------------------------------------------------- /lib/rejson/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "redis" 4 | require "json" 5 | 6 | # Extends Redis class to add JSON functions 7 | class Redis 8 | # rubocop:disable Metrics/AbcSize 9 | def json_set(key, path, data, options = {}) 10 | pieces = [key, str_path(path), json_encode(data)] 11 | options[:nx] ||= false if options.dig(:nx) 12 | 13 | options[:xx] ||= false if options.dig(:xx) 14 | 15 | if options[:nx] && options[:xx] 16 | raise ArgumentError, "nx and xx are mutually exclusive: use one, the other or neither - but not both" 17 | elsif options[:nx] 18 | pieces.append("NX") 19 | elsif options[:xx] 20 | pieces.append("XX") 21 | end 22 | 23 | call_client(:set, pieces) 24 | end 25 | # rubocop:enable Metrics/AbcSize 26 | 27 | def json_get(key, *args) 28 | pieces = [key] 29 | 30 | if args.empty? 31 | pieces.append(str_path(Rejson::Path.root_path)) 32 | else 33 | args.each do |arg| 34 | pieces.append(str_path(arg)) 35 | end 36 | end 37 | 38 | begin 39 | json_decode call_client(:get, pieces) 40 | rescue TypeError 41 | nil 42 | end 43 | end 44 | 45 | def json_mget(key, *args) 46 | pieces = [key] 47 | 48 | raise ArgumentError, "Invalid arguments: Missing path" if args.empty? 49 | 50 | pieces.append(args) 51 | json_bulk_decode call_client(:mget, pieces) 52 | end 53 | 54 | def json_del(key, path = Rejson::Path.root_path) 55 | pieces = [key, str_path(path)] 56 | call_client(:del, pieces).to_i 57 | end 58 | 59 | alias json_forget json_del 60 | 61 | def json_type(key, path = Rejson::Path.root_path) 62 | pieces = [key, str_path(path)] 63 | call_client(:type, pieces).to_s 64 | end 65 | 66 | def json_numincrby(key, path, number) 67 | pieces = [key, str_path(path), number] 68 | call_client(:numincrby, pieces).to_i 69 | end 70 | 71 | def json_nummultby(key, path, number) 72 | pieces = [key, str_path(path), number] 73 | call_client(:nummultby, pieces).to_i 74 | end 75 | 76 | def json_strappend(key, string, path = Rejson::Path.root_path) 77 | pieces = [key, str_path(path), json_encode(string)] 78 | call_client(:strappend, pieces).to_i 79 | end 80 | 81 | def json_strlen(key, path = Rejson::Path.root_path) 82 | pieces = [key, str_path(path)] 83 | call_client(:strlen, pieces).to_i 84 | end 85 | 86 | def json_arrappend(key, path, json, *args) 87 | json_objs = [json_encode(json)] 88 | args.each { |arg| json_objs.append(json_encode(arg)) } 89 | pieces = [key, str_path(path), json_objs] 90 | call_client(:arrappend, pieces).to_i 91 | end 92 | 93 | def json_arrindex(key, path, scalar, start = 0, stop = 0) 94 | pieces = [key, str_path(path), scalar, start, stop] 95 | call_client(:arrindex, pieces).to_i 96 | end 97 | 98 | def json_arrinsert(key, path, index, *args) 99 | json_objs = [] 100 | args.each { |arg| json_objs.append(json_encode(arg)) } 101 | pieces = [key, str_path(path), index, json_objs] 102 | call_client(:arrinsert, pieces).to_i 103 | end 104 | 105 | def json_arrlen(key, path = Rejson::Path.root_path) 106 | pieces = [key, str_path(path)] 107 | call_client(:arrlen, pieces).to_i 108 | end 109 | 110 | def json_arrpop(key, path = Rejson::Path.root_path, index = -1) 111 | pieces = [key, str_path(path), index] 112 | call_client(:arrpop, pieces).to_s 113 | end 114 | 115 | def json_arrtrim(key, path, start, stop) 116 | pieces = [key, str_path(path), start, stop] 117 | call_client(:arrtrim, pieces).to_i 118 | end 119 | 120 | def json_objkeys(key, path = Rejson::Path.root_path) 121 | pieces = [key, str_path(path)] 122 | call_client(:objkeys, pieces).to_a 123 | end 124 | 125 | def json_objlen(key, path = Rejson::Path.root_path) 126 | pieces = [key, str_path(path)] 127 | call_client(:objlen, pieces).to_i 128 | end 129 | 130 | def json_resp(key, path = Rejson::Path.root_path) 131 | pieces = [key, str_path(path)] 132 | call_client(:resp, pieces) 133 | end 134 | 135 | private 136 | 137 | def str_path(path) 138 | if path.instance_of?(Rejson::Path) 139 | path.str_path 140 | else 141 | path 142 | end 143 | end 144 | 145 | def json_encode(obj) 146 | JSON.generate(obj) 147 | end 148 | 149 | def json_decode(obj) 150 | JSON.parse(obj) 151 | end 152 | 153 | def json_bulk_decode(obj) 154 | res = [] 155 | obj.to_a.each do |o| 156 | if o.nil? 157 | res.append(nil) 158 | else 159 | res.append(JSON.parse(o)) 160 | end 161 | end 162 | res 163 | end 164 | 165 | def call_client(cmd, pieces) 166 | pieces.prepend("JSON.#{cmd.upcase}").join(" ") 167 | @client.call pieces 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/rejson/path.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rejson 4 | # Represents a Path in JSON value 5 | class Path 6 | attr_accessor :str_path 7 | 8 | def self.root_path 9 | root = Path.new(".") 10 | root 11 | end 12 | 13 | def initialize(path) 14 | @str_path = path 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rejson/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rejson 4 | VERSION = "1.0.1" 5 | end 6 | -------------------------------------------------------------------------------- /rejson-rb.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "./lib/rejson/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "rejson-rb" 7 | spec.version = Rejson::VERSION 8 | spec.authors = ["Pavan Vachhani"] 9 | spec.email = ["vachhanihpavan@gmail.com"] 10 | 11 | spec.summary = "Redis JSON Ruby Client" 12 | spec.description = "rejson-rb is a package that allows storing, updating and querying objects as JSON documents 13 | in Redis database that is intended with RedisJSON module." 14 | spec.homepage = "https://github.com/vachhanihpavan/rejson-rb" 15 | spec.license = "MIT" 16 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 17 | 18 | spec.metadata["homepage_uri"] = spec.homepage 19 | spec.metadata["source_code_uri"] = "https://github.com/vachhanihpavan/rejson-rb" 20 | spec.metadata["changelog_uri"] = "https://github.com/vachhanihpavan/rejson-rb" 21 | 22 | # Specify which files should be added to the gem when it is released. 23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 24 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 25 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 26 | end 27 | spec.bindir = "exe" 28 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 29 | spec.require_paths = ["lib"] 30 | 31 | spec.add_runtime_dependency "json", "~> 2.0" 32 | spec.add_runtime_dependency "redis", ">= 4.2.1", "<= 5.0.7" 33 | 34 | spec.add_development_dependency "bundler", "~> 2.0" 35 | spec.add_development_dependency "rspec", "~> 3.0" 36 | spec.add_development_dependency "rubocop", "=0.86.0" 37 | spec.add_development_dependency "simplecov", "~> 0.17" 38 | end 39 | -------------------------------------------------------------------------------- /spec/rejson_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rejson/path" 4 | require "rejson/client" 5 | require "spec_helper" 6 | require "json" 7 | 8 | describe "Test ReJSON" do 9 | before :all do 10 | @rcl = Redis.new(db: 14) 11 | 12 | @docs = { 13 | 'simple': { 14 | 'foo': "bar" 15 | }, 16 | 'basic': { 17 | 'string': "string value", 18 | 'nil': nil, 19 | 'bool': true, 20 | 'int': 42, 21 | 'num': 4.2, 22 | 'arr': [42, nil, -1.2, false, %w[sub array], { 'subdict': true }], 23 | 'dict': { 24 | 'a': 1, 25 | 'b': "2", 26 | 'c': nil 27 | } 28 | }, 29 | 'scalars': { 30 | 'unicode': "string value", 31 | 'NoneType': nil, 32 | 'bool': true, 33 | 'int': 42, 34 | 'float': -1.2 35 | }, 36 | 'values': { 37 | 'unicode': "string value", 38 | 'NoneType': nil, 39 | 'bool': true, 40 | 'int': 42, 41 | 'float': -1.2, 42 | 'dict': {}, 43 | 'list': [] 44 | }, 45 | 'types': { 46 | 'null': nil, 47 | 'boolean': false, 48 | 'integer': 42, 49 | 'number': 1.2, 50 | 'string': "str", 51 | 'object': {}, 52 | 'array': [] 53 | } 54 | } 55 | @rcl.flushdb 56 | end 57 | 58 | context "get/set" do 59 | context "basic functionality" do 60 | it "gets a blank value" do 61 | foo = @rcl.json_get "foo" 62 | foo.should be_nil 63 | end 64 | 65 | it "sets a value" do 66 | set = @rcl.json_set "foo", Rejson::Path.root_path, "bar" 67 | set.should eq "OK" 68 | end 69 | 70 | it "gets a value" do 71 | foo = @rcl.json_get "foo" 72 | foo.should eq "bar" 73 | end 74 | 75 | it "gets a value (w/ p)" do 76 | foo = @rcl.json_get "foo", "." 77 | foo.should eq "bar" 78 | end 79 | end 80 | 81 | context "with all data types" do 82 | it "should be OK" do 83 | set = @rcl.json_set "foo", Rejson::Path.root_path, @docs[:basic] 84 | set.should eq "OK" 85 | 86 | foo = @rcl.json_get "foo", "." 87 | expect(foo).to(match({ "arr" => [42, nil, -1.2, false, %w[sub array], { "subdict" => true }], 88 | "bool" => true, "dict" => { "a" => 1, "b" => "2", "c" => nil }, 89 | "int" => 42, "nil" => nil, "num" => 4.2, "string" => "string value" })) 90 | 91 | set = @rcl.json_set "foo", Rejson::Path.root_path, @docs[:scalars] 92 | set.should eq "OK" 93 | 94 | foo = @rcl.json_get "foo", "." 95 | foo.should eq @docs[:scalars].transform_keys(&:to_s) 96 | 97 | set = @rcl.json_set "foo", Rejson::Path.root_path, @docs[:types] 98 | set.should eq "OK" 99 | 100 | foo = @rcl.json_get "foo", "." 101 | foo.should eq @docs[:types].transform_keys(&:to_s) 102 | 103 | set = @rcl.json_set "foo", Rejson::Path.root_path, @docs[:values] 104 | set.should eq "OK" 105 | 106 | foo = @rcl.json_get "foo", "." 107 | foo.should eq @docs[:values].transform_keys(&:to_s) 108 | end 109 | end 110 | 111 | context "with path" do 112 | it "should be OK" do 113 | set = @rcl.json_set "test", ".", {} 114 | set.should eq "OK" 115 | set = @rcl.json_set "test", Rejson::Path.new(".foo"), "baz" 116 | set.should eq "OK" 117 | set = @rcl.json_set "test", ".bar", "qux" 118 | set.should eq "OK" 119 | get = @rcl.json_get "test" 120 | get.should == { "bar" => "qux", "foo" => "baz" } 121 | end 122 | end 123 | 124 | context "with flags" do 125 | before(:each) do 126 | set = @rcl.json_set "test", ".", foo: "bar" 127 | set.should eq "OK" 128 | end 129 | it "should allow nx/xx flags" do 130 | set = @rcl.json_set "test", "foo", "baz", nx: true 131 | set.should_not eq "OK" 132 | set = @rcl.json_set "test", "bar", "baz", xx: true 133 | set.should_not eq "OK" 134 | set = @rcl.json_set "test", "foo", "bam", xx: true 135 | set.should eq "OK" 136 | set = @rcl.json_set "test", "bar", "baz", nx: true 137 | set.should eq "OK" 138 | 139 | expect do 140 | @rcl.json_set "test", "foo", "baz", nx: true, xx: true 141 | end.to(raise_error ArgumentError) 142 | get = @rcl.json_get "test", "foo" 143 | get.should_not eq "baz" 144 | end 145 | end 146 | 147 | context "deep nested" do 148 | it "should get correct type" do 149 | set = @rcl.json_set "foo", Rejson::Path.root_path, @docs[:basic] 150 | set.should eq "OK" 151 | 152 | foo = @rcl.json_get "foo", ".arr" 153 | foo.should be_an_instance_of(Array) 154 | expect(foo.last.transform_keys(&:to_sym)).to(match({ 'subdict': true })) 155 | end 156 | end 157 | end 158 | 159 | context "delete" do 160 | before(:each) do 161 | @rcl.json_set "baz", Rejson::Path.root_path, { "name": "Pavan", 162 | "lastSeen": 1_800 } 163 | end 164 | 165 | it "deletes a values" do 166 | foo = @rcl.json_del "baz", ".name" 167 | foo.should eq 1 168 | 169 | foo = @rcl.json_get "baz" 170 | foo.should include("lastSeen" => 1800) 171 | foo.should_not include("name" => "Pavan") 172 | end 173 | 174 | it "skips missing key" do 175 | foo = @rcl.json_del "missing", "." 176 | foo.should eq 0 177 | end 178 | end 179 | 180 | context "mget" do 181 | before(:each) do 182 | @rcl.json_set "foo", Rejson::Path.root_path, [1, 2, 3] 183 | @rcl.json_set "bar", Rejson::Path.root_path, { "name": "John Doe" } 184 | @rcl.json_set "baz", Rejson::Path.root_path, { "name": "Pavan", 185 | "lastSeen": 1_800 } 186 | end 187 | 188 | it "fetches all values" do 189 | foo = @rcl.json_mget "bar", "baz", "." 190 | foo.should eq [{ "name" => "John Doe" }, { "lastSeen" => 1800, "name" => "Pavan" }] 191 | end 192 | 193 | it "fails due to missing argument" do 194 | expect do 195 | @rcl.json_mget "foo1" 196 | end.to(raise_error ArgumentError) 197 | end 198 | 199 | it "skips missing values" do 200 | foo = @rcl.json_mget "bar", "missing", "." 201 | foo.should eq [{ "name" => "John Doe" }, nil] 202 | end 203 | end 204 | 205 | context "type" do 206 | before(:each) do 207 | @rcl.json_set "foo", Rejson::Path.root_path, [1, 2, 3] 208 | @rcl.json_set "bar", Rejson::Path.root_path, { "name": "John Doe" } 209 | @rcl.json_set "baz", Rejson::Path.root_path, 1 210 | end 211 | 212 | it "gets object type" do 213 | type = @rcl.json_type "bar", "." 214 | type.should eq "object" 215 | end 216 | 217 | it "gets array type" do 218 | type = @rcl.json_type "foo", "." 219 | type.should eq "array" 220 | end 221 | 222 | it "gets integer type" do 223 | type = @rcl.json_type "baz", "." 224 | type.should eq "integer" 225 | end 226 | end 227 | 228 | context "number manipulation" do 229 | before(:each) do 230 | @rcl.json_set "test", ".", "foo" => 0, "bar" => 1 231 | end 232 | context "numicrby" do 233 | before(:each) do 234 | incr = @rcl.json_numincrby "test", ".foo", 1 235 | incr.should eq 1 236 | end 237 | 238 | it "should increment by 1" do 239 | get = @rcl.json_get "test" 240 | get.should == { "foo" => 1, "bar" => 1 } 241 | end 242 | end 243 | 244 | context "nummultby" do 245 | before(:each) do 246 | incr = @rcl.json_nummultby "test", ".bar", 2 247 | incr.should eq 2 248 | end 249 | 250 | it "should increment by 1" do 251 | get = @rcl.json_get "test" 252 | get.should == { "foo" => 0, "bar" => 2 } 253 | end 254 | end 255 | end 256 | 257 | context "test string manipulation" do 258 | before(:each) do 259 | @rcl.json_set "test", ".", "foo" 260 | @rcl.json_set "test2", ".", foo: "bar", baz: "zoo" 261 | end 262 | 263 | context "strappend" do 264 | it "append and return new length" do 265 | new_length = @rcl.json_strappend("test", "bar") 266 | new_length.should eq 6 267 | get = @rcl.json_get "test" 268 | get.should == "foobar" 269 | end 270 | end 271 | 272 | context "strlength" do 273 | it "return correct length" do 274 | length = @rcl.json_strlen("test") 275 | length.should be 3 276 | end 277 | end 278 | end 279 | 280 | context "test array manipulation" do 281 | before(:each) do 282 | @rcl.json_set "append", ".", @docs[:basic] 283 | @rcl.json_set "index", ".", "arr" => [0, 1, 2, 3, 2, 9, 0] 284 | @rcl.json_set "null", ".", "arr" => [] 285 | end 286 | 287 | it "should append to array" do 288 | new_length = @rcl.json_arrappend "append", ".arr", 42 289 | new_length.should be 7 290 | end 291 | 292 | it "should return index of array" do 293 | element = @rcl.json_arrindex "index", ".arr", 3 294 | element.should eq 3 295 | end 296 | 297 | it "should insert element in array" do 298 | element = @rcl.json_arrinsert "index", ".arr", 2, "str" 299 | element.should eq 8 300 | get = @rcl.json_get "index" 301 | get.should == { "arr" => [0, 1, "str", 2, 3, 2, 9, 0] } 302 | end 303 | 304 | it "should get length of array" do 305 | length = @rcl.json_arrlen "index", ".arr" 306 | length.should eq 7 307 | end 308 | 309 | it "should pop element from array" do 310 | popped = @rcl.json_arrpop "index", ".arr", 5 311 | popped.should eq "9" 312 | end 313 | 314 | it "return null on popping null array" do 315 | expect do 316 | @rcl.json_arrpop "null", ".arr", 5 317 | end.to(raise_error Redis::CommandError) 318 | end 319 | 320 | it "should trim array" do 321 | trimmed = @rcl.json_arrtrim "index", ".arr", 1, -2 322 | trimmed.should eq 5 323 | trimmed_list = @rcl.json_get "index", ".arr" 324 | trimmed_list.should eq [1, 2, 3, 2, 9] 325 | trimmed = @rcl.json_arrtrim "index", ".arr", 0, 99 326 | trimmed.should eq 5 327 | trimmed = @rcl.json_arrtrim "index", ".arr", 0, 2 328 | trimmed_list = @rcl.json_get "index", ".arr" 329 | trimmed_list.should eq [1, 2, 3] 330 | trimmed.should eq 3 331 | end 332 | end 333 | 334 | context "test object manipulation" do 335 | before(:each) do 336 | @rcl.json_set "test", ".", @docs 337 | end 338 | context "objkeys" do 339 | before(:each) do 340 | @type_keys = @rcl.json_objkeys "test", ".types" 341 | @root_keys = @rcl.json_objkeys "test" 342 | end 343 | it "should should return keys" do 344 | @type_keys.should eq @docs[:types].keys.map(&:to_s) 345 | @root_keys.should eq @docs.keys.map(&:to_s) 346 | end 347 | end 348 | 349 | context "objlen" do 350 | before(:each) do 351 | @objlen = @rcl.json_objlen "test", ".types" 352 | @root_objlen = @rcl.json_objlen "test" 353 | end 354 | it "should return keys" do 355 | @objlen.should eq @docs[:types].keys.size 356 | @root_objlen.should eq @docs.keys.size 357 | end 358 | end 359 | end 360 | 361 | context "uncommon methods" do 362 | context "resp" do 363 | it "should return correct format" do 364 | @rcl.json_set "test", ".", nil 365 | resp = @rcl.json_resp "test" 366 | resp.should eq nil 367 | @rcl.json_set "test", ".", true 368 | resp = @rcl.json_resp "test" 369 | resp.should eq "true" 370 | @rcl.json_set "test", ".", 2.5 371 | resp = @rcl.json_resp "test" 372 | resp.should eq "2.5" 373 | @rcl.json_set "test", ".", 42 374 | resp = @rcl.json_resp "test" 375 | resp.should eq 42 376 | expect(@rcl.json_set("test", ".", [1, 2])).to be == "OK" 377 | resp = @rcl.json_resp "test" 378 | resp[0].should eq "[" 379 | resp[1].should eq 1 380 | resp[2].should eq 2 381 | end 382 | end 383 | end 384 | 385 | context "workflows" do 386 | it "should set/get/delete" do 387 | obj = { 388 | 'answer': 42, 389 | 'arr': [nil, true, 3.14], 390 | 'truth': { 391 | 'coord': "out there" 392 | } 393 | } 394 | @rcl.json_set("obj", Rejson::Path.root_path, obj) 395 | 396 | get = @rcl.json_get("obj", Rejson::Path.new(".truth.coord")) 397 | get.should eq obj.dig(:truth).dig(:coord) 398 | 399 | del = @rcl.json_forget("obj", ".truth.coord") 400 | del.should eq 1 401 | 402 | get = @rcl.json_get("obj", Rejson::Path.new(".truth")) 403 | expect(get).to(match({})) 404 | end 405 | end 406 | end 407 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" 4 | SimpleCov.start 5 | 6 | require_relative "../lib/rejson" 7 | # require "redis_rejson" 8 | 9 | RSpec.configure do |config| 10 | # Enable flags like --only-failures and --next-failure 11 | config.example_status_persistence_file_path = ".rspec_status" 12 | 13 | config.expect_with :rspec do |c| 14 | c.syntax = %i[expect should] 15 | end 16 | end 17 | --------------------------------------------------------------------------------