code-ruby 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +8 -8
- data/code-ruby.gemspec +0 -3
- data/lib/code/error.rb +3 -0
- data/lib/code/node/call.rb +1 -0
- data/lib/code/node/chained_call.rb +4 -2
- data/lib/code/node/list.rb +2 -3
- data/lib/code/node/name.rb +7 -34
- data/lib/code/node/statement.rb +1 -1
- data/lib/code/node/string.rb +9 -4
- data/lib/code/node/string_component.rb +6 -6
- data/lib/code/node.rb +2 -2
- data/lib/code/object/argument.rb +1 -1
- data/lib/code/object/decimal.rb +99 -27
- data/lib/code/object/dictionnary.rb +29 -13
- data/lib/code/object/function.rb +11 -9
- data/lib/code/object/global.rb +37 -0
- data/lib/code/object/integer.rb +99 -35
- data/lib/code/object/list.rb +62 -89
- data/lib/code/object/range.rb +33 -46
- data/lib/code/object/ruby_function.rb +25 -0
- data/lib/code/object/string.rb +25 -15
- data/lib/code/object.rb +51 -49
- data/lib/code/parser/function.rb +3 -1
- data/lib/code/parser/if.rb +1 -1
- data/lib/code/parser/string.rb +8 -8
- data/lib/code/ruby.rb +161 -0
- data/lib/code-ruby.rb +6 -0
- data/lib/code.rb +20 -6
- data/lib/template/version.rb +1 -1
- data/lib/template-ruby.rb +6 -0
- data/lib/template.rb +24 -9
- data/spec/code/parser/string_spec.rb +1 -1
- data/spec/code_spec.rb +63 -1
- data/spec/template_spec.rb +25 -6
- data/template-ruby.gemspec +0 -3
- metadata +5 -30
    
        data/lib/code/object.rb
    CHANGED
    
    | @@ -5,38 +5,35 @@ class Code | |
| 5 5 | 
             
                def call(**args)
         | 
| 6 6 | 
             
                  operator = args.fetch(:operator, nil)
         | 
| 7 7 | 
             
                  arguments = args.fetch(:arguments, [])
         | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 8 | 
            +
             | 
| 9 | 
            +
                  if operator == "=="
         | 
| 10 | 
            +
                    sig(arguments, ::Code::Object)
         | 
| 11 | 
            +
                    equal(arguments.first.value)
         | 
| 12 | 
            +
                  elsif operator == "==="
         | 
| 13 | 
            +
                    sig(arguments, ::Code::Object)
         | 
| 14 | 
            +
                    strict_equal(arguments.first.value)
         | 
| 15 | 
            +
                  elsif operator == "!="
         | 
| 16 | 
            +
                    sig(arguments, ::Code::Object)
         | 
| 17 | 
            +
                    different(arguments.first.value)
         | 
| 10 18 | 
             
                  elsif operator == "<=>"
         | 
| 11 | 
            -
                     | 
| 19 | 
            +
                    sig(arguments, ::Code::Object)
         | 
| 20 | 
            +
                    compare(arguments.first.value)
         | 
| 12 21 | 
             
                  elsif operator == "&&"
         | 
| 13 | 
            -
                     | 
| 22 | 
            +
                    sig(arguments, ::Code::Object)
         | 
| 23 | 
            +
                    and_operator(arguments.first.value)
         | 
| 14 24 | 
             
                  elsif operator == "||"
         | 
| 15 | 
            -
                     | 
| 25 | 
            +
                    sig(arguments, ::Code::Object)
         | 
| 26 | 
            +
                    or_operator(arguments.first.value)
         | 
| 16 27 | 
             
                  elsif operator == "to_string"
         | 
| 17 | 
            -
                     | 
| 28 | 
            +
                    sig(arguments)
         | 
| 29 | 
            +
                    to_string
         | 
| 18 30 | 
             
                  else
         | 
| 19 | 
            -
                    raise | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 31 | 
            +
                    raise(
         | 
| 32 | 
            +
                      Code::Error::Undefined.new("#{operator} not defined on #{inspect}"),
         | 
| 33 | 
            +
                    )
         | 
| 22 34 | 
             
                  end
         | 
| 23 35 | 
             
                end
         | 
| 24 36 |  | 
| 25 | 
            -
                def []=(key, value)
         | 
| 26 | 
            -
                  @attributes ||= {}
         | 
| 27 | 
            -
                  @attributes[key] = value
         | 
| 28 | 
            -
                end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                def [](key)
         | 
| 31 | 
            -
                  @attributes ||= {}
         | 
| 32 | 
            -
                  @attributes[key]
         | 
| 33 | 
            -
                end
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                def key?(key)
         | 
| 36 | 
            -
                  @attributes ||= {}
         | 
| 37 | 
            -
                  @attributes.key?(key)
         | 
| 38 | 
            -
                end
         | 
| 39 | 
            -
             | 
| 40 37 | 
             
                def truthy?
         | 
| 41 38 | 
             
                  true
         | 
| 42 39 | 
             
                end
         | 
| @@ -78,10 +75,12 @@ class Code | |
| 78 75 |  | 
| 79 76 | 
             
                def sig(actual_arguments, *expected_arguments)
         | 
| 80 77 | 
             
                  if actual_arguments.size != expected_arguments.size
         | 
| 81 | 
            -
                    raise | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
                           | 
| 78 | 
            +
                    raise(
         | 
| 79 | 
            +
                      ::Code::Error::ArgumentError.new(
         | 
| 80 | 
            +
                        "Expected #{expected_arguments.size} arguments, " \
         | 
| 81 | 
            +
                          "got #{actual_arguments.size} arguments",
         | 
| 82 | 
            +
                      ),
         | 
| 83 | 
            +
                    )
         | 
| 85 84 | 
             
                  end
         | 
| 86 85 |  | 
| 87 86 | 
             
                  expected_arguments.each.with_index do |expected_argument, index|
         | 
| @@ -91,46 +90,49 @@ class Code | |
| 91 90 | 
             
                      if expected_argument.none? { |expected_arg|
         | 
| 92 91 | 
             
                           actual_argument.is_a?(expected_arg)
         | 
| 93 92 | 
             
                         }
         | 
| 94 | 
            -
                        raise | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 93 | 
            +
                        raise(
         | 
| 94 | 
            +
                          ::Code::Error::TypeError.new(
         | 
| 95 | 
            +
                            "Expected #{expected_argument}, got #{actual_argument.class}",
         | 
| 96 | 
            +
                          ),
         | 
| 97 | 
            +
                        )
         | 
| 97 98 | 
             
                      end
         | 
| 98 99 | 
             
                    else
         | 
| 99 100 | 
             
                      if !actual_argument.is_a?(expected_argument)
         | 
| 100 | 
            -
                        raise | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 101 | 
            +
                        raise(
         | 
| 102 | 
            +
                          ::Code::Error::TypeError.new(
         | 
| 103 | 
            +
                            "Expected #{expected_argument}, got #{actual_argument.class}",
         | 
| 104 | 
            +
                          ),
         | 
| 105 | 
            +
                        )
         | 
| 103 106 | 
             
                      end
         | 
| 104 107 | 
             
                    end
         | 
| 105 108 | 
             
                  end
         | 
| 106 109 | 
             
                end
         | 
| 107 110 |  | 
| 108 | 
            -
                def  | 
| 109 | 
            -
                   | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 111 | 
            +
                def equal(other)
         | 
| 112 | 
            +
                  ::Code::Object::Boolean.new(self == other)
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def strict_equal(other)
         | 
| 116 | 
            +
                  ::Code::Object::Boolean.new(self === other)
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def different(other)
         | 
| 120 | 
            +
                  ::Code::Object::Boolean.new(self != other)
         | 
| 112 121 | 
             
                end
         | 
| 113 122 |  | 
| 114 | 
            -
                def compare( | 
| 115 | 
            -
                  sig(arguments, ::Code::Object)
         | 
| 116 | 
            -
                  other = arguments.first.value
         | 
| 123 | 
            +
                def compare(other)
         | 
| 117 124 | 
             
                  ::Code::Object::Integer.new(self <=> other)
         | 
| 118 125 | 
             
                end
         | 
| 119 126 |  | 
| 120 | 
            -
                def and_operator( | 
| 121 | 
            -
                  sig(arguments, ::Code::Object)
         | 
| 122 | 
            -
                  other = arguments.first.value
         | 
| 127 | 
            +
                def and_operator(other)
         | 
| 123 128 | 
             
                  truthy? ? other : self
         | 
| 124 129 | 
             
                end
         | 
| 125 130 |  | 
| 126 | 
            -
                def or_operator( | 
| 127 | 
            -
                  sig(arguments, ::Code::Object)
         | 
| 128 | 
            -
                  other = arguments.first.value
         | 
| 131 | 
            +
                def or_operator(other)
         | 
| 129 132 | 
             
                  truthy? ? self : other
         | 
| 130 133 | 
             
                end
         | 
| 131 134 |  | 
| 132 | 
            -
                def to_string | 
| 133 | 
            -
                  sig(arguments)
         | 
| 135 | 
            +
                def to_string
         | 
| 134 136 | 
             
                  ::Code::Object::String.new(to_s)
         | 
| 135 137 | 
             
                end
         | 
| 136 138 | 
             
              end
         | 
    
        data/lib/code/parser/function.rb
    CHANGED
    
    | @@ -30,7 +30,9 @@ class Code | |
| 30 30 | 
             
                    ampersand.as(:block).maybe >>
         | 
| 31 31 | 
             
                      (asterisk >> asterisk).as(:keyword_splat).maybe >>
         | 
| 32 32 | 
             
                      asterisk.as(:splat).maybe >> name >>
         | 
| 33 | 
            -
                      ( | 
| 33 | 
            +
                      (
         | 
| 34 | 
            +
                        whitespace? >> equal >> whitespace? >> code_present.as(:default)
         | 
| 35 | 
            +
                      ).maybe
         | 
| 34 36 | 
             
                  end
         | 
| 35 37 |  | 
| 36 38 | 
             
                  rule(:argument) do
         | 
    
        data/lib/code/parser/if.rb
    CHANGED
    
    
    
        data/lib/code/parser/string.rb
    CHANGED
    
    | @@ -49,29 +49,29 @@ class Code | |
| 49 49 | 
             
                  end
         | 
| 50 50 |  | 
| 51 51 | 
             
                  rule(:single_quoted_character) do
         | 
| 52 | 
            -
                    escaped_character | | 
| 52 | 
            +
                    escaped_character |
         | 
| 53 | 
            +
                      (opening_curly_bracket.absent? >> single_quote.absent? >> any)
         | 
| 53 54 | 
             
                  end
         | 
| 54 55 |  | 
| 55 56 | 
             
                  rule(:double_quoted_character) do
         | 
| 56 | 
            -
                    escaped_character | | 
| 57 | 
            +
                    escaped_character |
         | 
| 58 | 
            +
                      (opening_curly_bracket.absent? >> double_quote.absent? >> any)
         | 
| 57 59 | 
             
                  end
         | 
| 58 60 |  | 
| 59 61 | 
             
                  rule(:single_quoted_string) do
         | 
| 60 62 | 
             
                    single_quote.ignore >>
         | 
| 61 63 | 
             
                      (
         | 
| 62 64 | 
             
                        interpolation.as(:interpolation) |
         | 
| 63 | 
            -
             | 
| 64 | 
            -
                      ).repeat >>
         | 
| 65 | 
            -
                      single_quote.ignore
         | 
| 65 | 
            +
                          single_quoted_character.repeat(1).as(:characters)
         | 
| 66 | 
            +
                      ).repeat >> single_quote.ignore
         | 
| 66 67 | 
             
                  end
         | 
| 67 68 |  | 
| 68 69 | 
             
                  rule(:double_quoted_string) do
         | 
| 69 70 | 
             
                    double_quote.ignore >>
         | 
| 70 71 | 
             
                      (
         | 
| 71 72 | 
             
                        interpolation.as(:interpolation) |
         | 
| 72 | 
            -
             | 
| 73 | 
            -
                      ).repeat >>
         | 
| 74 | 
            -
                      double_quote.ignore
         | 
| 73 | 
            +
                          double_quoted_character.repeat(1).as(:characters)
         | 
| 74 | 
            +
                      ).repeat >> double_quote.ignore
         | 
| 75 75 | 
             
                  end
         | 
| 76 76 |  | 
| 77 77 | 
             
                  rule(:symbol) { colon.ignore >> name }
         | 
    
        data/lib/code/ruby.rb
    ADDED
    
    | @@ -0,0 +1,161 @@ | |
| 1 | 
            +
            class Code
         | 
| 2 | 
            +
              class Ruby
         | 
| 3 | 
            +
                def initialize(raw = {})
         | 
| 4 | 
            +
                  @raw = raw
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def self.to_code(raw)
         | 
| 8 | 
            +
                  new(raw).to_code
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def self.from_code(raw)
         | 
| 12 | 
            +
                  new(raw).from_code
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def to_code
         | 
| 16 | 
            +
                  if code?
         | 
| 17 | 
            +
                    raw
         | 
| 18 | 
            +
                  elsif true?
         | 
| 19 | 
            +
                    ::Code::Object::Boolean.new(raw)
         | 
| 20 | 
            +
                  elsif false?
         | 
| 21 | 
            +
                    ::Code::Object::Boolean.new(raw)
         | 
| 22 | 
            +
                  elsif string?
         | 
| 23 | 
            +
                    ::Code::Object::String.new(raw)
         | 
| 24 | 
            +
                  elsif symbol?
         | 
| 25 | 
            +
                    ::Code::Object::String.new(raw.to_s)
         | 
| 26 | 
            +
                  elsif integer?
         | 
| 27 | 
            +
                    ::Code::Object::Integer.new(raw)
         | 
| 28 | 
            +
                  elsif float?
         | 
| 29 | 
            +
                    ::Code::Object::Decimal.new(raw.to_s)
         | 
| 30 | 
            +
                  elsif big_decimal?
         | 
| 31 | 
            +
                    ::Code::Object::Decimal.new(raw)
         | 
| 32 | 
            +
                  elsif hash?
         | 
| 33 | 
            +
                    ::Code::Object::Dictionnary.new(
         | 
| 34 | 
            +
                      raw.map do |key, value|
         | 
| 35 | 
            +
                        [::Code::Ruby.to_code(key), ::Code::Ruby.to_code(value)]
         | 
| 36 | 
            +
                      end.to_h
         | 
| 37 | 
            +
                    )
         | 
| 38 | 
            +
                  elsif array?
         | 
| 39 | 
            +
                    ::Code::Object::List.new(
         | 
| 40 | 
            +
                      raw.map do |element|
         | 
| 41 | 
            +
                        ::Code::Ruby.to_code(key)
         | 
| 42 | 
            +
                      end
         | 
| 43 | 
            +
                    )
         | 
| 44 | 
            +
                  elsif proc?
         | 
| 45 | 
            +
                    ::Code::Object::RubyFunction.new(raw)
         | 
| 46 | 
            +
                  else
         | 
| 47 | 
            +
                    raise "Unsupported class #{raw.class} for Ruby to Code conversion"
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def from_code
         | 
| 52 | 
            +
                  if code?
         | 
| 53 | 
            +
                    if code_boolean?
         | 
| 54 | 
            +
                      raw.raw
         | 
| 55 | 
            +
                    elsif code_decimal?
         | 
| 56 | 
            +
                      raw.raw
         | 
| 57 | 
            +
                    elsif code_integer?
         | 
| 58 | 
            +
                      raw.raw
         | 
| 59 | 
            +
                    elsif code_nothing?
         | 
| 60 | 
            +
                      raw.raw
         | 
| 61 | 
            +
                    elsif code_range?
         | 
| 62 | 
            +
                      raw.raw
         | 
| 63 | 
            +
                    elsif code_string?
         | 
| 64 | 
            +
                      raw.raw
         | 
| 65 | 
            +
                    elsif code_dictionnary?
         | 
| 66 | 
            +
                      raw.raw.map do |key, value|
         | 
| 67 | 
            +
                        [::Code::Ruby.to_code(key), ::Code::Ruby.to_code(value)]
         | 
| 68 | 
            +
                      end.to_h
         | 
| 69 | 
            +
                    elsif code_list?
         | 
| 70 | 
            +
                      raw.raw.map do |element|
         | 
| 71 | 
            +
                        ::Code::Ruby.to_code(element)
         | 
| 72 | 
            +
                      end
         | 
| 73 | 
            +
                    else
         | 
| 74 | 
            +
                      raise "Unsupported class #{raw.class} for Code to Ruby conversion"
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                  else
         | 
| 77 | 
            +
                    raw
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                private
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                attr_reader :raw
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def code?
         | 
| 86 | 
            +
                  raw.is_a?(::Code::Object)
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                def true?
         | 
| 90 | 
            +
                  raw.is_a?(::TrueClass)
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def false?
         | 
| 94 | 
            +
                  raw.is_a?(::FalseClass)
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def hash?
         | 
| 98 | 
            +
                  raw.is_a?(::Hash)
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def array?
         | 
| 102 | 
            +
                  raw.is_a?(::Array)
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def string?
         | 
| 106 | 
            +
                  raw.is_a?(::String)
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def symbol?
         | 
| 110 | 
            +
                  raw.is_a?(::Symbol)
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                def integer?
         | 
| 114 | 
            +
                  raw.is_a?(::Integer)
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def float?
         | 
| 118 | 
            +
                  raw.is_a?(::Float)
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                def big_decimal?
         | 
| 122 | 
            +
                  raw.is_a?(::BigDecimal)
         | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                def proc?
         | 
| 126 | 
            +
                  raw.is_a?(::Proc)
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                def code_boolean?
         | 
| 130 | 
            +
                  raw.is_a?(::Code::Object::Boolean)
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                def code_decimal?
         | 
| 134 | 
            +
                  raw.is_a?(::Code::Object::Decimal)
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                def code_integer?
         | 
| 138 | 
            +
                  raw.is_a?(::Code::Object::Integer)
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def code_nothing?
         | 
| 142 | 
            +
                  raw.is_a?(::Code::Object::Nothing)
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                def code_range?
         | 
| 146 | 
            +
                  raw.is_a?(::Code::Object::Range)
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                def code_string?
         | 
| 150 | 
            +
                  raw.is_a?(::Code::Object::String)
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                def code_dictionnary?
         | 
| 154 | 
            +
                  raw.is_a?(::Code::Object::Dictionnary)
         | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                def code_list?
         | 
| 158 | 
            +
                  raw.is_a?(::Code::Object::List)
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
              end
         | 
| 161 | 
            +
            end
         | 
    
        data/lib/code-ruby.rb
    CHANGED
    
    | @@ -11,3 +11,9 @@ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false) | |
| 11 11 | 
             
            loader.ignore("#{__dir__}/template-ruby.rb")
         | 
| 12 12 | 
             
            loader.ignore("#{__dir__}/code-ruby.rb")
         | 
| 13 13 | 
             
            loader.setup
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            class Hash
         | 
| 16 | 
            +
              def multi_fetch(*keys)
         | 
| 17 | 
            +
                keys.map { |key| [key, fetch(key)] }.to_h
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
    
        data/lib/code.rb
    CHANGED
    
    | @@ -1,30 +1,44 @@ | |
| 1 1 | 
             
            class Code
         | 
| 2 | 
            +
              GLOBALS = [:io, :context, :object]
         | 
| 2 3 | 
             
              DEFAULT_TIMEOUT = Template::DEFAULT_TIMEOUT
         | 
| 3 4 |  | 
| 4 | 
            -
              def initialize(input, io: $stdout, timeout: DEFAULT_TIMEOUT)
         | 
| 5 | 
            +
              def initialize(input, io: $stdout, timeout: DEFAULT_TIMEOUT, ruby: {})
         | 
| 5 6 | 
             
                @input = input
         | 
| 6 | 
            -
                @parsed = | 
| 7 | 
            +
                @parsed =
         | 
| 8 | 
            +
                  Timeout.timeout(timeout) { ::Code::Parser::Code.new.parse(@input) }
         | 
| 7 9 | 
             
                @io = io
         | 
| 8 10 | 
             
                @timeout = timeout || DEFAULT_TIMEOUT
         | 
| 11 | 
            +
                @ruby = ::Code::Ruby.to_code(ruby || {})
         | 
| 9 12 | 
             
              end
         | 
| 10 13 |  | 
| 11 | 
            -
              def self.evaluate(input, context = "", io: $stdout, timeout: DEFAULT_TIMEOUT)
         | 
| 12 | 
            -
                new(input, io: io, timeout: timeout).evaluate(context)
         | 
| 14 | 
            +
              def self.evaluate(input, context = "", io: $stdout, timeout: DEFAULT_TIMEOUT, ruby: {})
         | 
| 15 | 
            +
                new(input, io: io, timeout: timeout, ruby: ruby).evaluate(context)
         | 
| 13 16 | 
             
              end
         | 
| 14 17 |  | 
| 15 18 | 
             
              def evaluate(context = "")
         | 
| 16 19 | 
             
                Timeout.timeout(timeout) do
         | 
| 17 20 | 
             
                  if context.present?
         | 
| 18 | 
            -
                    context = ::Code.evaluate( | 
| 21 | 
            +
                    context = ::Code.evaluate(
         | 
| 22 | 
            +
                      context,
         | 
| 23 | 
            +
                      timeout: timeout,
         | 
| 24 | 
            +
                      io: io,
         | 
| 25 | 
            +
                      ruby: ruby
         | 
| 26 | 
            +
                    )
         | 
| 19 27 | 
             
                  else
         | 
| 20 28 | 
             
                    context = ::Code::Object::Dictionnary.new
         | 
| 21 29 | 
             
                  end
         | 
| 22 30 |  | 
| 31 | 
            +
                  if !context.is_a?(::Code::Object::Dictionnary)
         | 
| 32 | 
            +
                    raise ::Code::Error::IncompatibleContext.new("context must be a dictionnary")
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  context = context.merge(ruby)
         | 
| 36 | 
            +
             | 
| 23 37 | 
             
                  ::Code::Node::Code.new(parsed).evaluate(context: context, io: io)
         | 
| 24 38 | 
             
                end
         | 
| 25 39 | 
             
              end
         | 
| 26 40 |  | 
| 27 41 | 
             
              private
         | 
| 28 42 |  | 
| 29 | 
            -
              attr_reader :input, :parsed, :timeout, :io
         | 
| 43 | 
            +
              attr_reader :input, :parsed, :timeout, :io, :ruby
         | 
| 30 44 | 
             
            end
         | 
    
        data/lib/template/version.rb
    CHANGED
    
    
    
        data/lib/template-ruby.rb
    CHANGED
    
    | @@ -11,3 +11,9 @@ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false) | |
| 11 11 | 
             
            loader.ignore("#{__dir__}/template-ruby.rb")
         | 
| 12 12 | 
             
            loader.ignore("#{__dir__}/code-ruby.rb")
         | 
| 13 13 | 
             
            loader.setup
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            class Hash
         | 
| 16 | 
            +
              def multi_fetch(*keys)
         | 
| 17 | 
            +
                keys.map { |key| [key, fetch(key)] }.to_h
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
    
        data/lib/template.rb
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            class Template
         | 
| 2 2 | 
             
              DEFAULT_TIMEOUT = 10
         | 
| 3 3 |  | 
| 4 | 
            -
              def initialize(input, io: StringIO.new, timeout: DEFAULT_TIMEOUT)
         | 
| 4 | 
            +
              def initialize(input, io: StringIO.new, timeout: DEFAULT_TIMEOUT, ruby: {})
         | 
| 5 5 | 
             
                @input = input
         | 
| 6 6 | 
             
                @parsed =
         | 
| 7 7 | 
             
                  Timeout.timeout(timeout) do
         | 
| @@ -9,24 +9,39 @@ class Template | |
| 9 9 | 
             
                  end
         | 
| 10 10 | 
             
                @io = io
         | 
| 11 11 | 
             
                @timeout = timeout || DEFAULT_TIMEOUT
         | 
| 12 | 
            +
                @ruby = ::Code::Ruby.to_code(ruby || {})
         | 
| 12 13 | 
             
              end
         | 
| 13 14 |  | 
| 14 | 
            -
              def self.render( | 
| 15 | 
            -
                 | 
| 15 | 
            +
              def self.render(
         | 
| 16 | 
            +
                input,
         | 
| 17 | 
            +
                context = "",
         | 
| 18 | 
            +
                io: StringIO.new,
         | 
| 19 | 
            +
                timeout: DEFAULT_TIMEOUT,
         | 
| 20 | 
            +
                ruby: {}
         | 
| 21 | 
            +
              )
         | 
| 22 | 
            +
                new(input, io: io, timeout: timeout, ruby: ruby).render(context)
         | 
| 16 23 | 
             
              end
         | 
| 17 24 |  | 
| 18 25 | 
             
              def render(context = "")
         | 
| 19 26 | 
             
                Timeout.timeout(timeout) do
         | 
| 20 27 | 
             
                  if context.present?
         | 
| 21 | 
            -
                    context = ::Code.evaluate( | 
| 28 | 
            +
                    context = ::Code.evaluate(
         | 
| 29 | 
            +
                      context,
         | 
| 30 | 
            +
                      timeout: timeout,
         | 
| 31 | 
            +
                      io: io,
         | 
| 32 | 
            +
                      ruby: ruby
         | 
| 33 | 
            +
                    )
         | 
| 22 34 | 
             
                  else
         | 
| 23 35 | 
             
                    context = ::Code::Object::Dictionnary.new
         | 
| 24 36 | 
             
                  end
         | 
| 25 37 |  | 
| 26 | 
            -
                  :: | 
| 27 | 
            -
                    context | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 38 | 
            +
                  if !context.is_a?(::Code::Object::Dictionnary)
         | 
| 39 | 
            +
                    raise ::Code::Template::IncompatibleContext.new("context must be a dictionnary")
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  context = context.merge(ruby)
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  ::Template::Node::Template.new(parsed).evaluate(context: context, io: io)
         | 
| 30 45 |  | 
| 31 46 | 
             
                  io.is_a?(StringIO) ? io.string : nil
         | 
| 32 47 | 
             
                end
         | 
| @@ -34,7 +49,7 @@ class Template | |
| 34 49 |  | 
| 35 50 | 
             
              private
         | 
| 36 51 |  | 
| 37 | 
            -
              attr_reader :parsed, :io, :timeout
         | 
| 52 | 
            +
              attr_reader :parsed, :io, :timeout, :ruby
         | 
| 38 53 | 
             
            end
         | 
| 39 54 |  | 
| 40 55 | 
             
            require_relative "template/version"
         | 
    
        data/spec/code_spec.rb
    CHANGED
    
    | @@ -1,7 +1,15 @@ | |
| 1 1 | 
             
            require "spec_helper"
         | 
| 2 2 |  | 
| 3 3 | 
             
            RSpec.describe Code do
         | 
| 4 | 
            -
               | 
| 4 | 
            +
              let!(:input) { "" }
         | 
| 5 | 
            +
              let!(:context) { "" }
         | 
| 6 | 
            +
              let!(:io) { StringIO.new }
         | 
| 7 | 
            +
              let!(:timeout) { 0.1 }
         | 
| 8 | 
            +
              let!(:ruby) { {} }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              subject do
         | 
| 11 | 
            +
                Code.evaluate(input, context, io: io, timeout: timeout, ruby: ruby).to_s
         | 
| 12 | 
            +
              end
         | 
| 5 13 |  | 
| 6 14 | 
             
              [
         | 
| 7 15 | 
             
                ["nothing", ""],
         | 
| @@ -108,6 +116,7 @@ RSpec.describe Code do | |
| 108 116 | 
             
                ["1.0 << 1", "2"],
         | 
| 109 117 | 
             
                ["1 << 1.0", "2"],
         | 
| 110 118 | 
             
                ["1.0 << 1.0", "2"],
         | 
| 119 | 
            +
                ["eval('1 + 1')", "2"],
         | 
| 111 120 | 
             
              ].each do |(input, expected)|
         | 
| 112 121 | 
             
                context input.inspect do
         | 
| 113 122 | 
             
                  let(:input) { input }
         | 
| @@ -117,4 +126,57 @@ RSpec.describe Code do | |
| 117 126 | 
             
                  end
         | 
| 118 127 | 
             
                end
         | 
| 119 128 | 
             
              end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
              context "with ruby" do
         | 
| 131 | 
            +
                context "with a constant" do
         | 
| 132 | 
            +
                  let!(:input) { "a + a" }
         | 
| 133 | 
            +
                  let!(:ruby) { { a: 1 } }
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                  it "can access a" do
         | 
| 136 | 
            +
                    expect(subject).to eq("2")
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                context "with a function without arguments" do
         | 
| 141 | 
            +
                  let!(:input) { "a + a" }
         | 
| 142 | 
            +
                  let!(:ruby) { { a: ->{ "hello" } } }
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  it "can call a" do
         | 
| 145 | 
            +
                    expect(subject).to eq("hellohello")
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                context "with a function with regular arguments" do
         | 
| 150 | 
            +
                  let!(:input) { "add(1, 2)" }
         | 
| 151 | 
            +
                  let!(:ruby) { { add: ->(a, b){ a + b } } }
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                  it "can call add" do
         | 
| 154 | 
            +
                    expect(subject).to eq("3")
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                context "with a function with keyword arguments" do
         | 
| 159 | 
            +
                  let!(:input) { "add(a: 1, b: 2)" }
         | 
| 160 | 
            +
                  let!(:ruby) { { add: ->(a:, b:){ a + b } } }
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                  it "can call add" do
         | 
| 163 | 
            +
                    expect(subject).to eq("3")
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                context "with a complex function" do
         | 
| 168 | 
            +
                  let!(:input) { "add(1, 1, 1, 1, c: 1, d: 1, e: 1)" }
         | 
| 169 | 
            +
                  let!(:ruby) do
         | 
| 170 | 
            +
                    {
         | 
| 171 | 
            +
                      add: ->(a, b = 1, *args, c:, d: 2, **kargs){
         | 
| 172 | 
            +
                        a + b + args.sum + c + d + kargs.values.sum
         | 
| 173 | 
            +
                      }
         | 
| 174 | 
            +
                    }
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                  it "can call add" do
         | 
| 178 | 
            +
                    expect(subject).to eq("7")
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
                end
         | 
| 181 | 
            +
              end
         | 
| 120 182 | 
             
            end
         | 
    
        data/spec/template_spec.rb
    CHANGED
    
    | @@ -1,7 +1,11 @@ | |
| 1 1 | 
             
            require "spec_helper"
         | 
| 2 | 
            +
            require "prime"
         | 
| 2 3 |  | 
| 3 4 | 
             
            RSpec.describe Template do
         | 
| 4 | 
            -
               | 
| 5 | 
            +
              let!(:ruby) { {} }
         | 
| 6 | 
            +
              let!(:input_context) { "" }
         | 
| 7 | 
            +
              let!(:timeout) { 0 }
         | 
| 8 | 
            +
              subject { Template.render(input, input_context, ruby: ruby, timeout: timeout) }
         | 
| 5 9 |  | 
| 6 10 | 
             
              [
         | 
| 7 11 | 
             
                ["Hello World", "", "Hello World"],
         | 
| @@ -12,13 +16,10 @@ RSpec.describe Template do | |
| 12 16 | 
             
                  '{ user: { first_name: "Dorian" } }',
         | 
| 13 17 | 
             
                  "Hello Dorian",
         | 
| 14 18 | 
             
                ],
         | 
| 15 | 
            -
                [
         | 
| 16 | 
            -
                  "{add(1, 2)",
         | 
| 17 | 
            -
                  'add = (a, b) => { a + b } { add: context(:add) }',
         | 
| 18 | 
            -
                  "3",
         | 
| 19 | 
            -
                ],
         | 
| 19 | 
            +
                ["{add(1, 2)", "add = (a, b) => { a + b } { add: context(:add) }", "3"],
         | 
| 20 20 | 
             
                ["Hello {", "", "Hello "],
         | 
| 21 21 | 
             
                ["{{a: 1}.each { |k, v| print(k) } nothing", "", "a"],
         | 
| 22 | 
            +
                ["{{a: 1}.each { |k, v| puts(k) } nothing", "", "a\n"],
         | 
| 22 23 | 
             
                ["", "", ""],
         | 
| 23 24 | 
             
              ].each do |(input, input_context, expected)|
         | 
| 24 25 | 
             
                context "#{input.inspect} #{input_context.inspect}" do
         | 
| @@ -30,4 +31,22 @@ RSpec.describe Template do | |
| 30 31 | 
             
                  end
         | 
| 31 32 | 
             
                end
         | 
| 32 33 | 
             
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              context "with a function from ruby" do
         | 
| 36 | 
            +
                let!(:ruby) { { prime: ->(n){ n.prime? } } }
         | 
| 37 | 
            +
                let!(:input) { "{prime(19201)" }
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                it "calls the ruby function" do
         | 
| 40 | 
            +
                  expect(subject).to eq("false")
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              context "with a function from ruby in an object" do
         | 
| 45 | 
            +
                let!(:ruby) { { Prime: { prime: ->(n){ n.prime? } } } }
         | 
| 46 | 
            +
                let!(:input) { "{Prime.prime(19201)" }
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                it "calls the ruby function" do
         | 
| 49 | 
            +
                  expect(subject).to eq("false")
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 33 52 | 
             
            end
         | 
    
        data/template-ruby.gemspec
    CHANGED