diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0f896d1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: + ruby: [1.9, '2.0', 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, '3.0', 3.1, jruby-9.1, jruby-9.2] + gemfile: [no-deps, json-latest, json-old, json-pure] + exclude: + - ruby: '3.0' + gemfile: json-old + - ruby: 3.1 + gemfile: json-old + - ruby: 1.9 + gemfile: json-latest + - ruby: 1.9 + gemfile: json-pure + + steps: + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + + - uses: actions/checkout@v2 + + - name: Install gems + run: | + bundle install --gemfile=gemfiles/${{ matrix.gemfile }}.gemfile --without docs release benchmark + + - name: Unit Tests + run: BUNDLE_GEMFILE=gemfiles/${{ matrix.gemfile }}.gemfile bundle exec rake test:unit + + - name: Compliance Tests + run: BUNDLE_GEMFILE=gemfiles/${{ matrix.gemfile }}.gemfile bundle exec rake test:compliance diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..deec4bd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tasks/release"] + path = tasks/release + url = https://github.com/aws/aws-sdk-ruby-release-tools.git diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 50305b6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -script: bundle exec rake - -bundler_args: --without docs release benchmark - -rvm: -- 1.9.3 -- 2.0.0 -- 2.1 -- 2.2 -- 2.3.1 -- 2.4.1 -- jruby - -gemfile: - - gemfiles/no-deps.gemfile - - gemfiles/json-latest.gemfile - - gemfiles/json-old.gemfile - - gemfiles/json-pure.gemfile - -matrix: - exclude: - # latest version of `json` gem, 2.0+, is not supported on Ruby 1.9.3 - - rvm: 1.9.3 - gemfile: gemfiles/json-latest.gemfile - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/d9967f9fc130ccb637a9 - on_success: change - on_failure: always - on_start: false diff --git a/.yardopts b/.yardopts index 0fd6dab..fa9f2ff 100644 --- a/.yardopts +++ b/.yardopts @@ -1,5 +1,4 @@ --title 'jmespath.rb' ---output-dir docs --markup markdown --markup-provider rdiscount --hide-api private diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a2ef6..1609a8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ -Unreleased Changes +1.6.1 (2022-03-07) ------------------ +* Issue - Use `JSON.parse` instead of `JSON.load`. + +1.6.0 (2022-02-14) +------------------ + +* Feature - Add support for string comparisons. + +1.5.0 (2022-01-10) +------------------ + +* Support implicitly convertible objects/duck-type values responding to `to_hash` and `to_ary`. + + [See related GitHub pull request #51](https://github.com/jmespath/jmespath.rb/pull/51). + 1.4.0 (2018-04-04) ------------------ @@ -218,4 +232,3 @@ Unreleased Changes ------------------ * Passing all of the JMESPath compliance tests. - diff --git a/Gemfile b/Gemfile index 53ce4e6..a0642f9 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true source 'https://rubygems.org' gem 'rake', require: false diff --git a/Rakefile b/Rakefile index 2ed38f2..737f68f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,7 @@ +# frozen_string_literal: true $GEM_ROOT = File.dirname(__FILE__) -$: << File.join($GEM_ROOT, 'lib') +$LOAD_PATH << File.join($GEM_ROOT, 'lib') $VERSION = ENV['VERSION'] || File.read(File.join($GEM_ROOT, 'VERSION')) $GITHUB_ACCESS_TOKEN = ENV['JMESPATH_GITHUB_ACCESS_TOKEN'] diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..9d3b4f2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Reporting a Vulnerability + +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security +via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. +Please do **not** create a public GitHub issue. diff --git a/VERSION b/VERSION index 88c5fb8..9c6d629 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 +1.6.1 diff --git a/bin/jmespath.rb b/bin/jmespath.rb index 8180980..bf1c2a8 100755 --- a/bin/jmespath.rb +++ b/bin/jmespath.rb @@ -1,11 +1,12 @@ #!/usr/bin/env ruby +# frozen_string_literal: true -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'jmespath' require 'json' expression = ARGV[0] -json = JSON.load(STDIN.read) +json = JSON.parse(STDIN.read) $stdout.puts(JSON.dump(JMESPath.search(expression, json))) diff --git a/gemfiles/json-latest.gemfile b/gemfiles/json-latest.gemfile index fb8e2a5..cd173ac 100644 --- a/gemfiles/json-latest.gemfile +++ b/gemfiles/json-latest.gemfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true eval_gemfile(File.expand_path('../../Gemfile', __FILE__)) gem 'json' diff --git a/gemfiles/json-old.gemfile b/gemfiles/json-old.gemfile index 35d6acc..400b02c 100644 --- a/gemfiles/json-old.gemfile +++ b/gemfiles/json-old.gemfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true eval_gemfile(File.expand_path('../../Gemfile', __FILE__)) gem 'json', '< 2.0' diff --git a/gemfiles/json-pure.gemfile b/gemfiles/json-pure.gemfile index 6b02009..e7cd3f9 100644 --- a/gemfiles/json-pure.gemfile +++ b/gemfiles/json-pure.gemfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true eval_gemfile(File.expand_path('../../Gemfile', __FILE__)) gem 'json_pure', require: 'json/pure' diff --git a/gemfiles/no-deps.gemfile b/gemfiles/no-deps.gemfile index 6c06253..3373147 100644 --- a/gemfiles/no-deps.gemfile +++ b/gemfiles/no-deps.gemfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true eval_gemfile(File.expand_path('../../Gemfile', __FILE__)) # no dependency on `json` gem diff --git a/jmespath.gemspec b/jmespath.gemspec index c34329b..befbd87 100644 --- a/jmespath.gemspec +++ b/jmespath.gemspec @@ -1,3 +1,4 @@ +# frozen_string_literal: true Gem::Specification.new do |spec| spec.name = 'jmespath' spec.version = File.read(File.expand_path('../VERSION', __FILE__)).strip @@ -9,5 +10,5 @@ Gem::Specification.new do |spec| spec.license = 'Apache-2.0' spec.require_paths = ['lib'] spec.executables = Dir['bin/**'].map &File.method(:basename) - spec.files = Dir['lib/**/*.rb'] + ['LICENSE.txt'] + spec.files = Dir['lib/**/*.rb'] + %w(LICENSE.txt VERSION) end diff --git a/lib/jmespath.rb b/lib/jmespath.rb index a92b782..3c2e269 100644 --- a/lib/jmespath.rb +++ b/lib/jmespath.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true require 'json' require 'stringio' require 'pathname' module JMESPath - require 'jmespath/caching_parser' require 'jmespath/errors' require 'jmespath/lexer' @@ -16,7 +16,6 @@ module JMESPath require 'jmespath/version' class << self - # @param [String] expression A valid # [JMESPath](https://github.com/boto/jmespath) expression. # @param [Hash] data @@ -24,18 +23,17 @@ class << self # expression does not resolve inside `data`. def search(expression, data, runtime_options = {}) data = case data - when Hash, Struct then data # check for most common case first - when Pathname then load_json(data) - when IO, StringIO then JSON.load(data.read) - else data + when Hash, Struct then data # check for most common case first + when Pathname then load_json(data) + when IO, StringIO then JSON.parse(data.read) + else data end Runtime.new(runtime_options).search(expression, data) end # @api private def load_json(path) - JSON.load(File.open(path, 'r', encoding: 'UTF-8') { |f| f.read }) + JSON.parse(File.open(path, 'r', encoding: 'UTF-8', &:read)) end - end end diff --git a/lib/jmespath/caching_parser.rb b/lib/jmespath/caching_parser.rb index ffcc74d..2bb4ad0 100644 --- a/lib/jmespath/caching_parser.rb +++ b/lib/jmespath/caching_parser.rb @@ -1,8 +1,8 @@ +# frozen_string_literal: true require 'thread' module JMESPath class CachingParser - def initialize(options = {}) @parser = options[:parser] || Parser.new(options) @mutex = Mutex.new @@ -25,6 +25,5 @@ def cache_expression(expression) @cache[expression] = @parser.parse(expression) end end - end end diff --git a/lib/jmespath/errors.rb b/lib/jmespath/errors.rb index 7374d7b..a46f099 100644 --- a/lib/jmespath/errors.rb +++ b/lib/jmespath/errors.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + module JMESPath module Errors - class Error < StandardError; end class RuntimeError < Error; end @@ -14,6 +15,5 @@ class InvalidValueError < Error; end class InvalidArityError < Error; end class UnknownFunctionError < Error; end - end end diff --git a/lib/jmespath/lexer.rb b/lib/jmespath/lexer.rb index 4962567..b3bacd5 100644 --- a/lib/jmespath/lexer.rb +++ b/lib/jmespath/lexer.rb @@ -1,10 +1,10 @@ +# frozen_string_literal: true require 'json' require 'set' module JMESPath # @api private class Lexer - T_DOT = :dot T_STAR = :star T_COMMA = :comma @@ -134,14 +134,14 @@ class Lexer 'w' => STATE_IDENTIFIER, 'x' => STATE_IDENTIFIER, 'y' => STATE_IDENTIFIER, - 'z' => STATE_IDENTIFIER, - } + 'z' => STATE_IDENTIFIER + }.freeze VALID_IDENTIFIERS = Set.new(%w( - A B C D E F G H I J K L M N O P Q R S T U V W X Y Z - a b c d e f g h i j k l m n o p q r s t u v w x y z - _ 0 1 2 3 4 5 6 7 8 9 - )) + A B C D E F G H I J K L M N O P Q R S T U V W X Y Z + a b c d e f g h i j k l m n o p q r s t u v w x y z + _ 0 1 2 3 4 5 6 7 8 9 + )) NUMBERS = Set.new(%w(0 1 2 3 4 5 6 7 8 9)) @@ -155,13 +155,12 @@ class Lexer '(' => T_LPAREN, ')' => T_RPAREN, '{' => T_LBRACE, - '}' => T_RBRACE, - } + '}' => T_RBRACE + }.freeze # @param [String] expression # @return [Array] def tokenize(expression) - tokens = [] chars = CharacterStream.new(expression.chars.to_a) @@ -253,7 +252,7 @@ def tokenize(expression) tokens << match_or(chars, '&', '&', T_AND, T_EXPREF) when STATE_NOT # consume not equals - tokens << match_or(chars, '!', '=', T_COMPARATOR, T_NOT); + tokens << match_or(chars, '!', '=', T_COMPARATOR, T_NOT) else # either '<' or '>' # consume less than and greater than @@ -298,12 +297,12 @@ def inside(chars, delim, type) # Certain versions of Ruby and of the pure_json gem not support loading # scalar JSON values, such a numbers, booleans, strings, etc. These # simple values must be first wrapped inside a JSON object before calling - # `JSON.load`. + # `JSON.parse`. # # # works in most JSON versions, raises in some versions - # JSON.load("true") - # JSON.load("123") - # JSON.load("\"abc\"") + # JSON.parse("true") + # JSON.parse("123") + # JSON.parse("\"abc\"") # # This is an known issue for: # @@ -317,27 +316,25 @@ def inside(chars, delim, type) # causes issues in environments that cannot compile the gem. We previously # had a direct dependency on `json_pure`, but this broke with the v2 update. # - # This method allows us to detect how the `JSON.load` behaves so we know + # This method allows us to detect how the `JSON.parse` behaves so we know # if we have to wrap scalar JSON values to parse them or not. # @api private def self.requires_wrapping? - begin - JSON.load('false') - rescue JSON::ParserError - true - end + JSON.parse('false') + rescue JSON::ParserError + true end if requires_wrapping? def parse_json(token, quoted = false) begin if quoted - token.value = JSON.load("{\"value\":#{token.value}}")['value'] + token.value = JSON.parse("{\"value\":#{token.value}}")['value'] else begin - token.value = JSON.load("{\"value\":#{token.value}}")['value'] + token.value = JSON.parse("{\"value\":#{token.value}}")['value'] rescue - token.value = JSON.load(sprintf('{"value":"%s"}', token.value.lstrip))['value'] + token.value = JSON.parse(sprintf('{"value":"%s"}', token.value.lstrip))['value'] end end rescue JSON::ParserError @@ -349,9 +346,13 @@ def parse_json(token, quoted = false) def parse_json(token, quoted = false) begin if quoted - token.value = JSON.load(token.value) + token.value = JSON.parse(token.value) else - token.value = JSON.load(token.value) rescue JSON.load(sprintf('"%s"', token.value.lstrip)) + token.value = begin + JSON.parse(token.value) + rescue + JSON.parse(sprintf('"%s"', token.value.lstrip)) + end end rescue JSON::ParserError token.type = T_UNKNOWN @@ -361,7 +362,6 @@ def parse_json(token, quoted = false) end class CharacterStream - def initialize(chars) @chars = chars @position = 0 @@ -376,10 +376,7 @@ def next @chars[@position] end - def position - @position - end - + attr_reader :position end end end diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index e67cc0d..b65677f 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes @@ -5,19 +6,15 @@ class Node def visit(value) end - def hash_like?(value) - Hash === value || Struct === value - end - def optimize self end - def chains_with?(other) + def chains_with?(_other) false end end - + require 'jmespath/nodes/subexpression' require 'jmespath/nodes/and' require 'jmespath/nodes/comparator' @@ -39,7 +36,5 @@ def chains_with?(other) require 'jmespath/nodes/projection' require 'jmespath/nodes/projection' require 'jmespath/nodes/slice' - - end end diff --git a/lib/jmespath/nodes/and.rb b/lib/jmespath/nodes/and.rb index 63c621f..2ec1816 100644 --- a/lib/jmespath/nodes/and.rb +++ b/lib/jmespath/nodes/and.rb @@ -1,7 +1,7 @@ +# frozen_string_literal: true module JMESPath module Nodes class And < Node - def initialize(left, right) @left = left @right = right @@ -19,7 +19,6 @@ def visit(value) def optimize self.class.new(@left.optimize, @right.optimize) end - end end end diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index 3108b8d..5b69d8e 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes class Comparator < Node + COMPARABLE_TYPES = [Numeric, String].freeze + attr_reader :left, :right def initialize(left, right) @@ -33,62 +36,51 @@ def optimize private - def check(left_value, right_value) + def check(_left_value, _right_value) nil end + + def comparable?(left_value, right_value) + COMPARABLE_TYPES.any? do |type| + left_value.is_a?(type) && right_value.is_a?(type) + end + end end module Comparators - class Eq < Comparator def check(left_value, right_value) - left_value == right_value + Util.as_json(left_value) == Util.as_json(right_value) end end class Neq < Comparator def check(left_value, right_value) - left_value != right_value + Util.as_json(left_value) != Util.as_json(right_value) end end class Gt < Comparator def check(left_value, right_value) - if left_value.is_a?(Numeric) && right_value.is_a?(Numeric) - left_value > right_value - else - nil - end + left_value > right_value if comparable?(left_value, right_value) end end class Gte < Comparator def check(left_value, right_value) - if left_value.is_a?(Numeric) && right_value.is_a?(Numeric) - left_value >= right_value - else - nil - end + left_value >= right_value if comparable?(left_value, right_value) end end class Lt < Comparator def check(left_value, right_value) - if left_value.is_a?(Numeric) && right_value.is_a?(Numeric) - left_value < right_value - else - nil - end + left_value < right_value if comparable?(left_value, right_value) end end class Lte < Comparator def check(left_value, right_value) - if left_value.is_a?(Numeric) && right_value.is_a?(Numeric) - left_value <= right_value - else - nil - end + left_value <= right_value if comparable?(left_value, right_value) end end end diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index e467d78..ccefdf6 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes @@ -27,6 +28,7 @@ def optimize class ComparatorCondition < Node COMPARATOR_TO_CONDITION = {} + COMPARABLE_TYPES = [Integer, String].freeze def initialize(left, right, child) @left = left @@ -34,16 +36,24 @@ def initialize(left, right, child) @child = child end - def visit(value) + def visit(_value) nil end + + private + + def comparable?(left_value, right_value) + COMPARABLE_TYPES.any? do |type| + left_value.is_a?(type) && right_value.is_a?(type) + end + end end class EqCondition < ComparatorCondition COMPARATOR_TO_CONDITION[Comparators::Eq] = self def visit(value) - @left.visit(value) == @right.visit(value) ? @child.visit(value) : nil + Util.as_json(@left.visit(value)) == Util.as_json(@right.visit(value)) ? @child.visit(value) : nil end def optimize @@ -62,7 +72,7 @@ def initialize(left, right, child) end def visit(value) - @left.visit(value) == @right ? @child.visit(value) : nil + Util.as_json(@left.visit(value)) == @right ? @child.visit(value) : nil end end @@ -70,7 +80,7 @@ class NeqCondition < ComparatorCondition COMPARATOR_TO_CONDITION[Comparators::Neq] = self def visit(value) - @left.visit(value) != @right.visit(value) ? @child.visit(value) : nil + Util.as_json(@left.visit(value)) != Util.as_json(@right.visit(value)) ? @child.visit(value) : nil end def optimize @@ -89,7 +99,7 @@ def initialize(left, right, child) end def visit(value) - @left.visit(value) != @right ? @child.visit(value) : nil + Util.as_json(@left.visit(value)) != @right ? @child.visit(value) : nil end end @@ -99,7 +109,7 @@ class GtCondition < ComparatorCondition def visit(value) left_value = @left.visit(value) right_value = @right.visit(value) - left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value > right_value ? @child.visit(value) : nil + comparable?(left_value, right_value) && left_value > right_value ? @child.visit(value) : nil end end @@ -109,7 +119,7 @@ class GteCondition < ComparatorCondition def visit(value) left_value = @left.visit(value) right_value = @right.visit(value) - left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value >= right_value ? @child.visit(value) : nil + comparable?(left_value, right_value) && left_value >= right_value ? @child.visit(value) : nil end end @@ -119,7 +129,7 @@ class LtCondition < ComparatorCondition def visit(value) left_value = @left.visit(value) right_value = @right.visit(value) - left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value < right_value ? @child.visit(value) : nil + comparable?(left_value, right_value) && left_value < right_value ? @child.visit(value) : nil end end @@ -129,7 +139,7 @@ class LteCondition < ComparatorCondition def visit(value) left_value = @left.visit(value) right_value = @right.visit(value) - left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value <= right_value ? @child.visit(value) : nil + comparable?(left_value, right_value) && left_value <= right_value ? @child.visit(value) : nil end end end diff --git a/lib/jmespath/nodes/current.rb b/lib/jmespath/nodes/current.rb index 8e37ac3..915bc65 100644 --- a/lib/jmespath/nodes/current.rb +++ b/lib/jmespath/nodes/current.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes diff --git a/lib/jmespath/nodes/expression.rb b/lib/jmespath/nodes/expression.rb index 218fc02..15f5750 100644 --- a/lib/jmespath/nodes/expression.rb +++ b/lib/jmespath/nodes/expression.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes @@ -8,7 +9,7 @@ def initialize(expression) @expression = expression end - def visit(value) + def visit(_value) self end @@ -22,4 +23,3 @@ def optimize end end end - diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index 3aaee9d..d1c1327 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes @@ -8,9 +9,10 @@ def initialize(key) end def visit(value) - if value.is_a?(Array) && @key.is_a?(Integer) - value[@key] - elsif value.is_a?(Hash) + if value.respond_to?(:to_ary) && @key.is_a?(Integer) + value.to_ary[@key] + elsif value.respond_to?(:to_hash) + value = value.to_hash if !(v = value[@key]).nil? v elsif @key_sym && !(v = value[@key_sym]).nil? @@ -40,17 +42,16 @@ class ChainedField < Field def initialize(keys) @keys = keys @key_syms = keys.each_with_object({}) do |k, syms| - if k.respond_to?(:to_sym) - syms[k] = k.to_sym - end + syms[k] = k.to_sym if k.respond_to?(:to_sym) end end def visit(obj) @keys.reduce(obj) do |value, key| - if value.is_a?(Array) && key.is_a?(Integer) - value[key] - elsif value.is_a?(Hash) + if value.respond_to?(:to_ary) && key.is_a?(Integer) + value.to_ary[key] + elsif value.respond_to?(:to_hash) + value = value.to_hash if !(v = value[key]).nil? v elsif (sym = @key_syms[key]) && !(v = value[sym]).nil? @@ -68,10 +69,7 @@ def chain(other) private - def keys - @keys - end - + attr_reader :keys end end end diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb index 1009403..d83c6ee 100644 --- a/lib/jmespath/nodes/flatten.rb +++ b/lib/jmespath/nodes/flatten.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes @@ -8,16 +9,14 @@ def initialize(child) def visit(value) value = @child.visit(value) - if Array === value - value.each_with_object([]) do |v, values| - if Array === v - values.concat(v) + if value.respond_to?(:to_ary) + value.to_ary.each_with_object([]) do |v, values| + if v.respond_to?(:to_ary) + values.concat(v.to_ary) else values.push(v) end end - else - nil end end diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 6a6ab52..9d23e41 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -1,8 +1,8 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes class Function < Node - FUNCTIONS = {} def initialize(children, options = {}) @@ -38,26 +38,30 @@ def initialize(name) private def maybe_raise(error_type, message) - unless @disable_visit_errors - raise error_type, message - end + raise error_type, message unless @disable_visit_errors end - def call(args) + def call(_args) nil end end module TypeChecker def get_type(value) - case value - when String then STRING_TYPE - when true, false then BOOLEAN_TYPE - when nil then NULL_TYPE - when Numeric then NUMBER_TYPE - when Hash, Struct then OBJECT_TYPE - when Array then ARRAY_TYPE - when Expression then EXPRESSION_TYPE + if value.respond_to?(:to_str) + STRING_TYPE + elsif value == true || value == false + BOOLEAN_TYPE + elsif value.nil? + NULL_TYPE + elsif value.is_a?(Numeric) + NUMBER_TYPE + elsif value.respond_to?(:to_hash) || value.is_a?(Struct) + OBJECT_TYPE + elsif value.respond_to?(:to_ary) + ARRAY_TYPE + elsif value.is_a?(Expression) + EXPRESSION_TYPE end end @@ -76,7 +80,7 @@ def get_type(value) NULL_TYPE => 'null', NUMBER_TYPE => 'number', OBJECT_TYPE => 'object', - STRING_TYPE => 'string', + STRING_TYPE => 'string' }.freeze end @@ -87,12 +91,12 @@ def call(args) if args.count == 1 value = args.first else - return maybe_raise Errors::InvalidArityError, "function abs() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function abs() expects one argument' end if Numeric === value value.abs else - return maybe_raise Errors::InvalidTypeError, "function abs() expects a number" + return maybe_raise Errors::InvalidTypeError, 'function abs() expects a number' end end end @@ -104,19 +108,20 @@ def call(args) if args.count == 1 values = args.first else - return maybe_raise Errors::InvalidArityError, "function avg() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function avg() expects one argument' end - if Array === values + if values.respond_to?(:to_ary) + values = values.to_ary return nil if values.empty? - values.inject(0) do |total,n| + values.inject(0) do |total, n| if Numeric === n total + n else - return maybe_raise Errors::InvalidTypeError, "function avg() expects numeric values" + return maybe_raise Errors::InvalidTypeError, 'function avg() expects numeric values' end end / values.size.to_f else - return maybe_raise Errors::InvalidTypeError, "function avg() expects a number" + return maybe_raise Errors::InvalidTypeError, 'function avg() expects a number' end end end @@ -128,12 +133,12 @@ def call(args) if args.count == 1 value = args.first else - return maybe_raise Errors::InvalidArityError, "function ceil() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function ceil() expects one argument' end if Numeric === value value.ceil else - return maybe_raise Errors::InvalidTypeError, "function ceil() expects a numeric value" + return maybe_raise Errors::InvalidTypeError, 'function ceil() expects a numeric value' end end end @@ -144,14 +149,16 @@ class ContainsFunction < Function def call(args) if args.count == 2 haystack = args[0] - needle = args[1] - if String === haystack || Array === haystack - haystack.include?(needle) + needle = Util.as_json(args[1]) + if haystack.respond_to?(:to_str) + haystack.to_str.include?(needle) + elsif haystack.respond_to?(:to_ary) + haystack.to_ary.any? { |e| Util.as_json(e) == needle } else - return maybe_raise Errors::InvalidTypeError, "contains expects 2nd arg to be a list" + return maybe_raise Errors::InvalidTypeError, 'contains expects 2nd arg to be a list' end else - return maybe_raise Errors::InvalidArityError, "function contains() expects 2 arguments" + return maybe_raise Errors::InvalidArityError, 'function contains() expects 2 arguments' end end end @@ -163,12 +170,12 @@ def call(args) if args.count == 1 value = args.first else - return maybe_raise Errors::InvalidArityError, "function floor() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function floor() expects one argument' end if Numeric === value value.floor else - return maybe_raise Errors::InvalidTypeError, "function floor() expects a numeric value" + return maybe_raise Errors::InvalidTypeError, 'function floor() expects a numeric value' end end end @@ -180,36 +187,39 @@ def call(args) if args.count == 1 value = args.first else - return maybe_raise Errors::InvalidArityError, "function length() expects one argument" - end - case value - when Hash, Array, String then value.size - else return maybe_raise Errors::InvalidTypeError, "function length() expects string, array or object" + return maybe_raise Errors::InvalidArityError, 'function length() expects one argument' + end + if value.respond_to?(:to_hash) + value.to_hash.size + elsif value.respond_to?(:to_ary) + value.to_ary.size + elsif value.respond_to?(:to_str) + value.to_str.size + else + return maybe_raise Errors::InvalidTypeError, 'function length() expects string, array or object' end end end class Map < Function - FUNCTIONS['map'] = self def call(args) if args.count != 2 - return maybe_raise Errors::InvalidArityError, "function map() expects two arguments" + return maybe_raise Errors::InvalidArityError, 'function map() expects two arguments' end if Nodes::Expression === args[0] expr = args[0] else - return maybe_raise Errors::InvalidTypeError, "function map() expects the first argument to be an expression" + return maybe_raise Errors::InvalidTypeError, 'function map() expects the first argument to be an expression' end - if Array === args[1] - list = args[1] + if args[1].respond_to?(:to_ary) + list = args[1].to_ary else - return maybe_raise Errors::InvalidTypeError, "function map() expects the second argument to be an list" + return maybe_raise Errors::InvalidTypeError, 'function map() expects the second argument to be an list' end list.map { |value| expr.eval(value) } end - end class MaxFunction < Function @@ -221,14 +231,15 @@ def call(args) if args.count == 1 values = args.first else - return maybe_raise Errors::InvalidArityError, "function max() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function max() expects one argument' end - if Array === values + if values.respond_to?(:to_ary) + values = values.to_ary return nil if values.empty? first = values.first first_type = get_type(first) unless first_type == NUMBER_TYPE || first_type == STRING_TYPE - msg = "function max() expects numeric or string values" + msg = String.new('function max() expects numeric or string values') return maybe_raise Errors::InvalidTypeError, msg end values.inject([first, first_type]) do |(max, max_type), v| @@ -236,13 +247,13 @@ def call(args) if max_type == v_type v > max ? [v, v_type] : [max, max_type] else - msg = "function max() encountered a type mismatch in sequence: " + msg = String.new('function max() encountered a type mismatch in sequence: ') msg << "#{max_type}, #{v_type}" return maybe_raise Errors::InvalidTypeError, msg end end.first else - return maybe_raise Errors::InvalidTypeError, "function max() expects an array" + return maybe_raise Errors::InvalidTypeError, 'function max() expects an array' end end end @@ -256,14 +267,15 @@ def call(args) if args.count == 1 values = args.first else - return maybe_raise Errors::InvalidArityError, "function min() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function min() expects one argument' end - if Array === values + if values.respond_to?(:to_ary) + values = values.to_ary return nil if values.empty? first = values.first first_type = get_type(first) unless first_type == NUMBER_TYPE || first_type == STRING_TYPE - msg = "function min() expects numeric or string values" + msg = String.new('function min() expects numeric or string values') return maybe_raise Errors::InvalidTypeError, msg end values.inject([first, first_type]) do |(min, min_type), v| @@ -271,13 +283,13 @@ def call(args) if min_type == v_type v < min ? [v, v_type] : [min, min_type] else - msg = "function min() encountered a type mismatch in sequence: " + msg = String.new('function min() encountered a type mismatch in sequence: ') msg << "#{min_type}, #{v_type}" return maybe_raise Errors::InvalidTypeError, msg end end.first else - return maybe_raise Errors::InvalidTypeError, "function min() expects an array" + return maybe_raise Errors::InvalidTypeError, 'function min() expects an array' end end end @@ -291,7 +303,7 @@ def call(args) if args.count == 1 TYPE_NAMES[get_type(args.first)] else - return maybe_raise Errors::InvalidArityError, "function type() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function type() expects one argument' end end end @@ -302,17 +314,15 @@ class KeysFunction < Function def call(args) if args.count == 1 value = args.first - if hash_like?(value) - case value - when Hash then value.keys.map(&:to_s) - when Struct then value.members.map(&:to_s) - else raise NotImplementedError - end + if value.respond_to?(:to_hash) + value.to_hash.keys.map(&:to_s) + elsif value.is_a?(Struct) + value.members.map(&:to_s) else - return maybe_raise Errors::InvalidTypeError, "function keys() expects a hash" + return maybe_raise Errors::InvalidTypeError, 'function keys() expects a hash' end else - return maybe_raise Errors::InvalidArityError, "function keys() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function keys() expects one argument' end end end @@ -323,15 +333,17 @@ class ValuesFunction < Function def call(args) if args.count == 1 value = args.first - if hash_like?(value) + if value.respond_to?(:to_hash) + value.to_hash.values + elsif value.is_a?(Struct) value.values - elsif Array === value - value + elsif value.respond_to?(:to_ary) + value.to_ary else - return maybe_raise Errors::InvalidTypeError, "function values() expects an array or a hash" + return maybe_raise Errors::InvalidTypeError, 'function values() expects an array or a hash' end else - return maybe_raise Errors::InvalidArityError, "function values() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function values() expects one argument' end end end @@ -343,15 +355,15 @@ def call(args) if args.count == 2 glue = args[0] values = args[1] - if !(String === glue) - return maybe_raise Errors::InvalidTypeError, "function join() expects the first argument to be a string" - elsif Array === values && values.all? { |v| String === v } - values.join(glue) + if !glue.respond_to?(:to_str) + return maybe_raise Errors::InvalidTypeError, 'function join() expects the first argument to be a string' + elsif values.respond_to?(:to_ary) && values.to_ary.all? { |v| v.respond_to?(:to_str) } + values.to_ary.join(glue) else - return maybe_raise Errors::InvalidTypeError, "function join() expects values to be an array of strings" + return maybe_raise Errors::InvalidTypeError, 'function join() expects values to be an array of strings' end else - return maybe_raise Errors::InvalidArityError, "function join() expects an array of strings" + return maybe_raise Errors::InvalidArityError, 'function join() expects an array of strings' end end end @@ -362,9 +374,9 @@ class ToStringFunction < Function def call(args) if args.count == 1 value = args.first - String === value ? value : value.to_json + value.respond_to?(:to_str) ? value.to_str : value.to_json else - return maybe_raise Errors::InvalidArityError, "function to_string() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function to_string() expects one argument' end end end @@ -381,7 +393,7 @@ def call(args) nil end else - return maybe_raise Errors::InvalidArityError, "function to_number() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function to_number() expects one argument' end end end @@ -390,16 +402,16 @@ class SumFunction < Function FUNCTIONS['sum'] = self def call(args) - if args.count == 1 && Array === args.first - args.first.inject(0) do |sum,n| + if args.count == 1 && args.first.respond_to?(:to_ary) + args.first.to_ary.inject(0) do |sum, n| if Numeric === n sum + n else - return maybe_raise Errors::InvalidTypeError, "function sum() expects values to be numeric" + return maybe_raise Errors::InvalidTypeError, 'function sum() expects values to be numeric' end end else - return maybe_raise Errors::InvalidArityError, "function sum() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function sum() expects one argument' end end end @@ -411,7 +423,7 @@ def call(args) if args.count > 0 args.find { |value| !value.nil? } else - return maybe_raise Errors::InvalidArityError, "function not_null() expects one or more arguments" + return maybe_raise Errors::InvalidArityError, 'function not_null() expects one or more arguments' end end end @@ -424,29 +436,30 @@ class SortFunction < Function def call(args) if args.count == 1 value = args.first - if Array === value + if value.respond_to?(:to_ary) + value = value.to_ary # every element in the list must be of the same type array_type = get_type(value[0]) - if array_type == STRING_TYPE || array_type == NUMBER_TYPE || value.size == 0 + if array_type == STRING_TYPE || array_type == NUMBER_TYPE || value.empty? # stable sort n = 0 value.sort_by do |v| value_type = get_type(v) if value_type != array_type - msg = "function sort() expects values to be an array of only numbers, or only integers" + msg = 'function sort() expects values to be an array of only numbers, or only integers' return maybe_raise Errors::InvalidTypeError, msg end n += 1 [v, n] end else - return maybe_raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" + return maybe_raise Errors::InvalidTypeError, 'function sort() expects values to be an array of numbers or integers' end else - return maybe_raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" + return maybe_raise Errors::InvalidTypeError, 'function sort() expects values to be an array of numbers or integers' end else - return maybe_raise Errors::InvalidArityError, "function sort() expects one argument" + return maybe_raise Errors::InvalidArityError, 'function sort() expects one argument' end end end @@ -459,30 +472,30 @@ class SortByFunction < Function def call(args) if args.count == 2 if get_type(args[0]) == ARRAY_TYPE && get_type(args[1]) == EXPRESSION_TYPE - values = args[0] + values = args[0].to_ary expression = args[1] array_type = get_type(expression.eval(values[0])) - if array_type == STRING_TYPE || array_type == NUMBER_TYPE || values.size == 0 + if array_type == STRING_TYPE || array_type == NUMBER_TYPE || values.empty? # stable sort the list n = 0 values.sort_by do |value| value = expression.eval(value) value_type = get_type(value) if value_type != array_type - msg = "function sort() expects values to be an array of only numbers, or only integers" + msg = 'function sort() expects values to be an array of only numbers, or only integers' return maybe_raise Errors::InvalidTypeError, msg end n += 1 [value, n] end else - return maybe_raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" + return maybe_raise Errors::InvalidTypeError, 'function sort() expects values to be an array of numbers or integers' end else - return maybe_raise Errors::InvalidTypeError, "function sort_by() expects an array and an expression" + return maybe_raise Errors::InvalidTypeError, 'function sort_by() expects an array and an expression' end else - return maybe_raise Errors::InvalidArityError, "function sort_by() expects two arguments" + return maybe_raise Errors::InvalidArityError, 'function sort_by() expects two arguments' end end end @@ -495,6 +508,7 @@ def compare_by(mode, *args) values = args[0] expression = args[1] if get_type(values) == ARRAY_TYPE && get_type(expression) == EXPRESSION_TYPE + values = values.to_ary type = get_type(expression.eval(values.first)) if type != NUMBER_TYPE && type != STRING_TYPE msg = "function #{mode}() expects values to be strings or numbers" @@ -504,7 +518,7 @@ def compare_by(mode, *args) value = expression.eval(entry) value_type = get_type(value) if value_type != type - msg = "function #{mode}() encountered a type mismatch in " + msg = String.new("function #{mode}() encountered a type mismatch in ") msg << "sequence: #{type}, #{value_type}" return maybe_raise Errors::InvalidTypeError, msg end @@ -552,16 +566,16 @@ def call(args) search_type = get_type(search) suffix_type = get_type(suffix) if search_type != STRING_TYPE - msg = "function ends_with() expects first argument to be a string" + msg = 'function ends_with() expects first argument to be a string' return maybe_raise Errors::InvalidTypeError, msg end if suffix_type != STRING_TYPE - msg = "function ends_with() expects second argument to be a string" + msg = 'function ends_with() expects second argument to be a string' return maybe_raise Errors::InvalidTypeError, msg end search.end_with?(suffix) else - msg = "function ends_with() expects two arguments" + msg = 'function ends_with() expects two arguments' return maybe_raise Errors::InvalidArityError, msg end end @@ -578,16 +592,16 @@ def call(args) search_type = get_type(search) prefix_type = get_type(prefix) if search_type != STRING_TYPE - msg = "function starts_with() expects first argument to be a string" + msg = 'function starts_with() expects first argument to be a string' return maybe_raise Errors::InvalidTypeError, msg end if prefix_type != STRING_TYPE - msg = "function starts_with() expects second argument to be a string" + msg = 'function starts_with() expects second argument to be a string' return maybe_raise Errors::InvalidTypeError, msg end search.start_with?(prefix) else - msg = "function starts_with() expects two arguments" + msg = 'function starts_with() expects two arguments' return maybe_raise Errors::InvalidArityError, msg end end @@ -598,7 +612,7 @@ class MergeFunction < Function def call(args) if args.count == 0 - msg = "function merge() expects 1 or more arguments" + msg = 'function merge() expects 1 or more arguments' return maybe_raise Errors::InvalidArityError, msg end args.inject({}) do |h, v| @@ -612,14 +626,16 @@ class ReverseFunction < Function def call(args) if args.count == 0 - msg = "function reverse() expects 1 or more arguments" + msg = 'function reverse() expects 1 or more arguments' return maybe_raise Errors::InvalidArityError, msg end value = args.first - if Array === value || String === value - value.reverse + if value.respond_to?(:to_ary) + value.to_ary.reverse + elsif value.respond_to?(:to_str) + value.to_str.reverse else - msg = "function reverse() expects an array or string" + msg = 'function reverse() expects an array or string' return maybe_raise Errors::InvalidTypeError, msg end end @@ -630,7 +646,7 @@ class ToArrayFunction < Function def call(args) value = args.first - Array === value ? value : [value] + value.respond_to?(:to_ary) ? value.to_ary : [value] end end end diff --git a/lib/jmespath/nodes/index.rb b/lib/jmespath/nodes/index.rb index 5cf4120..5066c1a 100644 --- a/lib/jmespath/nodes/index.rb +++ b/lib/jmespath/nodes/index.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes diff --git a/lib/jmespath/nodes/literal.rb b/lib/jmespath/nodes/literal.rb index b73baa9..238958f 100644 --- a/lib/jmespath/nodes/literal.rb +++ b/lib/jmespath/nodes/literal.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes @@ -8,7 +9,7 @@ def initialize(value) @value = value end - def visit(value) + def visit(_value) @value end end diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb index 38bb295..1f90a8a 100644 --- a/lib/jmespath/nodes/multi_select_hash.rb +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes diff --git a/lib/jmespath/nodes/multi_select_list.rb b/lib/jmespath/nodes/multi_select_list.rb index 66ffc7b..a7b8dd9 100644 --- a/lib/jmespath/nodes/multi_select_list.rb +++ b/lib/jmespath/nodes/multi_select_list.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes diff --git a/lib/jmespath/nodes/not.rb b/lib/jmespath/nodes/not.rb index 4af3b1a..d1e86f9 100644 --- a/lib/jmespath/nodes/not.rb +++ b/lib/jmespath/nodes/not.rb @@ -1,7 +1,7 @@ +# frozen_string_literal: true module JMESPath module Nodes class Not < Node - def initialize(expression) @expression = expression end @@ -13,7 +13,6 @@ def visit(value) def optimize self.class.new(@expression.optimize) end - end end end diff --git a/lib/jmespath/nodes/or.rb b/lib/jmespath/nodes/or.rb index 42114ec..af2b87f 100644 --- a/lib/jmespath/nodes/or.rb +++ b/lib/jmespath/nodes/or.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes diff --git a/lib/jmespath/nodes/pipe.rb b/lib/jmespath/nodes/pipe.rb index 44c8023..d0fdafc 100644 --- a/lib/jmespath/nodes/pipe.rb +++ b/lib/jmespath/nodes/pipe.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index ef4e737..871dd13 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes @@ -12,9 +13,7 @@ def visit(value) list = [] targets.each do |v| vv = @projection.visit(v) - unless vv.nil? - list << vv - end + list << vv unless vv.nil? end list end @@ -30,7 +29,7 @@ def optimize private - def extract_targets(left_value) + def extract_targets(_left_value) nil end end @@ -45,11 +44,7 @@ def visit(value) class ArrayProjection < Projection def extract_targets(target) - if Array === target - target - else - nil - end + target.to_ary if target.respond_to?(:to_ary) end def fast_instance @@ -63,10 +58,10 @@ class FastArrayProjection < ArrayProjection class ObjectProjection < Projection def extract_targets(target) - if hash_like?(target) + if target.respond_to?(:to_hash) + target.to_hash.values + elsif target.is_a?(Struct) target.values - else - nil end end diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index 97f045f..1f6a8c5 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes @@ -6,11 +7,11 @@ def initialize(start, stop, step) @start = start @stop = stop @step = step - raise Errors::InvalidValueError.new('slice step cannot be 0') if @step == 0 + raise Errors::InvalidValueError, 'slice step cannot be 0' if @step == 0 end def visit(value) - if String === value || Array === value + if (value = value.respond_to?(:to_str) ? value.to_str : value.respond_to?(:to_ary) ? value.to_ary : nil) start, stop, step = adjust_slice(value.size, @start, @stop, @step) result = [] if step > 0 @@ -26,9 +27,7 @@ def visit(value) i += step end end - String === value ? result.join : result - else - nil + value.respond_to?(:to_str) ? result.join : result end end @@ -43,21 +42,19 @@ def optimize private def adjust_slice(length, start, stop, step) - if step.nil? - step = 1 - end + step = 1 if step.nil? - if start.nil? - start = step < 0 ? length - 1 : 0 - else - start = adjust_endpoint(length, start, step) - end + start = if start.nil? + step < 0 ? length - 1 : 0 + else + adjust_endpoint(length, start, step) + end - if stop.nil? - stop = step < 0 ? -1 : length - else - stop = adjust_endpoint(length, stop, step) - end + stop = if stop.nil? + step < 0 ? -1 : length + else + adjust_endpoint(length, stop, step) + end [start, stop, step] end @@ -80,10 +77,8 @@ def initialize(start, stop) end def visit(value) - if String === value || Array === value + if (value = value.respond_to?(:to_str) ? value.to_str : value.respond_to?(:to_ary) ? value.to_ary : nil) value[@start, @stop - @start] - else - nil end end end diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index 81e23dd..398fa02 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath # @api private module Nodes diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 8d0960d..adceafd 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -1,28 +1,28 @@ +# frozen_string_literal: true require 'set' module JMESPath # @api private class Parser - AFTER_DOT = Set.new([ - Lexer::T_IDENTIFIER, # foo.bar - Lexer::T_QUOTED_IDENTIFIER, # foo."bar" - Lexer::T_STAR, # foo.* - Lexer::T_LBRACE, # foo{a: 0} - Lexer::T_LBRACKET, # foo[1] - Lexer::T_FILTER, # foo.[?bar==10] - ]) + Lexer::T_IDENTIFIER, # foo.bar + Lexer::T_QUOTED_IDENTIFIER, # foo."bar" + Lexer::T_STAR, # foo.* + Lexer::T_LBRACE, # foo{a: 0} + Lexer::T_LBRACKET, # foo[1] + Lexer::T_FILTER, # foo.[?bar==10] + ]) NUM_COLON_RBRACKET = Set.new([ - Lexer::T_NUMBER, - Lexer::T_COLON, - Lexer::T_RBRACKET, - ]) + Lexer::T_NUMBER, + Lexer::T_COLON, + Lexer::T_RBRACKET + ]) COLON_RBRACKET = Set.new([ - Lexer::T_COLON, - Lexer::T_RBRACKET, - ]) + Lexer::T_COLON, + Lexer::T_RBRACKET + ]) CURRENT_NODE = Nodes::Current.new @@ -34,7 +34,7 @@ def initialize(options = {}) # @param [String] expression def parse(expression) - tokens = @lexer.tokenize(expression) + tokens = @lexer.tokenize(expression) stream = TokenStream.new(expression, tokens) result = expr(stream) if stream.token.type != Lexer::T_EOF @@ -110,13 +110,11 @@ def nud_identifier(stream) def nud_lbrace(stream) valid_keys = Set.new([:quoted_identifier, :identifier]) - stream.next(match:valid_keys) + stream.next(match: valid_keys) pairs = [] begin pairs << parse_key_value_pair(stream) - if stream.token.type == :comma - stream.next(match:valid_keys) - end + stream.next(match: valid_keys) if stream.token.type == :comma end while stream.token.type != :rbrace stream.next Nodes::MultiSelectHash.new(pairs) @@ -167,7 +165,7 @@ def led_comparator(stream, left) end def led_dot(stream, left) - stream.next(match:AFTER_DOT) + stream.next(match: AFTER_DOT) if stream.token.type == :star parse_wildcard_object(stream, left) else @@ -217,12 +215,10 @@ def led_lparen(stream, left) stream.next while stream.token.type != :rparen args << expr(stream, 0) - if stream.token.type == :comma - stream.next - end + stream.next if stream.token.type == :comma end stream.next - Nodes::Function.create(name, args, :disable_visit_errors => @disable_visit_errors) + Nodes::Function.create(name, args, disable_visit_errors: @disable_visit_errors) end def led_or(stream, left) @@ -286,7 +282,7 @@ def parse_dot(stream, binding_power) def parse_key_value_pair(stream) key = stream.token.value - stream.next(match:Set.new([:colon])) + stream.next(match: Set.new([:colon])) stream.next Nodes::MultiSelectHash::KeyValuePair.new(key, expr(stream)) end @@ -311,7 +307,7 @@ def parse_projection(stream, binding_power) if stream.token.binding_power < 10 CURRENT_NODE elsif type == :dot - stream.next(match:AFTER_DOT) + stream.next(match: AFTER_DOT) parse_dot(stream, binding_power) elsif type == :lbracket || type == :filter expr(stream, binding_power) @@ -321,7 +317,7 @@ def parse_projection(stream, binding_power) end def parse_wildcard_array(stream, left = nil) - stream.next(match:Set.new([:rbracket])) + stream.next(match: Set.new([:rbracket])) stream.next left ||= CURRENT_NODE right = parse_projection(stream, Token::BINDING_POWER[:star]) @@ -334,6 +330,5 @@ def parse_wildcard_object(stream, left = nil) right = parse_projection(stream, Token::BINDING_POWER[:star]) Nodes::ObjectProjection.new(left, right) end - end end diff --git a/lib/jmespath/runtime.rb b/lib/jmespath/runtime.rb index 171cfa7..2ee1873 100644 --- a/lib/jmespath/runtime.rb +++ b/lib/jmespath/runtime.rb @@ -1,7 +1,7 @@ +# frozen_string_literal: true module JMESPath # @api private class Runtime - # @api private DEFAULT_PARSER = CachingParser @@ -66,6 +66,5 @@ def default_parser(options) DEFAULT_PARSER.new(options) end end - end end diff --git a/lib/jmespath/token.rb b/lib/jmespath/token.rb index 303d5e9..f6fbd6a 100644 --- a/lib/jmespath/token.rb +++ b/lib/jmespath/token.rb @@ -1,7 +1,7 @@ +# frozen_string_literal: true module JMESPath # @api private class Token < Struct.new(:type, :value, :position, :binding_power) - NULL_TOKEN = Token.new(:eof, '', nil) BINDING_POWER = { @@ -28,8 +28,8 @@ class Token < Struct.new(:type, :value, :position, :binding_power) Lexer::T_NOT => 45, Lexer::T_LBRACE => 50, Lexer::T_LBRACKET => 55, - Lexer::T_LPAREN => 60, - } + Lexer::T_LPAREN => 60 + }.freeze # @param [Symbol] type # @param [Mixed] value @@ -37,6 +37,5 @@ class Token < Struct.new(:type, :value, :position, :binding_power) def initialize(type, value, position) super(type, value, position, BINDING_POWER[type]) end - end end diff --git a/lib/jmespath/token_stream.rb b/lib/jmespath/token_stream.rb index 76f5f0b..02295f1 100644 --- a/lib/jmespath/token_stream.rb +++ b/lib/jmespath/token_stream.rb @@ -1,7 +1,7 @@ +# frozen_string_literal: true module JMESPath # @api private class TokenStream - # @param [String] expression # @param [Array] tokens def initialize(expression, tokens) @@ -35,8 +35,8 @@ def lookahead(count) def inspect str = [] @tokens.each do |token| - str << "%3d %-15s %s" % - [token.position, token.type, token.value.inspect] + str << '%3d %-15s %s' % + [token.position, token.type, token.value.inspect] end str.join("\n") end @@ -50,11 +50,10 @@ def _next def validate_match(token, match) if match && !match.include?(token.type) - raise Errors::SyntaxError, "type missmatch" + raise Errors::SyntaxError, 'type missmatch' else token end end - end end diff --git a/lib/jmespath/util.rb b/lib/jmespath/util.rb index 9605393..682bc6f 100644 --- a/lib/jmespath/util.rb +++ b/lib/jmespath/util.rb @@ -1,18 +1,34 @@ +# frozen_string_literal: true module JMESPath # @api private module Util class << self - # Determines if a value is false as defined by JMESPath: # # https://github.com/jmespath/jmespath.site/blob/master/docs/proposals/improved-filters.rst#and-expressions-1 # def falsey?(value) !value || - (value.respond_to?(:empty?) && value.empty?) || - (value.respond_to?(:entries) && !value.entries.any?) + (value.respond_to?(:to_ary) && value.to_ary.empty?) || + (value.respond_to?(:to_hash) && value.to_hash.empty?) || + (value.respond_to?(:to_str) && value.to_str.empty?) || + (value.respond_to?(:entries) && !value.entries.any?) # final case necessary to support Enumerable and Struct end + + def as_json(value) + if value.respond_to?(:to_ary) + value.to_ary.map { |e| as_json(e) } + elsif value.respond_to?(:to_hash) + hash = {} + value.to_hash.each_pair { |k, v| hash[k] = as_json(v) } + hash + elsif value.respond_to?(:to_str) + value.to_str + else + value + end + end end end end diff --git a/lib/jmespath/version.rb b/lib/jmespath/version.rb index c7fa229..485a143 100644 --- a/lib/jmespath/version.rb +++ b/lib/jmespath/version.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module JMESPath - VERSION = '1.4.0' + VERSION = File.read(File.expand_path('../../../VERSION', __FILE__)).strip end diff --git a/spec/compliance/boolean.json b/spec/compliance/boolean.json index 60635ac..d10d8ee 100644 --- a/spec/compliance/boolean.json +++ b/spec/compliance/boolean.json @@ -207,6 +207,7 @@ "two": 2, "three": 3, "emptylist": [], + "complexlist": [{}, []], "boolvalue": false }, "cases": [ @@ -222,6 +223,22 @@ "expression": "one == one", "result": true }, + { + "expression": "emptylist == `[]`", + "result": true + }, + { + "expression": "emptylist == emptylist", + "result": true + }, + { + "expression": "complexlist == `[{}, []]`", + "result": true + }, + { + "expression": "complexlist == complexlist", + "result": true + }, { "expression": "one == two", "result": false @@ -269,6 +286,14 @@ { "expression": "two < one || three < one", "result": false + }, + { + "expression": "'2010-02-01' > '2011-05-01'", + "result": false + }, + { + "expression": "'2010-02-01' <= '2011-05-01'", + "result": true } ] } diff --git a/spec/compliance/filters.json b/spec/compliance/filters.json index 5b9f52b..5086e36 100644 --- a/spec/compliance/filters.json +++ b/spec/compliance/filters.json @@ -464,5 +464,29 @@ "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] } ] + }, + { + "given": { + "foo": ["2010-02-01", "2011-05-01"] + }, + "cases": [ + { + "comment": "Greater than with ISO 8601 date string", + "expression": "foo[?@ > '2010-06-01']", + "result": ["2011-05-01"] + } + ] + }, + { + "given": { + "foo": [{"date": "2010-02-01"}, {"date": "2011-05-01"}] + }, + "cases": [ + { + "comment": "Greater than with ISO 8601 date string", + "expression": "foo[?date > '2010-06-01'].date", + "result": ["2011-05-01"] + } + ] } ] diff --git a/spec/compliance/functions.json b/spec/compliance/functions.json index d2ec936..54747d4 100644 --- a/spec/compliance/functions.json +++ b/spec/compliance/functions.json @@ -4,7 +4,7 @@ "foo": -1, "zero": 0, "numbers": [-1, 3, 4, 5], - "array": [-1, 3, 4, 5, "a", "100"], + "array": [-1, 3, 4, 5, "a", "100", [{}]], "strings": ["a", "b", "c"], "decimals": [1.01, 1.2, -1.5], "str": "Str", @@ -131,6 +131,10 @@ "expression": "contains(decimals, `false`)", "result": false }, + { + "expression": "contains(array, `[{}]`)", + "result": true + }, { "expression": "ends_with(str, 'r')", "result": true @@ -201,7 +205,7 @@ }, { "expression": "length(array)", - "result": 6 + "result": 7 }, { "expression": "length(objects)", @@ -405,7 +409,7 @@ }, { "expression": "reverse(array)", - "result": ["100", "a", 5, 4, 3, -1] + "result": [[{}], "100", "a", 5, 4, 3, -1] }, { "expression": "reverse(`[]`)", diff --git a/spec/compliance_spec.rb b/spec/compliance_spec.rb index 9c8fcb7..07c8722 100644 --- a/spec/compliance_spec.rb +++ b/spec/compliance_spec.rb @@ -1,8 +1,8 @@ +# frozen_string_literal: true require 'spec_helper' describe 'Compliance' do Dir.glob('spec/{compliance,legacy}/*.json').each do |path| - test_file = File.basename(path).split('.').first next if test_file == 'benchmarks' next if ENV['TEST_FILE'] && ENV['TEST_FILE'] != test_file @@ -11,19 +11,17 @@ JMESPath.load_json(path).each do |scenario| describe("Given #{scenario['given'].to_json}") do scenario['cases'].each do |test_case| - if test_case['error'] it "the expression #{test_case['expression'].inspect} raises a #{test_case['error']} error" do - error_class = case test_case['error'] - when 'runtime' then JMESPath::Errors::RuntimeError - when 'syntax' then JMESPath::Errors::SyntaxError - when 'invalid-type' then JMESPath::Errors::InvalidTypeError - when 'invalid-value' then JMESPath::Errors::InvalidValueError - when 'invalid-arity' then JMESPath::Errors::InvalidArityError - when 'unknown-function' then JMESPath::Errors::UnknownFunctionError - else raise "unhandled error type #{test_case['error']}" + when 'runtime' then JMESPath::Errors::RuntimeError + when 'syntax' then JMESPath::Errors::SyntaxError + when 'invalid-type' then JMESPath::Errors::InvalidTypeError + when 'invalid-value' then JMESPath::Errors::InvalidValueError + when 'invalid-arity' then JMESPath::Errors::InvalidArityError + when 'unknown-function' then JMESPath::Errors::UnknownFunctionError + else raise "unhandled error type #{test_case['error']}" end raised = nil diff --git a/spec/compliance_without_errors_spec.rb b/spec/compliance_without_errors_spec.rb index 413ec91..4919603 100644 --- a/spec/compliance_without_errors_spec.rb +++ b/spec/compliance_without_errors_spec.rb @@ -1,40 +1,37 @@ +# frozen_string_literal: true require 'spec_helper' describe 'Compliance' do - PARSER = JMESPath::Parser.new(:disable_visit_errors => true) + PARSER = JMESPath::Parser.new(disable_visit_errors: true) Dir.glob('spec/compliance/*.json').each do |path| describe(File.basename(path).split('.').first) do JMESPath.load_json(path).each do |scenario| describe("Given #{scenario['given'].inspect}") do scenario['cases'].each do |test_case| + next unless test_case['error'] - if test_case['error'] - - if %w[invalid-type invalid-arity].include?(test_case['error']) - it "the expression #{test_case['expression'].inspect} returns nil if disable_visit_errors is true" do - - result = PARSER.parse(test_case['expression']).visit(scenario['given']) - expect(result).to be_nil + if %w(invalid-type invalid-arity).include?(test_case['error']) + it "the expression #{test_case['expression'].inspect} returns nil if disable_visit_errors is true" do + result = PARSER.parse(test_case['expression']).visit(scenario['given']) + expect(result).to be_nil + end + else + it "the expression #{test_case['expression'].inspect} raises a #{test_case['error']} error when parsing even if disable_visit_errors is true" do + error_class = case test_case['error'] + when 'syntax' then JMESPath::Errors::SyntaxError + when 'invalid-value' then JMESPath::Errors::InvalidValueError + when 'unknown-function' then JMESPath::Errors::UnknownFunctionError + else raise "unhandled error type #{test_case['error']}" end - else - it "the expression #{test_case['expression'].inspect} raises a #{test_case['error']} error when parsing even if disable_visit_errors is true" do - error_class = case test_case['error'] - when 'syntax' then JMESPath::Errors::SyntaxError - when 'invalid-value' then JMESPath::Errors::InvalidValueError - when 'unknown-function' then JMESPath::Errors::UnknownFunctionError - else raise "unhandled error type #{test_case['error']}" - end - - raised = nil - begin - PARSER.parse(test_case['expression']) - rescue JMESPath::Errors::Error => error - raised = error - end - - expect(raised).to be_kind_of(error_class) + raised = nil + begin + PARSER.parse(test_case['expression']) + rescue JMESPath::Errors::Error => error + raised = error end + + expect(raised).to be_kind_of(error_class) end end end diff --git a/spec/implicit_conversion_spec.rb b/spec/implicit_conversion_spec.rb new file mode 100644 index 0000000..eb0c0a6 --- /dev/null +++ b/spec/implicit_conversion_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true +require 'spec_helper' + +module Wrapper + def self.wrap(o) + o.respond_to?(:to_ary) ? Arrayish.new(o) : o.respond_to?(:to_hash) ? Hashish.new(o) : o + end +end + +class Arrayish + def initialize(ary) + @ary = ary.to_ary + end + + attr_reader :ary + + def to_ary + @ary.map { |e| Wrapper.wrap(e) } + end +end + +class Hashish + def initialize(hash) + @hash = hash.to_hash + end + + attr_reader :hash + + def to_hash + to_hash = {} + @hash.each_pair { |k, v| to_hash[k] = Wrapper.wrap(v) } + to_hash + end +end + +module JMESPath + describe '.search' do + describe 'implicit conversion' do + it 'searches hash/array structures' do + data = Hashish.new('foo' => { 'bar' => ['value'] }) + result = JMESPath.search('foo.bar', data) + expect(result).to be_instance_of(Arrayish) + expect(result.ary).to eq(['value']) + end + + it 'searches with flatten' do + data = Hashish.new('foo' => [[{ 'bar' => 0 }], [{ 'baz' => 0 }]]) + result = JMESPath.search('foo[]', data) + expect(result.size).to eq(2) + expect(result[0]).to be_instance_of(Hashish) + expect(result[0].hash).to eq('bar' => 0) + expect(result[1]).to be_instance_of(Hashish) + expect(result[1].hash).to eq('baz' => 0) + end + end + + describe 'Compliance' do + Dir.glob('spec/{compliance,legacy}/*.json').each do |path| + test_file = File.basename(path).split('.').first + next if test_file == 'benchmarks' + next if ENV['TEST_FILE'] && ENV['TEST_FILE'] != test_file + + describe(test_file) do + JMESPath.load_json(path).each do |scenario| + describe("Given #{scenario['given'].to_json}") do + scenario['cases'].each do |test_case| + next if test_case['error'] + it "searching #{test_case['expression'].inspect} returns #{test_case['result'].to_json}" do + result = JMESPath.search(test_case['expression'], Wrapper.wrap(scenario['given'])) + + expect(JMESPath::Util.as_json(result)).to eq(test_case['result']) + end + end + end + end + end + end + end + end +end diff --git a/spec/indifferent_access_spec.rb b/spec/indifferent_access_spec.rb index 8b2adb9..059ec28 100644 --- a/spec/indifferent_access_spec.rb +++ b/spec/indifferent_access_spec.rb @@ -1,11 +1,11 @@ +# frozen_string_literal: true require 'spec_helper' module JMESPath describe '.search' do describe 'indifferent access' do - it 'treats hashes indifferently with symbols/strings' do - data = {foo:{bar:{yuck:'abc'}}} + data = { foo: { bar: { yuck: 'abc' } } } expect(JMESPath.search('foo.bar.yuck', data)).to eq('abc') end @@ -26,7 +26,6 @@ module JMESPath ) expect(JMESPath.search('foo.baz.yuck', data)).to be(nil) end - end end end diff --git a/spec/jmespath_spec.rb b/spec/jmespath_spec.rb index 207092c..08dd1b9 100644 --- a/spec/jmespath_spec.rb +++ b/spec/jmespath_spec.rb @@ -1,24 +1,24 @@ +# frozen_string_literal: true require 'spec_helper' module JMESPath describe '.search' do - it 'searches data' do expression = 'foo.bar' - data = {'foo' => {'bar' => 'value'}} + data = { 'foo' => { 'bar' => 'value' } } expect(JMESPath.search(expression, data)).to eq('value') end it 'accepts data as a Pathname' do file_path = File.join(File.dirname(__FILE__), 'fixtures', 'sample.json') file_path = Pathname.new(file_path) - expect(JMESPath.search('foo.*.baz', file_path)).to eq([1,2,3]) + expect(JMESPath.search('foo.*.baz', file_path)).to eq([1, 2, 3]) end it 'accepts data as an IO object' do file_path = File.join(File.dirname(__FILE__), 'fixtures', 'sample.json') File.open(file_path, 'r') do |file| - expect(JMESPath.search('foo.*.baz', file)).to eq([1,2,3]) + expect(JMESPath.search('foo.*.baz', file)).to eq([1, 2, 3]) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dde4e71..0be5a51 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Using Bundler.require intentionally to simulate environments that have already # required a specific version of the `json` or `json_pure` gems. The actual version # loaded is decided by the specific gemfile being loaded from the gemfiles directory. diff --git a/tasks/benchmark.rake b/tasks/benchmark.rake index 2d33bef..ce5f087 100644 --- a/tasks/benchmark.rake +++ b/tasks/benchmark.rake @@ -1,6 +1,6 @@ +# frozen_string_literal: true desc 'Runs the benchmark suite' task 'benchmark' do - require 'jmespath' require 'absolute_time' @@ -11,7 +11,6 @@ task 'benchmark' do Dir.glob('benchmark/*.json').each do |path| JMESPath.load_json(path).first.tap do |scenario| scenario['cases'].each do |test_case| - expression = test_case['expression'] data = scenario['given'] @@ -24,7 +23,6 @@ task 'benchmark' do label = "#{scenario['description']} - #{test_case['name']}" printf("%fms, %s\n" % [time * 1000, label]) - end end end @@ -32,5 +30,5 @@ end desc 'Runs the benchmark suite, with expression caching' task 'benchmark:cached' do - sh({"CACHE" => "1"}, "bundle exec rake benchmark") + sh({ 'CACHE' => '1' }, 'bundle exec rake benchmark') end diff --git a/tasks/changelog.rake b/tasks/changelog.rake deleted file mode 100644 index aa3afff..0000000 --- a/tasks/changelog.rake +++ /dev/null @@ -1,36 +0,0 @@ -task 'changelog:version' do - # replaces "Next Release (TBD)" in the CHANGELOG with a version and date - changelog = File.open('CHANGELOG.md', 'r', encoding: 'UTF-8') { |f| f.read } - changelog = changelog.lines - changelog[0] = "#{$VERSION} (#{Time.now.strftime('%Y-%m-%d')})\n" - changelog = changelog.join - File.open('CHANGELOG.md', 'w', encoding: 'UTF-8') { |f| f.write(changelog) } - sh("git add CHANGELOG.md") -end - -task 'changelog:next_release' do - # inserts a "Next Release (TDB)" section at the top of the CHANGELOG - lines = [] - lines << "Unreleased Changes\n" - lines << "------------------\n" - lines << "\n" - changelog = File.open('CHANGELOG.md', 'r', encoding: 'UTF-8') { |f| f.read } - changelog = lines.join + changelog - File.open('CHANGELOG.md', 'w', encoding: 'UTF-8') { |f| f.write(changelog) } - sh("git add CHANGELOG.md") - sh("git commit -m 'Added next release section to the changelog.'") -end - -task 'changelog:latest' do - # Returns the contents of the most recent CHANGELOG section - changelog = File.open('CHANGELOG.md', 'r', encoding: 'UTF-8') { |f| f.read } - lines = [] - changelog.lines[3..-1].each do |line| - if line.match(/^\d+\.\d+\.\d+/) - break - else - lines << line - end - end - puts lines[0..-2].join -end diff --git a/tasks/docs.rake b/tasks/docs.rake deleted file mode 100644 index 522ca2d..0000000 --- a/tasks/docs.rake +++ /dev/null @@ -1,14 +0,0 @@ -task 'docs:clobber' do - rm_rf ".yardoc" - rm_rf "docs" -end - -desc "Generates docs.zip" -task 'docs:zip' => :docs do - sh("zip -9 -r -q docs.zip docs/") -end - -desc "Generate the API documentation." -task :docs => ['docs:clobber'] do - sh({"SOURCE" => "1"}, "bundle exec yard") -end diff --git a/tasks/gem.rake b/tasks/gem.rake deleted file mode 100644 index 0c3129c..0000000 --- a/tasks/gem.rake +++ /dev/null @@ -1,8 +0,0 @@ -desc 'Builds the gem' -task 'gem:build' do - sh("gem build jmespath.gemspec") -end - -task 'gem:push' do - sh("gem push jmespath-#{$VERSION}.gem") -end diff --git a/tasks/git.rake b/tasks/git.rake deleted file mode 100644 index c107af4..0000000 --- a/tasks/git.rake +++ /dev/null @@ -1,31 +0,0 @@ -task 'git:require-clean-workspace' do - # Ensure the git repo is free of unstaged or untracked files prior - # to building / testing / pushing a release. - unless `git diff --shortstat 2> /dev/null | tail -n1` == '' - warn('workspace must be clean to release') - exit - end -end - -task 'git:tag' do - sh("git commit -m \"Bumped version to v#{$VERSION}\"") - sh("git tag -a -m \"$(rake git:tag_message)\" v#{$VERSION}") -end - -task 'git:tag_message' do - issues = `git log $(git describe --tags --abbrev=0)...HEAD -E --grep '#[0-9]+' 2>/dev/null` - issues = issues.scan(/((?:\S+\/\S+)?#\d+)/).flatten - msg = "Tag release v#{$VERSION}" - msg << "\n\n" - unless issues.empty? - msg << "References: #{issues.uniq.sort.join(', ')}" - msg << "\n\n" - end - msg << `rake changelog:latest` - puts msg -end - -task 'git:push' do - sh('git push origin') - sh('git push origin --tags') -end diff --git a/tasks/github.rake b/tasks/github.rake deleted file mode 100644 index eaba366..0000000 --- a/tasks/github.rake +++ /dev/null @@ -1,29 +0,0 @@ -task 'github:require-access-token' do - unless $GITHUB_ACCESS_TOKEN - warn("you must export JMESPATH_GITHUB_ACCESS_TOKEN") - exit - end -end - -task 'github:release' do - require 'octokit' - - gh = Octokit::Client.new(access_token: $GITHUB_ACCESS_TOKEN) - - repo = 'trevorrowe/jmespath.rb' - tag_ref_sha = `git show-ref v#{$VERSION}`.split(' ').first - tag = gh.tag(repo, tag_ref_sha) - - release = gh.create_release(repo, "v#{$VERSION}", { - name: 'Release v' + $VERSION + ' - ' + tag.tagger.date.strftime('%Y-%m-%d'), - body: tag.message.lines[2..-1].join, - prerelease: $VERSION.match('rc') ? true : false, - }) - - gh.upload_asset(release.url, 'docs.zip', - :content_type => 'application/octet-stream') - - gh.upload_asset(release.url, "jmespath-#{$VERSION}.gem", - :content_type => 'application/octet-stream') - -end diff --git a/tasks/release b/tasks/release new file mode 160000 index 0000000..bcf3a74 --- /dev/null +++ b/tasks/release @@ -0,0 +1 @@ +Subproject commit bcf3a7470dd7b39af5a35a2e6ac99fc6587d1a00 diff --git a/tasks/release.rake b/tasks/release.rake deleted file mode 100644 index d3c1f61..0000000 --- a/tasks/release.rake +++ /dev/null @@ -1,42 +0,0 @@ -task 'release:require-version' do - unless ENV['VERSION'] - warn("usage: VERSION=x.y.z rake release") - exit - end -end - -task 'release:bump-version' do - sh("echo '#{$VERSION}' > VERSION") - path = 'lib/jmespath/version.rb' - file = File.read(path) - file = file.gsub(/VERSION = '.+?'/, "VERSION = '#{$VERSION}'") - File.open(path, 'w') { |f| f.write(file) } - sh("git add #{path}") - sh("git add VERSION") -end - -task 'release:stage' => [ - 'release:require-version', - 'github:require-access-token', - 'git:require-clean-workspace', - 'test:unit', - 'test:compliance', - 'changelog:version', - 'release:bump-version', - 'git:tag', - 'gem:build', - 'docs:zip', -] - -task 'release:publish' => [ - 'git:push', - 'gem:push', - 'github:release', -] - -task 'release:cleanup' => [ - 'changelog:next_release', -] - -desc "Public release" -task :release => ['release:stage', 'release:publish', 'release:cleanup'] diff --git a/tasks/test.rake b/tasks/test.rake index e437c8a..8763ac2 100644 --- a/tasks/test.rake +++ b/tasks/test.rake @@ -1,17 +1,19 @@ +# frozen_string_literal: true require 'rspec/core/rake_task' $RSPEC_OPTS = ENV['VERBOSE'] ? ' --format doc --color' : '' -desc "Run unit tests" +desc 'Run unit tests' RSpec::Core::RakeTask.new('test:unit') do |t| t.rspec_opts = "-I lib#{$RSPEC_OPTS}" t.exclude_pattern = 'spec/compliance_*.rb' end -desc "Run compliance tests" +desc 'Run compliance tests' RSpec::Core::RakeTask.new('test:compliance') do |t| t.rspec_opts = "-I lib#{$RSPEC_OPTS}" t.pattern = 'spec/compliance_*.rb' end task 'test' => ['test:unit', 'test:compliance'] +task 'release:test' => :test