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.
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