Skip to content

Commit 16efc44

Browse files
n-rodriguezclaude
andcommitted
test(examples): add compile-checked usage examples
The README code blocks had no compile guarantee, so API changes could silently rot them. Keep the examples as real files, type-checked in CI. - examples/client.cr: high-level Client (get / multi-get / walks / typed set / notifications / v3) - examples/v2c_raw.cr, v3_raw.cr: raw-socket exchanges with response-id and error-status validation - mise: dev:examples runs crystal build --no-codegen on examples/*.cr - ci: type-check the examples in the docs job Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2d634f4 commit 16efc44

5 files changed

Lines changed: 128 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ jobs:
6767
- name: Build API docs
6868
run: mise dev:docs
6969

70+
- name: Type-check the README examples
71+
run: mise dev:examples
72+
7073
test_linux:
7174
runs-on: ubuntu-latest
7275
steps:

examples/client.cr

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# High-level client usage — one `SNMP::Client` per fiber.
2+
# In your project: require "snmp"
3+
require "../src/snmp"
4+
5+
# ---- v2c ----
6+
client = SNMP::Client.new("localhost", community: "public")
7+
8+
# Single get: the shortcut accessors read the first varbind
9+
message = client.get("1.3.6.1.2.1.1.4.0")
10+
puts message.value.get_string
11+
12+
# Multi-varbind get: one round-trip for several OIDs
13+
message = client.get(["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.5.0"])
14+
message.varbinds.each { |varbind| puts "#{varbind.oid} => #{varbind.value.get_string}" }
15+
16+
# Walk a subtree (GetNext based); the block form streams
17+
client.walk("1.3.6.1.2.1.1.9.1.3") do |msg|
18+
puts "#{msg.oid} => #{msg.value.get_string}"
19+
end
20+
21+
# Bulk walk (GetBulk based, fewer round-trips); yields each VarBind
22+
client.bulk_walk("1.3.6.1.2.1.2.2.1.2", max_repetitions: 25) do |varbind|
23+
puts "#{varbind.oid} => #{varbind.value.get_string}"
24+
end
25+
26+
# Set values — Crystal primitives or typed SNMP values
27+
client.set("1.3.6.1.2.1.1.6.0", "server room")
28+
client.set("1.3.6.1.2.1.1.3.0", SNMP::TimeTicks.new(12_345_u32))
29+
30+
# Multi-varbind set: one SetRequest for several assignments
31+
client.set({
32+
"1.3.6.1.2.1.1.5.0" => "hostname",
33+
"1.3.6.1.2.1.1.6.0" => "the location",
34+
})
35+
36+
# Send notifications (community sessions)
37+
client.send_trap_v2("1.3.6.1.4.1.8072.2.3.0.1", uptime: 12_345)
38+
response = client.send_inform("1.3.6.1.4.1.8072.2.3.0.1")
39+
puts response.request # Response (the inform acknowledgement)
40+
41+
# Release the reused UDP socket when done (reconnects transparently if reused)
42+
client.close
43+
44+
# ---- v3 ----
45+
# The client drives engine discovery, HMAC verification, the RFC 3414 time
46+
# window, and auto-resyncs/retries once on a usmStats Report.
47+
security = SNMP::V3::Security.new(
48+
"usr-sha-aes",
49+
auth_protocol: SNMP::V3::Security::AuthProtocol::SHA256,
50+
auth_password: "authkey1",
51+
priv_protocol: SNMP::V3::Security::PrivacyProtocol::AES256,
52+
priv_password: "privkey1"
53+
)
54+
v3_client = SNMP::Client.new("localhost", SNMP::V3::Session.new(security))
55+
puts v3_client.get("1.3.6.1.2.1.1.4.0").value.get_string

examples/v2c_raw.cr

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Raw-socket v2c exchange, for when you manage the transport yourself.
2+
# In your project: require "snmp"
3+
require "../src/snmp"
4+
5+
# Connect to the agent
6+
socket = UDPSocket.new
7+
socket.connect("localhost", 161)
8+
socket.sync = false
9+
socket.read_timeout = 3.seconds
10+
11+
# Build and send the request
12+
session = SNMP::Session.new(community: "public")
13+
request = session.get("1.3.6.1.2.1.1.4.0")
14+
socket.write_bytes request
15+
socket.flush
16+
17+
# Parse the response, verifying it answers *this* request
18+
response = session.parse(socket.read_bytes(ASN1::BER))
19+
raise "response-id mismatch" unless response.request_id == request.request_id
20+
raise "agent error: #{response.error_status}" unless response.error_status.no_error?
21+
22+
puts response.value.get_string
23+
socket.close

examples/v3_raw.cr

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Raw-socket SNMPv3 exchange: engine discovery, auth+priv, response validation.
2+
# In your project: require "snmp"
3+
require "../src/snmp"
4+
5+
# Connect to the agent
6+
socket = UDPSocket.new
7+
socket.connect("localhost", 161)
8+
socket.sync = false
9+
socket.read_timeout = 3.seconds
10+
11+
# Session with authentication and privacy (see AuthProtocol / PrivacyProtocol
12+
# for the supported algorithms, incl. SHA-2 and AES-256)
13+
session = SNMP::V3::Session.new(
14+
"usr-md5-aes", "authkey1", "privkey1",
15+
priv_protocol: SNMP::V3::Security::PrivacyProtocol::AES
16+
)
17+
18+
# Discover the engine id / boots / time (required before authenticated requests;
19+
# also drives periodic revalidation of the RFC 3414 time window)
20+
if session.must_revalidate?
21+
socket.write_bytes session.engine_validation_probe
22+
socket.flush
23+
session.validate socket.read_bytes(ASN1::BER)
24+
end
25+
26+
# Build the request, then prepare it (encrypt + sign) for transmission
27+
request = session.get("1.3.6.1.2.1.1.4.0")
28+
socket.write_bytes session.prepare(request)
29+
socket.flush
30+
31+
# Parse the response: the HMAC is verified and the time window enforced.
32+
# Still check that it answers *this* request before trusting the values.
33+
response = session.parse(socket.read_bytes(ASN1::BER))
34+
raise "response-id mismatch" unless response.request_id == request.request_id
35+
raise "agent error: #{response.error_status}" unless response.error_status.no_error?
36+
37+
puts response.value.get_string
38+
socket.close

mise.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ env = { CRYSTAL_WORKERS = "4" }
5454
description = "Build the API docs (fails on doc-comment / compile errors)"
5555
run = "crystal docs"
5656

57+
[tasks."dev:examples"]
58+
description = "Type-check the README examples (keeps the docs honest)"
59+
run = """
60+
for f in examples/*.cr; do
61+
echo "==> $f"
62+
crystal build --no-codegen "$f"
63+
done
64+
"""
65+
5766
[tasks."dev:check"]
5867
description = "Format-check, lint and test in one shot"
5968
run = "mise dev:format-check && mise dev:ameba && mise dev:spec && mise dev:spec-mt"

0 commit comments

Comments
 (0)