Skip to content

Commit e22d829

Browse files
committed
Merge branch 'main' into preview-rc-rdi
2 parents 5fc5e4f + 1b376f2 commit e22d829

File tree

187 files changed

+12351
-1179
lines changed

Some content is hidden

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

187 files changed

+12351
-1179
lines changed

assets/css/index.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,25 @@ section.prose {
101101
@apply mt-16 scroll-mt-6;
102102
}
103103

104+
/* Reduce top margin when headers immediately follow other headers */
105+
.prose h1[id] + h2[id],
106+
.prose h1[id] + h3[id],
107+
.prose h1[id] + h4[id],
108+
.prose h1[id] + h5[id],
109+
.prose h1[id] + h6[id],
110+
.prose h2[id] + h3[id],
111+
.prose h2[id] + h4[id],
112+
.prose h2[id] + h5[id],
113+
.prose h2[id] + h6[id],
114+
.prose h3[id] + h4[id],
115+
.prose h3[id] + h5[id],
116+
.prose h3[id] + h6[id],
117+
.prose h4[id] + h5[id],
118+
.prose h4[id] + h6[id],
119+
.prose h5[id] + h6[id] {
120+
@apply mt-6;
121+
}
122+
104123
.prose p:empty,
105124
.prose li:empty {
106125
@apply hidden;
@@ -579,6 +598,11 @@ html {
579598
scrollbar-gutter: stable;
580599
}
581600

601+
/* Auto-clickable for standalone images */
602+
img:not(a img):not(.image-card-img):not([src*="#no-click"]) {
603+
cursor: pointer;
604+
}
605+
582606
/* Chroma syntax highlighting */
583607

584608
/* Background */

build/add_cmds.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import json
4+
import logging
5+
import os
6+
import sys
7+
8+
from components.syntax import Command
9+
from components.markdown import Markdown
10+
11+
12+
def command_filename(name: str) -> str:
13+
"""Convert command name to filename format."""
14+
return name.lower().replace(' ', '-')
15+
16+
17+
def parse_args() -> argparse.Namespace:
18+
parser = argparse.ArgumentParser(description='Creates new Redis command pages from JSON input')
19+
parser.add_argument('json_file', type=str,
20+
help='Path to JSON file containing command definitions')
21+
parser.add_argument('--loglevel', type=str,
22+
default='INFO',
23+
help='Python logging level (overwrites LOGLEVEL env var)')
24+
return parser.parse_args()
25+
26+
27+
def validate_json_structure(data: dict, filename: str) -> None:
28+
"""Validate that the JSON has the expected structure for Redis commands."""
29+
if not isinstance(data, dict):
30+
raise ValueError(f"JSON file {filename} must contain a dictionary at root level")
31+
32+
for command_name, command_data in data.items():
33+
if not isinstance(command_data, dict):
34+
raise ValueError(f"Command '{command_name}' must be a dictionary")
35+
36+
# Check for required fields
37+
required_fields = ['summary', 'since', 'group']
38+
for field in required_fields:
39+
if field not in command_data:
40+
logging.warning(f"Command '{command_name}' missing recommended field: {field}")
41+
42+
# Validate arguments structure if present
43+
if 'arguments' in command_data:
44+
if not isinstance(command_data['arguments'], list):
45+
raise ValueError(f"Command '{command_name}' arguments must be a list")
46+
47+
48+
def load_and_validate_json(filepath: str) -> dict:
49+
"""Load and validate the JSON file containing command definitions."""
50+
if not os.path.exists(filepath):
51+
raise FileNotFoundError(f"JSON file not found: {filepath}")
52+
53+
try:
54+
with open(filepath, 'r') as f:
55+
data = json.load(f)
56+
except json.JSONDecodeError as e:
57+
raise ValueError(f"Invalid JSON in file {filepath}: {e}")
58+
59+
validate_json_structure(data, filepath)
60+
return data
61+
62+
63+
def add_standard_categories(fm_data: dict) -> None:
64+
"""Add the standard categories from create.sh script."""
65+
standard_categories = [
66+
'docs', 'develop', 'stack', 'oss', 'rs', 'rc', 'oss', 'kubernetes', 'clients'
67+
]
68+
fm_data['categories'] = standard_categories
69+
70+
71+
def get_full_command_name(command_name: str, command_data: dict) -> str:
72+
"""Get the full command name, handling container commands."""
73+
container = command_data.get('container')
74+
if container:
75+
return f"{container} {command_name}"
76+
return command_name
77+
78+
79+
def generate_command_frontmatter(command_name: str, command_data: dict, all_commands: dict) -> dict:
80+
"""Generate complete Hugo frontmatter for a command using existing build infrastructure."""
81+
# Get the full command name (handles container commands)
82+
full_command_name = get_full_command_name(command_name, command_data)
83+
84+
# Create Command object to generate syntax using the full command name
85+
c = Command(full_command_name, command_data)
86+
87+
# Start with the command data
88+
fm_data = command_data.copy()
89+
90+
# Add required Hugo frontmatter fields
91+
fm_data.update({
92+
'title': full_command_name,
93+
'linkTitle': full_command_name,
94+
'description': command_data.get('summary'),
95+
'syntax_str': str(c),
96+
'syntax_fmt': c.syntax(),
97+
'hidden': False # Default to not hidden
98+
})
99+
100+
# Add the standard categories from create.sh
101+
add_standard_categories(fm_data)
102+
103+
return fm_data
104+
105+
106+
def generate_argument_sections(command_data: dict) -> str:
107+
"""Generate placeholder sections for Required arguments and Optional arguments."""
108+
content = ""
109+
110+
arguments = command_data.get('arguments', [])
111+
if not arguments:
112+
return content
113+
114+
required_args = []
115+
optional_args = []
116+
117+
# Categorize arguments
118+
for arg in arguments:
119+
if arg.get('optional', False):
120+
optional_args.append(arg)
121+
else:
122+
required_args.append(arg)
123+
124+
# Generate Required arguments section
125+
if required_args:
126+
content += "## Required arguments\n\n"
127+
for arg in required_args:
128+
arg_name = arg.get('name', 'unknown')
129+
arg_type = arg.get('type', 'unknown')
130+
display_text = arg.get('display_text', arg_name)
131+
132+
content += f"<details open><summary><code>{display_text}</code></summary>\n\n"
133+
content += f"TODO: Add description for {arg_name} ({arg_type})\n\n"
134+
content += "</details>\n\n"
135+
136+
# Generate Optional arguments section
137+
if optional_args:
138+
content += "## Optional arguments\n\n"
139+
for arg in optional_args:
140+
arg_name = arg.get('name', 'unknown')
141+
arg_type = arg.get('type', 'unknown')
142+
display_text = arg.get('display_text', arg_name)
143+
token = arg.get('token', '')
144+
145+
content += f"<details open><summary><code>{token if token else display_text}</code></summary>\n\n"
146+
content += f"TODO: Add description for {arg_name} ({arg_type})\n\n"
147+
content += "</details>\n\n"
148+
149+
return content
150+
151+
152+
def generate_return_section() -> str:
153+
"""Generate placeholder Return information section."""
154+
return '''## Return information
155+
156+
{{< multitabs id="return-info"
157+
tab1="RESP2"
158+
tab2="RESP3" >}}
159+
160+
TODO: Add RESP2 return information
161+
162+
-tab-sep-
163+
164+
TODO: Add RESP3 return information
165+
166+
{{< /multitabs >}}
167+
168+
'''
169+
170+
171+
def generate_complete_markdown_content(command_name: str, command_data: dict) -> str:
172+
"""Generate the complete markdown content for a command page."""
173+
content = ""
174+
175+
# Add command summary as the main description
176+
summary = command_data.get('summary', f'TODO: Add summary for {command_name}')
177+
content += f"{summary}\n\n"
178+
179+
# Add argument sections
180+
content += generate_argument_sections(command_data)
181+
182+
# Add return information section
183+
content += generate_return_section()
184+
185+
return content
186+
187+
188+
def create_command_file(command_name: str, command_data: dict, all_commands: dict) -> str:
189+
"""Create a complete command markdown file with frontmatter and content."""
190+
# Get the full command name (handles container commands)
191+
full_command_name = get_full_command_name(command_name, command_data)
192+
193+
# Generate the file path using the full command name
194+
filename = command_filename(full_command_name)
195+
filepath = f'content/commands/{filename}.md'
196+
197+
# Ensure the directory exists
198+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
199+
200+
# Check if file already exists
201+
if os.path.exists(filepath):
202+
logging.warning(f"File {filepath} already exists, skipping...")
203+
return filepath
204+
205+
# Generate frontmatter
206+
frontmatter_data = generate_command_frontmatter(command_name, command_data, all_commands)
207+
208+
# Generate content
209+
content = generate_complete_markdown_content(command_name, command_data)
210+
211+
# Create markdown object and set data
212+
md = Markdown(filepath)
213+
md.fm_data = frontmatter_data
214+
md.payload = content
215+
216+
# Write the file
217+
md.persist()
218+
219+
logging.info(f"Created command file: {filepath}")
220+
return filepath
221+
222+
223+
if __name__ == '__main__':
224+
args = parse_args()
225+
226+
# Configure logging BEFORE creating objects
227+
log_level = getattr(logging, args.loglevel.upper())
228+
logging.basicConfig(
229+
level=log_level,
230+
format='%(message)s %(filename)s:%(lineno)d - %(funcName)s',
231+
force=True # Force reconfiguration in case logging was already configured
232+
)
233+
234+
try:
235+
# Load and validate JSON data
236+
commands_data = load_and_validate_json(args.json_file)
237+
logging.info(f"Loaded {len(commands_data)} commands from {args.json_file}")
238+
239+
# Process each command and generate markdown files
240+
created_files = []
241+
for command_name in commands_data:
242+
try:
243+
logging.info(f"Processing command: {command_name}")
244+
filepath = create_command_file(command_name, commands_data[command_name], commands_data)
245+
created_files.append(filepath)
246+
except Exception as e:
247+
logging.error(f"Failed to create file for command '{command_name}': {e}")
248+
# Continue processing other commands
249+
continue
250+
251+
# Summary
252+
logging.info(f"Successfully created {len(created_files)} command files:")
253+
for filepath in created_files:
254+
logging.info(f" - {filepath}")
255+
256+
except (FileNotFoundError, ValueError) as e:
257+
logging.error(f"Error: {e}")
258+
sys.exit(1)
259+
except Exception as e:
260+
logging.error(f"Unexpected error: {e}")
261+
sys.exit(1)

build/components/example.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
'go': '//',
2727
'c#': '//',
2828
'redisvl': '#',
29-
'php': '//'
29+
'php': '//',
30+
'rust': '//',
31+
'rust-sync': '//',
32+
'rust-async': '//'
3033
}
3134

3235

build/local_examples.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
'.go': 'go',
2626
'.cs': 'c#',
2727
'.java': 'java',
28-
'.php': 'php'
28+
'.php': 'php',
29+
'.rs': 'rust'
2930
}
3031

3132
# Language to client name mapping (from config.toml clientsExamples)
@@ -36,7 +37,8 @@
3637
'c#': 'C#',
3738
'java': 'Java-Sync', # Default to sync, could be overridden
3839
'php': 'PHP',
39-
'redisvl': 'RedisVL'
40+
'redisvl': 'RedisVL',
41+
'rust': 'Rust-Sync'
4042
}
4143

4244

@@ -65,6 +67,11 @@ def get_client_name_from_language_and_path(language: str, path: str) -> str:
6567
return 'Java-Async'
6668
if 'lettuce-reactive' in path:
6769
return 'Java-Reactive'
70+
if language == 'rust':
71+
if 'rust-async' in path:
72+
return 'Rust-Async'
73+
if 'rust-sync' in path:
74+
return 'Rust-Sync'
6875
# Default behavior for all languages (and Java fallback)
6976
return get_client_name_from_language(language)
7077

config.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ tagManagerId = "GTM-TKZ6J9R"
4545
gitHubRepo = "https://github.com/redis/docs"
4646

4747
# Display and sort order for client examples
48-
clientsExamples = ["Python", "Node.js", "Java-Sync", "Java-Async", "Java-Reactive", "Go", "C#", "RedisVL", "PHP"]
48+
clientsExamples = ["Python", "Node.js", "Java-Sync", "Java-Async", "Java-Reactive", "Go", "C#", "RedisVL", "PHP", "Rust-Sync", "Rust-Async"]
4949
searchService = "/convai/api/search-service"
5050
ratingsService = "/docusight/api/rate"
5151

@@ -55,7 +55,7 @@ rdi_redis_gears_version = "1.2.6"
5555
rdi_debezium_server_version = "2.3.0.Final"
5656
rdi_db_types = "cassandra|mysql|oracle|postgresql|sqlserver"
5757
rdi_cli_latest = "latest"
58-
rdi_current_version = "1.14.0"
58+
rdi_current_version = "1.14.1"
5959

6060
[params.clientsConfig]
6161
"Python"={quickstartSlug="redis-py"}
@@ -67,6 +67,8 @@ rdi_current_version = "1.14.0"
6767
"C#"={quickstartSlug="dotnet"}
6868
"RedisVL"={quickstartSlug="redis-vl"}
6969
"PHP"={quickstartSlug="php"}
70+
"Rust-sync"={quickstartSlug="rust"}
71+
"Rust-async"={quickstartSlug="rust"}
7072

7173
# Markup
7274
[markup]

content/commands/acl-log.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ title: ACL LOG
4747
---
4848
The command shows a list of recent ACL security events:
4949

50-
1. Failures to authenticate their connections with [`AUTH`]({{< relref "/commands/auth" >}}) or [`HELLO`]({{< relref "/commands/hello" >}}).
51-
2. Commands denied because against the current ACL rules.
52-
3. Commands denied because accessing keys not allowed in the current ACL rules.
50+
1. Failed authentications with [`AUTH`]({{< relref "/commands/auth" >}}) or [`HELLO`]({{< relref "/commands/hello" >}}) (reason = auth)
51+
2. Commands violating the current ACL rules
52+
- Disallowed commands (reason = command).
53+
- Disallowed keys (reason = key).
54+
- Disallowed pub/sub channel (reason = channel).
5355

5456
The optional argument specifies how many entries to show. By default
5557
up to ten failures are returned. The special [`RESET`]({{< relref "/commands/reset" >}}) argument clears the log.

0 commit comments

Comments
 (0)