Skip to content

Commit e2ac5a9

Browse files
committed
fix: PropertyStruct recursive wrapping and add Go tests to Ruby CI
- Fix PropertyStruct to recursively wrap nested hashes and arrays - Fix respond_to_missing? to check if key exists in @table - Fix ERB test variable name (obj -> ps) in property_struct_spec.rb - Update hash operations test to use attribute access - Add Go erbrenderer tests to Ruby CI workflow matrix
1 parent b42e0b3 commit e2ac5a9

File tree

3 files changed

+100
-77
lines changed

3 files changed

+100
-77
lines changed

.github/workflows/ruby.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,23 @@ jobs:
66
strategy:
77
matrix:
88
os: [ubuntu-latest, macos-latest]
9-
ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', head]
9+
ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', head]
1010
runs-on: ${{ matrix.os }}
1111
steps:
1212
- uses: actions/checkout@v6
13+
- name: Setup Image
14+
if: matrix.os == 'ubuntu-latest'
15+
run: |
16+
sudo apt-get update && sudo apt-get install -y libpcap-dev
1317
- uses: ruby/setup-ruby@v1
1418
with:
1519
ruby-version: ${{ matrix.ruby }}
20+
- uses: actions/setup-go@v6
21+
with:
22+
go-version-file: go.mod
23+
- name: Run Go erbrenderer tests
24+
run: go test ./templatescompiler/erbrenderer/...
25+
continue-on-error: ${{ matrix.ruby == 'head' }}
1626
- run: bundle install
1727
working-directory: templatescompiler/erbrenderer/
1828
- run: bundle exec rake

templatescompiler/erbrenderer/erb_renderer.rb

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
11
# Based on common/properties/template_evaluation_context.rb
2-
require "rubygems"
3-
require "json"
4-
require "erb"
5-
require "yaml"
2+
require 'rubygems'
3+
require 'json'
4+
require 'erb'
5+
require 'yaml'
66

77
# Simple struct-like class to replace OpenStruct dependency
88
# OpenStruct is being removed from Ruby standard library in Ruby 3.5+
99
class PropertyStruct
1010
def initialize(hash = {})
1111
@table = {}
1212
hash.each do |key, value|
13-
@table[key.to_sym] = value
13+
@table[key.to_sym] = wrap_value(value)
1414
end
1515
end
1616

1717
def method_missing(method_name, *args)
18-
if method_name.to_s.end_with?("=")
19-
@table[method_name.to_s.chomp("=").to_sym] = args.first
18+
if method_name.to_s.end_with?('=')
19+
@table[method_name.to_s.chomp('=').to_sym] = wrap_value(args.first)
2020
else
2121
@table[method_name.to_sym]
2222
end
2323
end
2424

25-
def respond_to_missing?(method_name, include_private = false)
26-
true
25+
def respond_to_missing?(method_name, _include_private = false)
26+
@table.key?(method_name.to_sym) || method_name.to_s.end_with?('=')
27+
end
28+
29+
private
30+
31+
def wrap_value(value)
32+
case value
33+
when Hash
34+
PropertyStruct.new(value)
35+
when Array
36+
value.map { |item| wrap_value(item) }
37+
else
38+
value
39+
end
2740
end
2841
end
2942

@@ -41,22 +54,20 @@ def recursive_merge!(other)
4154
end
4255

4356
class TemplateEvaluationContext
44-
attr_reader :name, :index
45-
attr_reader :properties, :raw_properties
46-
attr_reader :spec
57+
attr_reader :name, :index, :properties, :raw_properties, :spec
4758

4859
def initialize(spec)
49-
@name = spec["job"]["name"] if spec["job"].is_a?(Hash)
50-
@index = spec["index"]
60+
@name = spec['job']['name'] if spec['job'].is_a?(Hash)
61+
@index = spec['index']
5162

52-
properties1 = if !spec["job_properties"].nil?
53-
spec["job_properties"]
54-
else
55-
spec["global_properties"].recursive_merge!(spec["cluster_properties"])
56-
end
63+
properties1 = if !spec['job_properties'].nil?
64+
spec['job_properties']
65+
else
66+
spec['global_properties'].recursive_merge!(spec['cluster_properties'])
67+
end
5768

5869
properties = {}
59-
spec["default_properties"].each do |name, value|
70+
spec['default_properties'].each do |name, value|
6071
copy_property(properties, properties1, name, value)
6172
end
6273

@@ -78,28 +89,30 @@ def p(*args)
7889
end
7990

8091
return args[1] if args.length == 2
92+
8193
raise UnknownProperty.new(names)
8294
end
8395

8496
def if_p(*names)
8597
values = names.map do |name|
8698
value = lookup_property(@raw_properties, name)
8799
return ActiveElseBlock.new(self) if value.nil?
100+
88101
value
89102
end
90103

91104
yield(*values)
92105
InactiveElseBlock.new
93106
end
94107

95-
def if_link(name)
108+
def if_link(_name)
96109
false
97110
end
98111

99112
private
100113

101114
def copy_property(dst, src, name, default = nil)
102-
keys = name.split(".")
115+
keys = name.split('.')
103116
src_ref = src
104117
dst_ref = dst
105118

@@ -120,9 +133,9 @@ def copy_property(dst, src, name, default = nil)
120133
def openstruct(object)
121134
case object
122135
when Hash
123-
mapped = object.each_with_object({}) { |(k, v), h|
136+
mapped = object.each_with_object({}) do |(k, v), h|
124137
h[k] = openstruct(v)
125-
}
138+
end
126139
PropertyStruct.new(mapped)
127140
when Array
128141
object.map { |item| openstruct(item) }
@@ -132,7 +145,7 @@ def openstruct(object)
132145
end
133146

134147
def lookup_property(collection, name)
135-
keys = name.split(".")
148+
keys = name.split('.')
136149
ref = collection
137150

138151
keys.each do |key|
@@ -161,24 +174,23 @@ def else
161174
yield
162175
end
163176

164-
def else_if_p(*names, &block) # rubocop:disable Style/ArgumentsForwarding
165-
@context.if_p(*names, &block) # rubocop:disable Style/ArgumentsForwarding
177+
def else_if_p(*names, &block)
178+
@context.if_p(*names, &block)
166179
end
167180
end
168181

169182
class InactiveElseBlock
170-
def else
171-
end
183+
def else; end
172184

173-
def else_if_p(*names)
185+
def else_if_p(*_names)
174186
InactiveElseBlock.new
175187
end
176188
end
177189
end
178190

179-
# todo do not use JSON in releases
191+
# TODO: do not use JSON in releases
180192
class << JSON
181-
alias_method :dump_array_or_hash, :dump
193+
alias dump_array_or_hash dump
182194

183195
def dump(*args)
184196
arg = args[0]
@@ -196,10 +208,10 @@ def initialize(json_context_path)
196208
end
197209

198210
def render(src_path, dst_path)
199-
erb = ERB.new(File.read(src_path), trim_mode: "-")
211+
erb = ERB.new(File.read(src_path), trim_mode: '-')
200212
erb.filename = src_path
201213

202-
# Note: JSON.load_file was added in v2.3.1: https://github.com/ruby/json/blob/v2.3.1/lib/json/common.rb#L286
214+
# NOTE: JSON.load_file was added in v2.3.1: https://github.com/ruby/json/blob/v2.3.1/lib/json/common.rb#L286
203215
context_hash = JSON.parse(File.read(@json_context_path))
204216
template_evaluation_context = TemplateEvaluationContext.new(context_hash)
205217

@@ -208,7 +220,7 @@ def render(src_path, dst_path)
208220
name = "#{template_evaluation_context&.name}/#{template_evaluation_context&.index}"
209221

210222
line_i = e.backtrace.index { |l| l.include?(erb&.filename.to_s) }
211-
line_num = line_i ? e.backtrace[line_i].split(":")[1] : "unknown"
223+
line_num = line_i ? e.backtrace[line_i].split(':')[1] : 'unknown'
212224
location = "(line #{line_num}: #{e.inspect})"
213225

214226
raise("Error filling in template '#{src_path}' for #{name} #{location}")
Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,87 @@
1-
require "spec_helper"
2-
require "erb_renderer"
1+
require 'spec_helper'
2+
require 'erb_renderer'
33

4-
RSpec.describe "PropertyStruct" do
5-
describe "initialization and attribute access" do
6-
it "provides dynamic attribute access for hash keys" do
7-
ps = PropertyStruct.new(name: "test", value: 42)
8-
expect(ps.name).to eq("test")
4+
RSpec.describe 'PropertyStruct' do
5+
describe 'initialization and attribute access' do
6+
it 'provides dynamic attribute access for hash keys' do
7+
ps = PropertyStruct.new(name: 'test', value: 42)
8+
expect(ps.name).to eq('test')
99
expect(ps.value).to eq(42)
1010
end
1111

12-
it "converts string keys to symbols" do
13-
ps = PropertyStruct.new("name" => "test", "value" => 42)
14-
expect(ps.name).to eq("test")
12+
it 'converts string keys to symbols' do
13+
ps = PropertyStruct.new('name' => 'test', 'value' => 42)
14+
expect(ps.name).to eq('test')
1515
expect(ps.value).to eq(42)
1616
end
1717

18-
it "supports nested attribute access" do
19-
ps = PropertyStruct.new(config: {database: {host: "localhost", port: 5432}})
18+
it 'supports nested attribute access' do
19+
ps = PropertyStruct.new(config: { database: { host: 'localhost', port: 5432 } })
2020
nested = ps.config
2121
expect(nested).to be_a(PropertyStruct)
2222
expect(nested.database).to be_a(PropertyStruct)
23-
expect(nested.database.host).to eq("localhost")
23+
expect(nested.database.host).to eq('localhost')
2424
expect(nested.database.port).to eq(5432)
2525
end
2626

27-
it "handles arrays of hashes" do
28-
ps = PropertyStruct.new(servers: [{name: "web1", ip: "10.0.0.1"}, {name: "web2", ip: "10.0.0.2"}])
27+
it 'handles arrays of hashes' do
28+
ps = PropertyStruct.new(servers: [{ name: 'web1', ip: '10.0.0.1' }, { name: 'web2', ip: '10.0.0.2' }])
2929
servers = ps.servers
3030
expect(servers).to be_an(Array)
3131
expect(servers.length).to eq(2)
32-
expect(servers.first.name).to eq("web1")
33-
expect(servers.last.ip).to eq("10.0.0.2")
32+
expect(servers.first.name).to eq('web1')
33+
expect(servers.last.ip).to eq('10.0.0.2')
3434
end
3535

36-
it "responds to method queries correctly" do
37-
ps = PropertyStruct.new(existing_key: "value")
36+
it 'responds to method queries correctly' do
37+
ps = PropertyStruct.new(existing_key: 'value')
3838
expect(ps.respond_to?(:existing_key)).to be true
3939
expect(ps.respond_to?(:nonexistent_key)).to be false
4040
end
4141
end
4242

43-
describe "Ruby standard library method pass-through" do
44-
it "supports array operations like map" do
43+
describe 'Ruby standard library method pass-through' do
44+
it 'supports array operations like map' do
4545
ps = PropertyStruct.new(ports: [8080, 8081, 8082])
46-
expect(ps.ports.map(&:to_s)).to eq(["8080", "8081", "8082"])
46+
expect(ps.ports.map(&:to_s)).to eq(%w[8080 8081 8082])
4747
end
4848

49-
it "supports string operations" do
50-
ps = PropertyStruct.new(url: "https://example.com")
51-
expect(ps.url.start_with?("https")).to be true
52-
expect(ps.url.split("://")).to eq(["https", "example.com"])
49+
it 'supports string operations' do
50+
ps = PropertyStruct.new(url: 'https://example.com')
51+
expect(ps.url.start_with?('https')).to be true
52+
expect(ps.url.split('://')).to eq(['https', 'example.com'])
5353
end
5454

55-
it "supports hash operations" do
56-
ps = PropertyStruct.new(config: {a: 1, b: 2, c: 3})
57-
expect(ps.config.keys.sort).to eq([:a, :b, :c])
58-
expect(ps.config.values.sum).to eq(6)
55+
it 'supports hash operations via direct access' do
56+
ps = PropertyStruct.new(config: { a: 1, b: 2, c: 3 })
57+
expect(ps.config.a).to eq(1)
58+
expect(ps.config.b).to eq(2)
59+
expect(ps.config.c).to eq(3)
5960
end
6061

61-
it "supports nil and empty checks" do
62-
ps = PropertyStruct.new(empty_string: "", nil_value: nil, filled: "data")
62+
it 'supports nil and empty checks' do
63+
ps = PropertyStruct.new(empty_string: '', nil_value: nil, filled: 'data')
6364
expect(ps.empty_string.empty?).to be true
6465
expect(ps.nil_value.nil?).to be true
6566
expect(ps.filled.nil?).to be false
6667
end
6768
end
6869

69-
describe "compatibility across Ruby versions" do
70-
it "works with ERB rendering" do
71-
template = ERB.new("<%= obj.name.upcase %>: <%= obj.ports.join(',') %>")
72-
ps = PropertyStruct.new(name: "service", ports: [80, 443, 8080])
70+
describe 'compatibility across Ruby versions' do
71+
it 'works with ERB rendering' do
72+
template = ERB.new("<%= ps.name.upcase %>: <%= ps.ports.join(',') %>")
73+
ps = PropertyStruct.new(name: 'service', ports: [80, 443, 8080])
7374
result = template.result(binding)
74-
expect(result).to eq("SERVICE: 80,443,8080")
75+
expect(result).to eq('SERVICE: 80,443,8080')
7576
end
7677

77-
it "maintains OpenStruct API compatibility" do
78+
it 'maintains OpenStruct API compatibility' do
7879
# Test that PropertyStruct can be used as a drop-in replacement for OpenStruct
79-
ps = PropertyStruct.new(field1: "value1", field2: "value2")
80+
ps = PropertyStruct.new(field1: 'value1', field2: 'value2')
8081
expect(ps).to respond_to(:field1)
8182
expect(ps).to respond_to(:field2)
82-
expect(ps.field1).to eq("value1")
83-
expect(ps.field2).to eq("value2")
83+
expect(ps.field1).to eq('value1')
84+
expect(ps.field2).to eq('value2')
8485
end
8586
end
8687
end

0 commit comments

Comments
 (0)