Skip to content

Commit b04c7c5

Browse files
committed
changes from prod
1 parent 70ceb76 commit b04c7c5

8 files changed

Lines changed: 320 additions & 11 deletions

File tree

.pre-commit-config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
repos:
2+
- repo: local
3+
hooks:
4+
- id: sort-yaml-rules
5+
name: Sort and format rule YAML files
6+
language: python
7+
entry: python scripts/sort_yaml.py --check
8+
types: [yaml]
9+
files: ".*/rule\\.yml$"
10+
additional_dependencies: [pyyaml]
11+
pass_filenames: true

.vscode/extensions.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"recommendations": [
33
"redhat.vscode-yaml",
44
"GrapeCity.gc-excelviewer",
5-
"mechatroner.rainbow-csv"
5+
"mechatroner.rainbow-csv",
6+
"emeraldwalk.runonsave"
67
]
78
}

.vscode/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
{
22
"yaml.schemas": {
33
"https://raw.githubusercontent.com/cdisc-org/cdisc-rules-engine/refs/heads/main/resources/schema/rule-merged/CORE-base.json": "rule.yml"
4+
},
5+
"emeraldwalk.runonsave": {
6+
"commands": [
7+
{
8+
"match": "rule\\.yml$",
9+
"isAsync": false,
10+
"cmd": "${workspaceFolder}/venv/bin/python ${workspaceFolder}/scripts/sort_yaml.py ${file}"
11+
}
12+
]
413
}
514
}

.vscode/tasks.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "Sort & Format rule.yml",
6+
"type": "shell",
7+
"command": "${workspaceFolder}/venv/bin/python",
8+
"args": ["${workspaceFolder}/scripts/sort_yaml.py", "${file}"],
9+
"presentation": {
10+
"reveal": "silent",
11+
"panel": "shared"
12+
},
13+
"problemMatcher": []
14+
},
15+
{
16+
"label": "Sort & Format ALL rule.yml files",
17+
"type": "shell",
18+
"command": "${workspaceFolder}/venv/bin/python",
19+
"args": ["${workspaceFolder}/scripts/sort_yaml.py"],
20+
"presentation": {
21+
"reveal": "always",
22+
"panel": "shared"
23+
},
24+
"problemMatcher": []
25+
}
26+
]
27+
}
28+

scripts/sort_yaml.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Sort and format rule YAML files alphabetically and recursively by key name.
4+
5+
This matches the auto-format/auto-sort behavior of the CDISC conformance rules editor.
6+
7+
Usage:
8+
# Format files in-place (default: all rule.yml under Published/ and Unpublished/)
9+
python scripts/sort_yaml.py
10+
11+
# Format specific files
12+
python scripts/sort_yaml.py path/to/rule.yml another/rule.yml
13+
14+
# Check mode: exit with code 1 if any file is not formatted correctly
15+
python scripts/sort_yaml.py --check [files...]
16+
"""
17+
18+
import sys
19+
import argparse
20+
from pathlib import Path
21+
22+
try:
23+
import yaml
24+
except ImportError:
25+
print("ERROR: pyyaml is not installed. Run: pip install pyyaml", file=sys.stderr)
26+
sys.exit(1)
27+
28+
29+
# ---------------------------------------------------------------------------
30+
# Custom YAML Dumper
31+
# ---------------------------------------------------------------------------
32+
33+
class _SortedDumper(yaml.Dumper):
34+
"""YAML Dumper that produces consistent, human-readable output."""
35+
pass
36+
37+
38+
def _str_representer(dumper: yaml.Dumper, data: str):
39+
"""Represent strings: use literal block style for multi-line, plain otherwise.
40+
Strings that look like YAML scalars (booleans, numbers) are quoted.
41+
"""
42+
if "\n" in data:
43+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
44+
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
45+
46+
47+
_SortedDumper.add_representer(str, _str_representer)
48+
49+
50+
# ---------------------------------------------------------------------------
51+
# Core logic
52+
# ---------------------------------------------------------------------------
53+
54+
def sort_recursive(obj):
55+
"""Recursively sort dict keys alphabetically. Lists are preserved as-is."""
56+
if isinstance(obj, dict):
57+
return {k: sort_recursive(obj[k]) for k in sorted(obj.keys(), key=str)}
58+
if isinstance(obj, list):
59+
return [sort_recursive(item) for item in obj]
60+
return obj
61+
62+
63+
def canonical(content: str) -> str:
64+
"""Return the canonical (sorted + formatted) representation of a YAML string."""
65+
data = yaml.safe_load(content)
66+
if data is None:
67+
return content
68+
sorted_data = sort_recursive(data)
69+
return yaml.dump(
70+
sorted_data,
71+
Dumper=_SortedDumper,
72+
default_flow_style=False,
73+
allow_unicode=True,
74+
indent=2,
75+
sort_keys=False, # we already sorted manually
76+
width=100,
77+
)
78+
79+
80+
def find_rule_files(root: Path) -> list[Path]:
81+
"""Find all rule.yml files under Published/ and Unpublished/."""
82+
files = []
83+
for folder in ("Published", "Unpublished"):
84+
folder_path = root / folder
85+
if folder_path.exists():
86+
files.extend(folder_path.rglob("rule.yml"))
87+
return sorted(files)
88+
89+
90+
def process_files(files: list[Path], check_mode: bool) -> int:
91+
"""Format (or check) the given files. Returns exit code."""
92+
changed = []
93+
errors = []
94+
95+
for path in files:
96+
try:
97+
original = path.read_text(encoding="utf-8")
98+
formatted = canonical(original)
99+
except Exception as exc:
100+
errors.append(f" {path}: {exc}")
101+
continue
102+
103+
if original != formatted:
104+
changed.append(path)
105+
if not check_mode:
106+
path.write_text(formatted, encoding="utf-8")
107+
print(f" Formatted: {path}")
108+
109+
if errors:
110+
print("\nERROR: Failed to process the following files:", file=sys.stderr)
111+
for e in errors:
112+
print(e, file=sys.stderr)
113+
return 1
114+
115+
if check_mode:
116+
if changed:
117+
print(
118+
"\nThe following rule.yml files are not correctly sorted/formatted:\n",
119+
file=sys.stderr,
120+
)
121+
for p in changed:
122+
print(f" {p}", file=sys.stderr)
123+
print(
124+
"\nRun `python scripts/sort_yaml.py` to fix them automatically.",
125+
file=sys.stderr,
126+
)
127+
return 1
128+
else:
129+
print("All rule.yml files are correctly sorted and formatted.")
130+
else:
131+
if not changed:
132+
print("All rule.yml files are already correctly sorted and formatted.")
133+
134+
return 0
135+
136+
137+
# ---------------------------------------------------------------------------
138+
# Entry point
139+
# ---------------------------------------------------------------------------
140+
141+
def main():
142+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
143+
parser.add_argument(
144+
"--check",
145+
action="store_true",
146+
help="Check mode: exit 1 if any file needs formatting, without modifying files.",
147+
)
148+
parser.add_argument(
149+
"files",
150+
nargs="*",
151+
type=Path,
152+
help="rule.yml files to process. Defaults to all rule.yml files under Published/ and Unpublished/.",
153+
)
154+
args = parser.parse_args()
155+
156+
repo_root = Path(__file__).resolve().parent.parent
157+
158+
if args.files:
159+
files = [p.resolve() for p in args.files]
160+
else:
161+
files = find_rule_files(repo_root)
162+
163+
if not files:
164+
print("No rule.yml files found.")
165+
return 0
166+
167+
mode = "Checking" if args.check else "Formatting"
168+
print(f"{mode} {len(files)} rule.yml file(s)...")
169+
170+
sys.exit(process_files(files, check_mode=args.check))
171+
172+
173+
if __name__ == "__main__":
174+
main()
175+
176+

setup/bash_setup.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,15 @@ fi
145145

146146
VENV_PYTHON=$(which python)
147147

148+
echo "Installing pre-commit..."
149+
pip install pre-commit --index-url https://pypi.org/simple/ --quiet 2>/dev/null || \
150+
pip install pre-commit --quiet 2>/dev/null || true
151+
if command -v pre-commit >/dev/null 2>&1; then
152+
pre-commit install
153+
echo "Pre-commit hook installed."
154+
else
155+
echo "Warning: pre-commit not found on PATH after install; skipping hook setup."
156+
echo "You can install it manually with: pip install pre-commit && pre-commit install"
157+
fi
158+
148159
echo "Setup completed successfully!"

setup/windows_setup.bat

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ if !errorlevel! neq 0 (
177177
exit /b 1
178178
)
179179

180+
echo.
181+
echo Installing pre-commit...
182+
python -m pip install pre-commit --index-url https://pypi.org/simple/ --quiet 2>nul || python -m pip install pre-commit --quiet 2>nul
183+
where pre-commit >nul 2>&1
184+
if !errorlevel! equ 0 (
185+
pre-commit install
186+
echo Pre-commit hook installed.
187+
) else (
188+
echo Warning: pre-commit not found on PATH; skipping hook setup.
189+
echo You can install it manually with: pip install pre-commit ^&^& pre-commit install
190+
)
191+
180192
echo.
181193
echo Setup completed successfully!
182194
pause

0 commit comments

Comments
 (0)