Skip to content

Commit 9b18cb9

Browse files
committed
hrw4u: Adds procedures (macros) and libraries
Extends the hrw4u grammar and compiler with a procedure system for defining reusable, parameterized blocks of rules. Procedures use namespaced names (e.g. `local::set-cache`) with `$param` substitution, and can be defined inline or loaded from external `.hrw4u` library files via `use` directives. A `--output=hrw4u` flatten mode expands all procedure calls into a self-contained source file. This commit also adds LSP hover/completion support for procedures, comprehensive test coverage, validation for circular imports and arity mismatches, and documentation in the admin guide.
1 parent 3f6f207 commit 9b18cb9

103 files changed

Lines changed: 1761 additions & 740 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

doc/admin-guide/configuration/hrw4u.en.rst

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,101 @@ or when integrating with existing header_rewrite rules that reference specific s
416416
addition, a remap configuration can use ``@PPARAM`` to set one of these slot variables explicitly
417417
as part of the configuration.
418418

419+
Procedures
420+
----------
421+
422+
Procedures allow you to define reusable blocks of rules that can be called from
423+
multiple sections or files. A procedure is a named, parameterized block of
424+
conditions and operators that expands inline at the call site.
425+
426+
Defining Procedures
427+
^^^^^^^^^^^^^^^^^^^
428+
429+
Procedures are declared with the ``procedure`` keyword and must use a qualified
430+
name with the ``::`` namespace separator::
431+
432+
procedure local::add-debug-header($tag) {
433+
inbound.req.X-Debug = "$tag";
434+
}
435+
436+
The namespace prefix (``local::`` in this example) groups related procedures.
437+
Parameters are prefixed with ``$`` and substituted at the call site.
438+
439+
Procedures may be defined in the same file as the sections that use them, or in
440+
separate ``.hrw4u`` files loaded with the ``use`` directive. Procedure declarations
441+
must appear before any section blocks.
442+
443+
Using Procedures
444+
^^^^^^^^^^^^^^^^
445+
446+
Call a procedure from any section by its qualified name::
447+
448+
procedure local::set-cache-headers($ttl) {
449+
outbound.resp.Cache-Control = "max-age=$ttl";
450+
outbound.resp.X-Cache-TTL = "$ttl";
451+
}
452+
453+
READ_RESPONSE {
454+
local::set-cache-headers("3600");
455+
}
456+
457+
SEND_RESPONSE {
458+
local::set-cache-headers("0");
459+
}
460+
461+
The procedure body is expanded inline — each section gets its own copy with
462+
the correct hook context.
463+
464+
Procedure Files and ``use``
465+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
466+
467+
For larger projects, procedures can be organized into separate files and loaded
468+
with the ``use`` directive. The ``use`` spec maps to a file path: ``use Acme::Common``
469+
loads ``Acme/Common.hrw4u`` from the procedures search path.
470+
471+
The ``--procedures-path`` flag specifies where to search::
472+
473+
hrw4u --procedures-path /etc/trafficserver/procedures rules.hrw4u
474+
475+
Given this file structure::
476+
477+
/etc/trafficserver/procedures/
478+
└── Acme/
479+
└── Common.hrw4u
480+
481+
Where ``Acme/Common.hrw4u`` contains::
482+
483+
procedure Acme::add-security-headers() {
484+
outbound.resp.X-Frame-Options = "DENY";
485+
outbound.resp.X-Content-Type-Options = "nosniff";
486+
}
487+
488+
Then in ``rules.hrw4u``::
489+
490+
use Acme::Common
491+
492+
READ_RESPONSE {
493+
Acme::add-security-headers();
494+
}
495+
496+
The ``use`` directive enforces namespace consistency: all procedures in a file
497+
loaded via ``use Acme::Common`` must use the ``Acme::`` namespace prefix.
498+
499+
Parameters and Defaults
500+
^^^^^^^^^^^^^^^^^^^^^^^
501+
502+
Procedures support positional parameters with optional defaults::
503+
504+
procedure local::tag-request($env, $version = "v1") {
505+
inbound.req.X-Env = "$env";
506+
inbound.req.X-Version = "$version";
507+
}
508+
509+
REMAP {
510+
local::tag-request("prod");
511+
# $version defaults to "v1"
512+
}
513+
419514
Groups
420515
------
421516

@@ -814,3 +909,4 @@ References
814909
==========
815910

816911
- :ref:`admin-plugins-header-rewrite`
912+
- :ref:`admin-vscode-hrw4u` — VSCode extension for HRW4U development

tools/hrw4u/Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ UTILS_FILES=src/symbols_base.py \
5151
SRC_FILES_HRW4U=src/visitor.py \
5252
src/symbols.py \
5353
src/suggestions.py \
54-
src/kg_visitor.py
54+
src/kg_visitor.py \
55+
src/procedures.py
5556

5657
ALL_HRW4U_FILES=$(SHARED_FILES) $(UTILS_FILES) $(SRC_FILES_HRW4U)
5758

tools/hrw4u/grammar/hrw4u.g4

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ TRUE : [tT][rR][uU][eE];
2929
FALSE : [fF][aA][lL][sS][eE];
3030
WITH : 'with';
3131
BREAK : 'break';
32+
USE : 'use';
33+
PROCEDURE : 'procedure';
3234

3335
REGEX : '/' ( '\\/' | ~[/\r\n] )* '/' ;
34-
STRING : '"' ( '\\' . | ~["\\\r\n] )* '"' ;
36+
STRING : '"' ( ESCAPED_BLOCK | '\\' . | ~["\\\r\n] )* '"' ;
37+
38+
// {{ ... }} is an escape hatch — contents are passed through verbatim, inner quotes allowed
39+
fragment ESCAPED_BLOCK : '{{' ( ~'}' | '}' ~'}' )* '}}';
3540
3641
IPV4_LITERAL
3742
: (OCTET '.' OCTET '.' OCTET '.' OCTET ('/' IPV4_CIDR)?)
@@ -59,8 +64,13 @@ fragment IPV6_CIDR : '3'[3-9]
5964
| '12'[0-8]
6065
;
6166
67+
// Qualified identifier: Namespace::Name (one or more :: segments).
68+
QUALIFIED_IDENT : [a-zA-Z_][a-zA-Z0-9_-]* ('::' [a-zA-Z_][a-zA-Z0-9_-]*)+
69+
;
70+
6271
IDENT : [a-zA-Z_][a-zA-Z0-9_@.-]* ;
6372
NUMBER : [0-9]+ ;
73+
DOLLAR : '$';
6474
LPAREN : '(';
6575
RPAREN : ')';
6676
LBRACE : '{';
@@ -89,14 +99,36 @@ WS : [ \t\r\n]+ -> skip ;
8999
// Parser Rules
90100
// -----------------------------
91101
program
92-
: programItem+ EOF
102+
: programItem* EOF
93103
;
94104
95105
programItem
96-
: section
106+
: useDirective
107+
| procedureDecl
108+
| section
97109
| commentLine
98110
;
99111
112+
useDirective
113+
: USE QUALIFIED_IDENT
114+
;
115+
116+
procedureDecl
117+
: PROCEDURE QUALIFIED_IDENT LPAREN paramList? RPAREN block
118+
;
119+
120+
paramList
121+
: param (COMMA param)*
122+
;
123+
124+
param
125+
: DOLLAR IDENT (EQUAL value)?
126+
;
127+
128+
paramRef
129+
: DOLLAR IDENT
130+
;
131+
100132
section
101133
: varSection
102134
| name=IDENT LBRACE sectionBody+ RBRACE
@@ -211,7 +243,7 @@ comparable
211243
;
212244

213245
functionCall
214-
: funcName=IDENT LPAREN argumentList? RPAREN
246+
: funcName=(IDENT | QUALIFIED_IDENT) LPAREN argumentList? RPAREN
215247
;
216248

217249
argumentList
@@ -251,6 +283,7 @@ value
251283
| ident=IDENT
252284
| ip
253285
| iprange
286+
| paramRef
254287
;
255288

256289
commentLine

tools/hrw4u/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ build-backend = "setuptools.build_meta"
2020

2121
[project]
2222
name = "hrw4u"
23-
version = "1.4.1"
23+
version = "1.5.0"
2424
description = "HRW4U CLI tool for Apache Traffic Server header rewrite rules"
2525
authors = [
2626
{name = "Leif Hedstrom", email = "leif@apache.org"}
@@ -76,6 +76,7 @@ markers = [
7676
"examples: marks tests for all header_rewrite docs examples",
7777
"reverse: marks tests for reverse conversion (header_rewrite -> hrw4u)",
7878
"ast: marks tests for AST validation",
79+
"procedures: marks tests for procedure expansion",
7980
]
8081

8182
[dependency-groups]

tools/hrw4u/scripts/hrw4u

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,38 @@
1919

2020
from __future__ import annotations
2121

22+
import argparse
23+
import os
24+
from pathlib import Path
25+
from typing import Any
26+
2227
from hrw4u.hrw4uLexer import hrw4uLexer
2328
from hrw4u.hrw4uParser import hrw4uParser
2429
from hrw4u.visitor import HRW4UVisitor
2530
from hrw4u.common import run_main
2631

2732

33+
def _add_args(parser: argparse.ArgumentParser, output_group: argparse._MutuallyExclusiveGroup) -> None:
34+
output_group.add_argument(
35+
"--output",
36+
choices=["hrw", "hrw4u"],
37+
default="hrw",
38+
help="Output format: hrw (header_rewrite, default) or hrw4u (expand procedures inline)")
39+
parser.add_argument(
40+
"--procedures-path",
41+
metavar="DIR[:DIR...]",
42+
dest="procedures_path",
43+
default="",
44+
help="Colon-separated list of directories to search for procedure files")
45+
46+
47+
def _visitor_kwargs(args: argparse.Namespace) -> dict[str, Any]:
48+
kwargs: dict[str, Any] = {}
49+
if args.procedures_path:
50+
kwargs['proc_search_paths'] = [Path(p) for p in args.procedures_path.split(os.pathsep) if p]
51+
return kwargs
52+
53+
2854
def main() -> None:
2955
"""Main entry point for the hrw4u script."""
3056
run_main(
@@ -34,7 +60,9 @@ def main() -> None:
3460
visitor_class=HRW4UVisitor,
3561
error_prefix="hrw4u",
3662
output_flag_name="hrw",
37-
output_flag_help="Produce the HRW output (default)")
63+
output_flag_help="Produce the HRW output (default)",
64+
add_args=_add_args,
65+
visitor_kwargs=_visitor_kwargs)
3866

3967

4068
if __name__ == "__main__":

0 commit comments

Comments
 (0)