code-ruby 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
- if %w[== === !=].detect { |o| operator == o }
9
- comparaison(operator.to_sym, arguments)
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
- compare(arguments)
19
+ sig(arguments, ::Code::Object)
20
+ compare(arguments.first.value)
12
21
  elsif operator == "&&"
13
- and_operator(arguments)
22
+ sig(arguments, ::Code::Object)
23
+ and_operator(arguments.first.value)
14
24
  elsif operator == "||"
15
- or_operator(arguments)
25
+ sig(arguments, ::Code::Object)
26
+ or_operator(arguments.first.value)
16
27
  elsif operator == "to_string"
17
- to_string(arguments)
28
+ sig(arguments)
29
+ to_string
18
30
  else
19
- raise ::Code::Error::Undefined.new(
20
- "#{operator} not defined on #{inspect}",
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 ::Code::Error::ArgumentError.new(
82
- "Expected #{expected_arguments.size} arguments, " \
83
- "got #{actual_arguments.size} arguments",
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 ::Code::Error::TypeError.new(
95
- "Expected #{expected_argument}, got #{actual_argument.class}",
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 ::Code::Error::TypeError.new(
101
- "Expected #{expected_argument}, got #{actual_argument.class}",
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 comparaison(operator, arguments)
109
- sig(arguments, ::Code::Object)
110
- other = arguments.first.value
111
- ::Code::Object::Boolean.new(public_send(operator, other))
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(arguments)
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(arguments)
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(arguments)
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(arguments)
133
- sig(arguments)
135
+ def to_string
134
136
  ::Code::Object::String.new(to_s)
135
137
  end
136
138
  end
@@ -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
- (whitespace? >> equal >> whitespace? >> code_present.as(:default)).maybe
33
+ (
34
+ whitespace? >> equal >> whitespace? >> code_present.as(:default)
35
+ ).maybe
34
36
  end
35
37
 
36
38
  rule(:argument) do
@@ -16,7 +16,7 @@ class Code
16
16
  rule(:if_rule) do
17
17
  (
18
18
  (if_keyword | unless_keyword).as(:if_operator) >> whitespace >>
19
- if_modifier.as(:if_statement) >> code.as(:if_body).maybe >>
19
+ if_modifier.as(:if_statement) >> code.as(:if_body).maybe >>
20
20
  (
21
21
  else_keyword >>
22
22
  (
@@ -49,29 +49,29 @@ class Code
49
49
  end
50
50
 
51
51
  rule(:single_quoted_character) do
52
- escaped_character | (opening_curly_bracket.absent? >> single_quote.absent? >> any)
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 | (opening_curly_bracket.absent? >> double_quote.absent? >> any)
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
- single_quoted_character.repeat(1).as(:characters)
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
- double_quoted_character.repeat(1).as(:characters)
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 = Timeout.timeout(timeout) { ::Code::Parser::Code.new.parse(@input) }
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(context, timeout: timeout)
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
@@ -1,3 +1,3 @@
1
1
  require_relative "../template"
2
2
 
3
- Template::Version = Gem::Version.new("0.3.1")
3
+ Template::Version = Gem::Version.new("0.4.0")
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(input, context = "", io: StringIO.new, timeout: DEFAULT_TIMEOUT)
15
- new(input, io: io, timeout: timeout).render(context)
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(context, timeout: timeout)
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
- ::Template::Node::Template.new(parsed).evaluate(
27
- context: context,
28
- io: io,
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"
@@ -16,7 +16,7 @@ RSpec.describe Code::Parser::String do
16
16
  "'\\u0123\\u4567\\u89aA\\ubBcC\\UdDeE\\ufFfF'",
17
17
  ":asc",
18
18
  "'1 + 1 = {1 + 1}'",
19
- "'a + b = {'{'a'}{'b'}'}'"
19
+ "'a + b = {'{'a'}{'b'}'}'",
20
20
  ].each do |input|
21
21
  context input.inspect do
22
22
  let(:input) { input }
data/spec/code_spec.rb CHANGED
@@ -1,7 +1,15 @@
1
1
  require "spec_helper"
2
2
 
3
3
  RSpec.describe Code do
4
- subject { described_class.evaluate(input).to_s }
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
@@ -1,7 +1,11 @@
1
1
  require "spec_helper"
2
+ require "prime"
2
3
 
3
4
  RSpec.describe Template do
4
- subject { described_class.render(input, input_context) }
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
@@ -18,7 +18,4 @@ Gem::Specification.new do |s|
18
18
  s.add_dependency "activesupport", "~> 7"
19
19
  s.add_dependency "parslet", "~> 2"
20
20
  s.add_dependency "zeitwerk", "~> 2.6"
21
-
22
- s.add_development_dependency "prettier", "~> 3"
23
- s.add_development_dependency "rspec", "~> 3"
24
21
  end