Skip to content

Commit bef8761

Browse files
committed
Add parsing of simple conditionals
At the moment this only supports direct boolean dictionary lookups.
1 parent 3b387b5 commit bef8761

File tree

4 files changed

+193
-9
lines changed

4 files changed

+193
-9
lines changed

include/ur_client_library/control/script_reader.h

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,33 @@ class ScriptReader
5050
RobotType robot_type;
5151
};
5252

53-
using DataDict = std::unordered_map<std::string, std::variant<std::string, double, int>>;
53+
using DataDict = std::unordered_map<std::string, std::variant<std::string, double, int, bool>>;
5454

5555
ScriptReader() = default;
5656

5757
std::string readScriptFile(const std::string& filename, const DataDict& data = DataDict());
5858

5959
private:
60+
enum BlockType
61+
{
62+
IF,
63+
ELIF,
64+
ELSE
65+
};
66+
struct BlockState
67+
{
68+
BlockType type;
69+
bool condition_matched; // Has any previous condition in this block matched?
70+
bool should_render; // Should this block render?
71+
bool parent_render; // Is the parent block rendering?
72+
};
73+
6074
std::filesystem::path script_path_;
6175

6276
std::string readFileContent(const std::string& filename);
6377
void replaceIncludes(std::string& script_code);
6478
void replaceVariables(std::string& script_code, const DataDict& data);
79+
void replaceConditionals(std::string& script_code, const DataDict& data);
6580
};
6681
} // namespace control
67-
} // namespace urcl
82+
} // namespace urcl

include/ur_client_library/exceptions.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,17 @@ class UnsupportedMotionType : public UrException
213213
{
214214
}
215215
};
216+
217+
class UnknownVariable : public UrException
218+
{
219+
private:
220+
std::string text_;
221+
222+
public:
223+
explicit UnknownVariable() = delete;
224+
explicit UnknownVariable(const std::string& variable_name) : std::runtime_error("Unknown variable: " + variable_name)
225+
{
226+
}
227+
};
216228
} // namespace urcl
217-
#endif // ifndef UR_CLIENT_LIBRARY_EXCEPTIONS_H_INCLUDED
229+
#endif // ifndef UR_CLIENT_LIBRARY_EXCEPTIONS_H_INCLUDED

src/control/script_reader.cpp

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ std::string ScriptReader::readScriptFile(const std::string& filename, const Data
4646

4747
replaceIncludes(script_code);
4848
replaceVariables(script_code, data);
49+
replaceConditionals(script_code, data);
4950

5051
return script_code;
5152
}
@@ -93,13 +94,12 @@ void ScriptReader::replaceVariables(std::string& script_code, const DataDict& da
9394
while (std::regex_search(script_code, match, pattern))
9495
{
9596
std::string key = match[1];
96-
URCL_LOG_INFO("Found replacement pattern %s", match[0].str().c_str());
9797
if (data.find(key) == data.end())
9898
{
9999
std::stringstream ss;
100100
ss << "Variable '" << key << "' not found in data.";
101101
URCL_LOG_ERROR(ss.str().c_str());
102-
throw UrException(ss.str().c_str());
102+
throw UnknownVariable(ss.str().c_str());
103103
}
104104
std::string replaced_value;
105105

@@ -115,8 +115,13 @@ void ScriptReader::replaceVariables(std::string& script_code, const DataDict& da
115115
{
116116
replaced_value = std::to_string(std::get<int>(data.at(key)));
117117
}
118+
else if (std::holds_alternative<bool>(data.at(key)))
119+
{
120+
std::get<bool>(data.at(key)) ? replaced_value = "True" : replaced_value = "False";
121+
}
118122
else
119123
{
124+
// This is more of a reminder if we add types to the variant and forget to add it here.
120125
std::stringstream ss;
121126
ss << "Unsupported type for variable '" << key << "'.";
122127
URCL_LOG_ERROR(ss.str().c_str());
@@ -126,5 +131,79 @@ void ScriptReader::replaceVariables(std::string& script_code, const DataDict& da
126131
}
127132
}
128133

134+
void ScriptReader::replaceConditionals(std::string& script_code, const DataDict& data)
135+
{
136+
std::istringstream stream(script_code);
137+
std::ostringstream output;
138+
std::string line;
139+
std::stack<BlockState> block_stack;
140+
141+
std::regex if_pattern(R"(\{\%\s*if\s+([^\s].*[^\s])\s+\%\})");
142+
std::regex elif_pattern(R"(\{\%\s*elif\s+([^\s].*[^\s])\s+\%\})");
143+
std::regex else_pattern(R"(\{\%\s*else\s*\%\})");
144+
std::regex endif_pattern(R"(\{\%\s*endif\s*\%\})");
145+
std::smatch match;
146+
147+
while (std::getline(stream, line))
148+
{
149+
if (std::regex_search(line, match, if_pattern))
150+
{
151+
std::string condition = match[1];
152+
bool result = data.count(condition) && std::holds_alternative<bool>(data.at(condition)) &&
153+
std::get<bool>(data.at(condition));
154+
bool parent_render = block_stack.empty() ? true : block_stack.top().should_render;
155+
block_stack.push({ IF, result, parent_render && result, parent_render });
156+
}
157+
else if (std::regex_search(line, match, elif_pattern))
158+
{
159+
if (!block_stack.empty())
160+
{
161+
BlockState& top = block_stack.top();
162+
if (top.type == ELSE)
163+
continue;
164+
std::string condition = match[1];
165+
bool result = data.count(condition) && std::holds_alternative<bool>(data.at(condition)) &&
166+
std::get<bool>(data.at(condition));
167+
top.type = ELIF;
168+
if (!top.condition_matched && result)
169+
{
170+
top.condition_matched = true;
171+
top.should_render = top.parent_render;
172+
}
173+
else
174+
{
175+
top.should_render = false;
176+
}
177+
}
178+
}
179+
else if (std::regex_search(line, match, else_pattern))
180+
{
181+
if (!block_stack.empty())
182+
{
183+
BlockState& top = block_stack.top();
184+
top.type = ELSE;
185+
top.should_render = top.parent_render && !top.condition_matched;
186+
}
187+
}
188+
else if (std::regex_search(line, match, endif_pattern))
189+
{
190+
if (!block_stack.empty())
191+
{
192+
block_stack.pop();
193+
}
194+
}
195+
else
196+
{
197+
bool render = block_stack.empty() ? true : block_stack.top().should_render;
198+
if (render)
199+
{
200+
output << line << "\n";
201+
}
202+
}
203+
}
204+
205+
script_code = output.str();
206+
}
207+
129208
} // namespace control
130-
} // namespace urcl
209+
} // namespace urcl

tests/test_script_reader.cpp

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
// POSSIBILITY OF SUCH DAMAGE.
2929
// -- END LICENSE BLOCK ------------------------------------------------
3030

31+
#include "ur_client_library/exceptions.h"
32+
3133
#include <gtest/gtest.h>
3234
#include "ur_client_library/control/script_reader.h"
3335

@@ -82,7 +84,7 @@ TEST_F(ScriptReaderTest, ReadValidScript)
8284
{
8385
ScriptReader reader;
8486
std::string content = reader.readScriptFile(valid_script_path_);
85-
EXPECT_EQ(content, simple_script_.str());
87+
EXPECT_EQ(content, simple_script_.str() + "\n");
8688
}
8789

8890
TEST_F(ScriptReaderTest, ReadEmptyScript)
@@ -127,7 +129,7 @@ TEST_F(ScriptReaderTest, ReplaceIncludes)
127129
ofs_included.close();
128130

129131
std::string processed_script = reader.readScriptFile(existing_script_file);
130-
EXPECT_EQ(processed_script, "movej([1,2,3,4,5,6])");
132+
EXPECT_EQ(processed_script, "movej([1,2,3,4,5,6])\n");
131133

132134
std::remove(existing_script_file);
133135
std::remove(existing_include_file);
@@ -140,6 +142,8 @@ TEST_F(ScriptReaderTest, ReplaceVariables)
140142
data["VAR1"] = "value1";
141143
data["VAR2"] = 42;
142144
data["VAR3"] = 6.28;
145+
data["VAR4"] = true;
146+
data["VAR5"] = false;
143147

144148
char existing_script_file[] = "main_script.XXXXXX";
145149
std::ignore = mkstemp(existing_script_file);
@@ -150,11 +154,85 @@ TEST_F(ScriptReaderTest, ReplaceVariables)
150154
GTEST_FAIL();
151155
}
152156
ofs << "movej([{{VAR1}}, {{VAR2}}, {{VAR3}}, 0, 0, 0])" << std::endl;
157+
ofs << "local is_true = {{VAR4}}" << std::endl;
158+
ofs << "local is_false = {{VAR5}}" << std::endl;
153159
ofs << "This is just a line without any replacement" << std::endl;
154160
ofs.close();
155161

156162
std::string script = reader.readScriptFile(existing_script_file, data);
157163

158164
// By default std::to_string will convert double to 6 decimal places
159-
EXPECT_EQ(script, "movej([value1, 42, 6.280000, 0, 0, 0])\nThis is just a line without any replacement\n");
165+
EXPECT_EQ(script, "movej([value1, 42, 6.280000, 0, 0, 0])\nlocal is_true = True\nlocal is_false = False\nThis is "
166+
"just a line without any replacement\n");
167+
}
168+
169+
TEST_F(ScriptReaderTest, VariableNotInDictThrowsError)
170+
{
171+
ScriptReader reader;
172+
ScriptReader::DataDict data;
173+
data["VAR1"] = "value1";
174+
175+
char existing_script_file[] = "main_script.XXXXXX";
176+
std::ignore = mkstemp(existing_script_file);
177+
std::ofstream ofs(existing_script_file);
178+
if (ofs.bad())
179+
{
180+
std::cout << "Failed to create temporary files" << std::endl;
181+
GTEST_FAIL();
182+
}
183+
ofs << "movej([{{VAR1}}, {{VAR2}}, {{VAR3}}, 0, 0, 0])" << std::endl;
184+
ofs << "This is just a line without any replacement" << std::endl;
185+
ofs.close();
186+
187+
EXPECT_THROW(reader.readScriptFile(existing_script_file, data), urcl::UnknownVariable);
160188
}
189+
190+
TEST_F(ScriptReaderTest, ReplaceConditionals)
191+
{
192+
ScriptReader reader;
193+
ScriptReader::DataDict data;
194+
data["is_logged_in"] = true;
195+
data["is_guest"] = false;
196+
data["has_username"] = true;
197+
data["username"] = "test_user";
198+
199+
char existing_script_file[] = "main_script.XXXXXX";
200+
std::ignore = mkstemp(existing_script_file);
201+
std::ofstream ofs(existing_script_file);
202+
if (ofs.bad())
203+
{
204+
std::cout << "Failed to create temporary files" << std::endl;
205+
GTEST_FAIL();
206+
}
207+
ofs <<
208+
R"({% if is_logged_in %}
209+
Welcome back, {{ username }}!
210+
{% elif is_guest %}
211+
{% if has_username %}
212+
Welcome, {{ username }}!
213+
{%else %}
214+
Welcome, guest!
215+
{% endif %}
216+
{% else %}
217+
Please log in.
218+
{% endif %}
219+
)";
220+
ofs.close();
221+
222+
std::string script = reader.readScriptFile(existing_script_file, data);
223+
224+
EXPECT_EQ(script, "Welcome back, test_user!\n");
225+
226+
data["is_logged_in"] = false;
227+
data["is_guest"] = true;
228+
script = reader.readScriptFile(existing_script_file, data);
229+
EXPECT_EQ(script, "Welcome, test_user!\n");
230+
231+
data["has_username"] = false;
232+
script = reader.readScriptFile(existing_script_file, data);
233+
EXPECT_EQ(script, "Welcome, guest!\n");
234+
235+
data["is_guest"] = false;
236+
script = reader.readScriptFile(existing_script_file, data);
237+
EXPECT_EQ(script, "Please log in.\n");
238+
}

0 commit comments

Comments
 (0)