Skip to content

Instantly share code, notes, and snippets.

@mpapis
Last active July 25, 2026 10:11
Show Gist options
  • Select an option

  • Save mpapis/c114ed6a3b48d2863bdd3f104d0ecf83 to your computer and use it in GitHub Desktop.

Select an option

Save mpapis/c114ed6a3b48d2863bdd3f104d0ecf83 to your computer and use it in GitHub Desktop.
DeepSeek-V4-Flash 2bit (vq2) on GB10/sm_121 — MTP spec-decode repro + fix examples
# secrets & local config — never published
.env
CLAUDE.md
# orphan box-local experiment scripts — NOT part of the public suite (real home:
# ~/deepseek-2bit-vllm); kept out of the gist (bench_2bit.py referenced an internal path)
bench_2bit.py
bench_mhc.py
# byproducts & results — this package is a driver, results live elsewhere
__pycache__/
result*.json
rollup*.json
*.perf.json
*.deepeval.json
*.concurrency*.json
*.fullctx-conc.json
*.curve.json
*.wctrl.json
*.bfcl.json
*.degradation.*.json

LLM server benchmark suite

Benchmark harnesses for OpenAI-compatible LLM servers: long-context needle recall, prefill/decode/latency probes, concurrency limits, a quality suite (instruction following, coding, tool use, multi-turn), and a quality-degradation curve on real OSS code. Everything talks to a running server over plain HTTP and takes the endpoint as a parameter — point it at any OpenAI-compatible server.

The full spec — tiers, metrics, sample sizes, safety rules, and the harness index — is in BENCHMARK.md. This package is a driver: result JSONs are written to a persistent directory outside it and are never committed here.

Quick start

# one-shot needle recall + decode probe against a running server
python3 run_needle.py --model <served-id> --url http://localhost/v1 --out /tmp/needle.json
python3 score_answers.py /tmp/needle.json answer_key.json
python3 run_needle.py --model <served-id> --decode-probe

# full perf phases (warmup, decode probe, needle levels, warm repeat, concurrency)
python3 bench_levels.py --model <served-id> --url http://localhost/v1 \
    --max-model-len 262144 --out /tmp/perf.json

# quality suite (subject = served model; judge = independent external model, see BENCHMARK.md)
python3 deepeval_full.py <served-id> --bench

# full-window concurrency (saturation ramp, or an explicit level sweep)
python3 bench_concurrency.py --model <served-id> --url http://localhost:80
python3 bench_concurrency.py --model <served-id> --levels 1,2,3,4 --gen 1000

# degradation curve (real-code tasks at increasing context; corpus setup in BENCHMARK.md)
python3 bench_degradation.py --model <served-id> --task 3 --max-ctx 262144 --out /tmp/deg.json

# serial fleet run over a model catalog (stop → start → health → perf → quality → stop)
./run_fleet.sh

Reasoning models need a generous output budget (--max-tokens ≥ 8000) or they run out mid-think and score a false 0/18 — see BENCHMARK.md for this and every other interpretation rule.


Appendix — DeepSeek-V4-Flash 2-bit (vq2) on GB10 / sm_121: MTP spec-decode repro + fix examples

Companion examples for the PR that routes the MTP drafter's mhc_post through the torch reference on sm_121. These are the exact scripts/commands used to reproduce the crash, prove the fix without touching the plugin, measure decode rate, and run a long-context recall test.

Environment (matched to requirements-serve.lock.txt): vLLM 0.23.0, torch 2.11.0+cu130, tilelang 0.1.9, apache-tvm-ffi 0.1.9; NVIDIA GB10 (Grace Blackwell, sm_121), 128 GB unified memory; CUDA 13.0 / driver 580.x; DGX-OS Ubuntu 24.04 aarch64. Stock vLLM + the auto-registering vq2_vllm plugin, run in the host .venv-vllm (not Docker).

1. Reproduce the crash (unmodified plugin)

# MTP spec decode + FULL cudagraph -> illegal memory access during the drafter's
# cudagraph capture, in mhc_post_tilelang. enforce_eager and non-spec both run clean.
python scripts/bench.py --spec-mtp 2

2. Prove the fix WITHOUT editing the plugin (bench_mhc.py)

bench_mhc.py just swaps mtp.mhc_post_tilelang = mhc_post_torch between setup_env and build_llm, then runs the same coherence + differential decode bench as scripts/bench.py. MHC_TORCH=0 reproduces the crash; =1 (default) applies the swap.

python bench_mhc.py                 # patched -> coherent, ~33.7 tok/s (MTP K=2)
MHC_TORCH=0 python bench_mhc.py     # unpatched -> illegal-access crash

3. Durable fix

The PR makes this permanent inside vq2_vllm/__init__.py (_patch_mhc_torch(), gated by VQ2_MHC_TORCH), mirroring the existing sparse-MLA / indexer torch-reference patches. With it, scripts/bench.py --spec-mtp 2 runs unmodified.

4. Decode-rate bench (durable fix active)

# Differential method: time(gen=N+1) - time(gen=1), isolates decode from prefill.
python scripts/bench.py --spec-mtp 2 --no-frspec --gen 64

5. Long-context single-stream run (~320k)

prompt.txt here is a ~320k-token needle-in-a-haystack test: the haystack is a concatenation of public Ruby OSS source (rvm/pluginator, ruby-git, graphql-ruby, etc.), with 18 fictional needles (NOTE [id]: lines carrying invented sentinel facts) spread across early/mid/late positions. answer_key.json holds the needles; score_answers.py parses the model's A<n>: answers and reports recall.

Memory is the only knob that matters at this length. The fp8_ds_mla KV is ~6.5 KB/token, so ~320k needs only ~2 GiB of KV — but vLLM's default util-sized pool is far larger (~11 GiB at 0.95), leaving too little physical headroom for the prefill activation transient on the 128 GB ceiling (OOM, no swap). At --gpu-memory-utilization 0.85 the pool instead goes negative (weights 96.9 GiB + ~8.3 GiB fixed overhead leave nothing). The fix: cap the KV pool and shrink the prefill chunk so the transient has room:

VQ2_ADAPTIVE_PREFILL=1 MAX_NUM_BATCHED_TOKENS=1024 GPU_UTIL=0.92 \
KV_CACHE_MEMORY_BYTES=3000000000 SPEC_MTP=2 \
  python bench_2bit.py \
    --prompt-file prompt.txt --out result-2bit-320k.json \
    --max-model-len 325000 --max-tokens 3000 --temperature 0.6 --top-p 0.95

python score_answers.py result-2bit-320k.json answer_key.json

In our testing this 2-bit model at ~320k completes coherently (no degeneration / BOS-spam) but recalls poorly — it stays fluent yet answers generically rather than retrieving from context. Run the two commands above to reproduce and score it.

bench_2bit.py and bench_mhc.py import the repo's scripts/_common.py; run them from inside the cloned repo (or put scripts/ on PYTHONPATH).

#!/usr/bin/env python3
"""Emit a docker-compose OVERRIDE patching --gpu-memory-utilization and
--max-num-seqs into a model dir's serve command — the committed compose.yaml is
never edited. Compose replaces `command` from the last -f file, so we emit the
FULL patched command. Prints the service name (for the orchestrator).
python3 _conc_override.py <dir>/compose.yaml <util> <max_num_seqs> <override_out>
"""
import sys
import yaml
compose_path, util, mns, out = sys.argv[1:5]
doc = yaml.safe_load(open(compose_path))
svcs = doc["services"]
name = next(iter(svcs)) # single service per dir (vllm / ds4)
cmd = list(svcs[name].get("command") or [])
def set_flag(cmd, flag, val):
if flag in cmd:
cmd[cmd.index(flag) + 1] = str(val)
else:
cmd += [flag, str(val)]
return cmd
cmd = set_flag(cmd, "--gpu-memory-utilization", util)
cmd = set_flag(cmd, "--max-num-seqs", mns)
yaml.safe_dump({"services": {name: {"command": cmd}}}, open(out, "w"),
default_flow_style=False)
print(name)
#!/usr/bin/env python3
"""Aggregate fleet results ($RESULTS/<dir>.perf.json + <dir>.deepeval.json) into TWO
ranked Markdown tables (Cost+Performance, Quality) + a JSON rollup. Stdlib only.
python3 aggregate.py --results /tmp/gx10-bench-results --out-json /tmp/rollup.json
The judge is an INDEPENDENT external model (deepseek-v4-flash-free via OpenCode Zen),
not the served model grading itself — so the quality scores measure each subject.
"""
import argparse
import glob
import json
import os
# engine + on-disk weight size (GB, `du -sh` on the box). Presentation-only metadata:
# the perf/quality JSONs don't carry these, so they're keyed by model dir here.
MODEL_META = {
"qwen3.5-122b-nvfp4-full-gb10": ("vLLM", 66),
"nemotron-3-super-120b-nvfp4": ("vLLM", 75),
"nex-n2-mini": ("vLLM", 66),
"qwen-agentworld-35b": ("vLLM", 65),
"step-3.7-flash-apex": ("llama.cpp", 90),
"qwen3.5-35b-a3b-apex": ("llama.cpp", 23),
"qwen3.6-35b-a3b-nvfp4": ("vLLM", 22),
"kimi-dev-72b-q8": ("llama.cpp", 72),
"qwen3.6-27b-bf16": ("vLLM", 53),
"qwen3-coder-next-80b-apex": ("llama.cpp", 54),
"nex-n2-pro-iq2xxs": ("llama.cpp", 106),
"huihui-qwen3.6-35b-a3b-nvfp4": ("vLLM", 22),
"huihui-qwen3-next-80b-a3b-nvfp4": ("vLLM", 48),
"huihui-qwen3.5-27b-opus-nvfp4": ("vLLM", 18),
# current v5 keepers
"mistral-small-4-119b-nvfp4": ("vLLM", 66),
"gemma-4-26b-a4b-opus-apex": ("llama.cpp", 19),
"qwen3-coder-30b-a3b-int4": ("vLLM", 16),
"qwen3-coder-30b-a3b-huihui-apex": ("llama.cpp", 18),
}
# TRUE 18/18 context ceilings (INPUT tokens) from dedicated bench_levels probes (W4.5).
# The gate perf.json `input_18of18` is START-CLAMPED by run_fleet's --start-input, so it is a
# LOWER BOUND, not a ceiling — feeding it into Q under-credits the long-context models (their
# gate CTX all read ~340 because the gate's start-input caps there, while their real ceilings
# are far higher). These values come from the ceiling-probe JSONs in $RESULTS (provenance per
# entry, box-local); resolve_ctx() re-verifies each against its file at aggregate time and warns
# on drift. Re-measure + update when a window/quant/YaRN change moves a model's ceiling.
# A model ABSENT here falls back to its gate input_18of18 (its probe is pending).
TRUE_CTX_CEILING = {
"qwen3-coder-30b-a3b-int4": (193862, "coder-int4.native-ceiling.json"), # retention-limited inside native 262144
"mistral-small-4-119b-nvfp4": (644498, "mistral.ctx-bisect.json"), # window 1M, recall ceiling ~644k
"nemotron-3-super-120b-nvfp4": (487284, "nemotron.ext-500k-probe.json"), # YaRN x2, window 500k
"qwen3-coder-next-80b-apex": (722005, "qwen3-coder-next-80b-apex.ctxprobe-x3top.json"), # YaRN x3, recall unbroken at x3 window top (was x2 471962; capped here for practicality, extends further)
"gemma-4-26b-a4b-opus-apex": (1000213, "gemma-4-26b-a4b-opus-apex.ctxprobe-x4.json"), # YaRN x4, recall UNBROKEN at 1M (floor, not a break)
"qwen3-coder-30b-a3b-huihui-apex": (165225, "qwen3-coder-30b-a3b-huihui-apex.ctxprobe.json"), # retention cliff ~165-182k INSIDE native 262144
}
def resolve_ctx(dir_name, gate_input, results):
"""True-ceiling CTX in INPUT tokens: the curated probe value when present, else the
(start-clamped) gate value. When the provenance file is on disk, re-read it and prefer the
file's max_passing over the curated constant (warn on drift), so the map can't silently rot.
Returns (ctx_input_tokens|None, source_str|None)."""
ent = TRUE_CTX_CEILING.get(dir_name)
if not ent:
return gate_input, ("gate" if gate_input else None)
val, prov = ent
d = load(os.path.join(results, prov))
if d is not None:
file_val = (d.get("needle") or {}).get("max_passing_input_tokens")
if file_val and file_val != val:
print(f" [ctx] WARN {dir_name}: curated {val} != {prov} {file_val}; using file value",
flush=True)
val = file_val
return val, f"probe:{prov}"
def load(path):
try:
with open(path) as fh:
return json.load(fh)
except Exception:
return None
def cell(v):
return "—" if v is None else str(v)
def perf_row(dir_name, p):
nd = p.get("needle") or {}
levels = nd.get("levels") or []
passing = next((l for l in levels if l.get("needle_score") == 1.0 and not l.get("degenerate")), None)
probe = p.get("decode_probe") or {}
warm = (p.get("needle_warm") or {}).get("warm") or {}
conc = p.get("concurrency") or {}
min_mem = min([l["memory"]["min_mem_available_mb"] for l in levels
if l.get("memory", {}).get("min_mem_available_mb") is not None] or [None])
engine, weight_gb = MODEL_META.get(dir_name, ("?", None))
# concurrency cell: "agg t/s ×speedup", or "fail" when no concurrent request succeeded
if conc.get("n_success") == 0:
conc_cell = "fail (n=0)"
elif conc.get("aggregate_tok_per_s") is not None:
sp = conc.get("speedup_vs_single")
conc_cell = f"{conc['aggregate_tok_per_s']}" + (f" ×{sp}" if sp else "")
else:
conc_cell = None
return {
"model": dir_name,
"engine": engine,
"weight_gb": weight_gb,
"served": p.get("model"),
"window": p.get("max_model_len"),
"input_18of18": (passing or {}).get("prompt_tokens"),
"output_cap": (passing or {}).get("max_tokens_allowed"),
"needle_gen_tok": (passing or {}).get("completion_tokens"),
"decode_out_tps": probe.get("decode_tok_per_s"),
"cold_pp": (passing or {}).get("pp_tok_per_s"),
"warm_pp": warm.get("pp_tok_per_s"),
"concurrency": conc_cell,
"conc_n": conc.get("concurrency"),
"min_mem_mb": min_mem,
# extra fields kept in the JSON rollup (not shown in the compact table)
"levels_tried": len(levels),
"decode_at_max": (passing or {}).get("overall_tok_per_s"),
"cold_wall_s": (passing or {}).get("wall_s"),
"warm_wall_s": warm.get("wall_s"),
"start_note": nd.get("start_note"),
}
def flatten_scores(d):
"""Pull every numeric metric/benchmark score out of a deepeval result."""
scores = {}
for group, metrics in (d.get("metrics") or {}).items():
for name, res in metrics.items():
if isinstance(res, dict) and res.get("ok") and isinstance(res.get("result"), dict):
sc = res["result"].get("score") or res["result"].get("overall_score")
if isinstance(sc, (int, float)):
scores[f"{group}.{name}"] = round(sc, 3)
for name, res in (d.get("benchmarks") or {}).items():
if isinstance(res, dict) and res.get("ok") and isinstance(res.get("result"), dict):
sc = res["result"].get("overall_score")
if isinstance(sc, (int, float)):
scores[f"bench.{name}"] = round(sc, 3)
return scores
def _num(scores, key):
v = scores.get(key)
return v if isinstance(v, (int, float)) else None
def _bench_pct(d, name):
"""Benchmark score as a percent string 'NN.N% (k-of-N context)' — v4 sizes
(IFEval-100 / HumanEval-157) make percent display meaningful."""
res = (d.get("benchmarks") or {}).get(name) or {}
r = res.get("result") if isinstance(res.get("result"), dict) else {}
pct = r.get("pct")
if pct is None and isinstance(r.get("overall_score"), (int, float)):
pct = round(100 * r["overall_score"], 1)
if pct is None:
return None, None
n = r.get("n_problems") or r.get("tasks")
return f"{pct}%", n
def quality_row(dir_name, d):
scores = flatten_scores(d)
metrics = d.get("metrics") or {}
ok = sum(1 for g in metrics.values() for r in g.values()
if isinstance(r, dict) and r.get("ok"))
total = sum(len(g) for g in metrics.values())
# MT = mean of the multiturn group's numeric scores
mt_vals = [scores[k] for k in scores if k.startswith("multiturn.")]
mt = round(sum(mt_vals) / len(mt_vals), 2) if mt_vals else None
jval, tval = _num(scores, "nonllm.json_correctness"), _num(scores, "agentic.tool_correctness")
ifeval_pct, ifeval_n = _bench_pct(d, "ifeval")
he_pct, he_n = _bench_pct(d, "humaneval")
mmlu_pct, mmlu_n = _bench_pct(d, "mmlu")
return {
"model": dir_name,
"judge": d.get("judge"),
"suite": d.get("suite"),
"ifeval": ifeval_pct, "ifeval_n": ifeval_n,
"humaneval": he_pct, "humaneval_n": he_n,
"mmlu_deep": mmlu_pct, "mmlu_n": mmlu_n,
"ifeval_raw": _num(scores, "bench.ifeval"),
"humaneval_raw": _num(scores, "bench.humaneval"),
"json_mt_tool": {"json": jval, "mt": mt, "tool": tval},
"jmt_disp": f"{cell(jval)}/{cell(mt)}/{cell(tval)}",
"eval_wall_s": d.get("total_wall_s"),
"metrics_ok": f"{ok}/{total}",
"scores": scores,
"usage": d.get("usage"),
}
def _lvl_norm(lv):
"""One degradation level → 0-1. Judge tasks: judge_score/3; keyword tasks:
keyword_score (already 0-1). A missing/errored level = 0.0 — a failure at that
context IS maximal degradation, which is the signal we want to capture."""
if lv.get("score_kind") == "judge":
js = lv.get("judge_score")
return max(0.0, min(1.0, js / 3.0)) if isinstance(js, (int, float)) else 0.0
kw = lv.get("keyword_score")
return max(0.0, min(1.0, kw)) if isinstance(kw, (int, float)) else 0.0
def load_degradation(results, dir_name):
"""Reduce the 5 real-project tasks (250k-cap runs only; `.native.` excluded) to one
0-100 Degradation score + summed wall seconds. Task score = mean over its levels;
Degradation = mean over the present tasks × 100. Returns (score|None, wall_s)."""
task_means, wall = [], 0.0
files = sorted(f for f in glob.glob(os.path.join(results, dir_name + ".degradation.t*.json"))
if ".native." not in os.path.basename(f))
for f in files:
d = load(f)
if not d:
continue
norms = []
for lv in d.get("levels") or []:
wall += lv.get("wall_s") or 0.0
norms.append(_lvl_norm(lv))
if norms:
task_means.append(sum(norms) / len(norms))
if not task_means:
return None, round(wall, 1)
return round(100 * sum(task_means) / len(task_means), 1), round(wall, 1)
def load_bfcl(results, dir_name):
"""BFCL (tool-calling) OVERALL as 0-100 + wall seconds. Returns (pct|None, wall_s)."""
d = load(os.path.join(results, dir_name + ".bfcl.json"))
if not d:
return None, 0.0
pct = d.get("overall_pct")
if pct is None and isinstance(d.get("overall"), (int, float)):
pct = round(d["overall"] * 100, 1) if d["overall"] <= 1 else round(d["overall"], 1)
return pct, round(d.get("wall_s") or 0.0, 1)
def load_w9_degradation(results, dir_name):
"""W9 degradation (bench_context_curve.py, DeepSeek-judged, temp-0): the Degradation
term = quality RETAINED at the common 200k cap = the curve's baseline_mean @200k, plus
the wall of that 200k eval. This SUPERSEDES the v5 kimi-judged .degradation.t*.json:
W9 is a dedicated per-task degradation run (temp-0, warm/cold controlled, 5 repeats) and
is the current authoritative degradation axis. HE/IF/BFCL in Q are judge-INDEPENDENT
(execution / rule / offline AST), so a Q built on W9 depends on exactly one judge
(DeepSeek, via this term) — no cross-judge mixing. Returns (score|None, wall_s, source).
Absent curve.json => (None, 0, None) so compute_qr falls back to the kimi degradation."""
c = load(os.path.join(results, dir_name + ".curve.json"))
if not c:
return None, 0.0, None
brec = (c.get("lengths") or {}).get(str(c.get("baseline_length"))) or {}
wall = sum(r.get("wall_s") or 0.0 for runs in (brec.get("tasks") or {}).values() for r in runs)
return c.get("baseline_mean"), round(wall, 1), "W9:curve.json@200k(DeepSeek,temp0)"
def compute_qr(qrow, prow, results):
"""The operator's ranking formula (2026-07-16), all test scores 0-100:
CTX = input_at_18of18 / 1000
Q = 2*HumanEval + 3*Degradation + CTX + IFEval + 2*BFCL
W = (deepeval + degradation + bfcl) wall, in MINUTES
R = Q / W (points per minute)
Q/R are computed only when every term is present; else they stay None (shown '—')
so a partial fleet never renders a misleadingly-low number. Mutates qrow (+ prow)."""
he = qrow.get("humaneval_raw")
iff = qrow.get("ifeval_raw")
gate_input = (prow or {}).get("input_18of18")
ctx_input, ctx_source = resolve_ctx(qrow["model"], gate_input, results)
# Degradation: prefer W9 (DeepSeek, dedicated per-task curve) — the authoritative axis;
# fall back to the v5 kimi .degradation.t*.json only when no curve.json exists. deg_source
# records which was used so a mixed fleet (some models W9, some kimi) is never silently
# cross-judge-compared — W9 and kimi degradation are NOT comparable across rows.
deg, deg_wall, deg_src = load_w9_degradation(results, qrow["model"])
if deg is None:
deg, deg_wall = load_degradation(results, qrow["model"])
deg_src = "v5-kimi:degradation.t*" if deg is not None else None
bfcl, bfcl_wall = load_bfcl(results, qrow["model"])
qrow["deg"] = deg
qrow["deg_source"] = deg_src
qrow["bfcl"] = bfcl
qrow["ctx"] = round(ctx_input / 1000.0, 1) if ctx_input else None
qrow["ctx_source"] = ctx_source
qrow["deg_wall_s"] = deg_wall
qrow["bfcl_wall_s"] = bfcl_wall
complete = None not in (he, iff, ctx_input, deg, bfcl) and (qrow.get("eval_wall_s") is not None)
if not complete:
qrow["Q"] = qrow["R"] = qrow["W_min"] = None
else:
q = 2 * he * 100 + 3 * deg + ctx_input / 1000.0 + iff * 100 + 2 * bfcl
w_min = (qrow["eval_wall_s"] + deg_wall + bfcl_wall) / 60.0
qrow["Q"] = round(q, 1)
qrow["W_min"] = round(w_min, 1)
qrow["R"] = round(q / w_min, 2) if w_min > 0 else None
if prow is not None:
for k in ("ctx", "ctx_source", "deg", "bfcl", "Q", "W_min", "R"):
prow[k] = qrow[k]
def md_table(rows, cols):
"""cols = [(key, label), ...]."""
out = ["| " + " | ".join(c[1] for c in cols) + " |",
"|" + "|".join("---" for _ in cols) + "|"]
for r in rows:
out.append("| " + " | ".join(cell(r.get(c[0])) for c in cols) + " |")
return "\n".join(out)
def main():
ap = argparse.ArgumentParser()
default_results = os.environ.get("RESULTS") or os.path.expanduser("~/.cache/gx10-bench-results")
ap.add_argument("--results", default=default_results)
ap.add_argument("--out-json", default=os.path.join(default_results, "rollup.json"))
a = ap.parse_args()
perf_rows, qual_rows = [], []
for pf in sorted(glob.glob(os.path.join(a.results, "*.perf.json"))):
dir_name = os.path.basename(pf)[:-len(".perf.json")]
p = load(pf)
if p:
perf_rows.append(perf_row(dir_name, p))
d = load(os.path.join(a.results, dir_name + ".deepeval.json"))
if d:
qual_rows.append(quality_row(dir_name, d))
# Cross-join: eval-wall lives in the deepeval JSON (shown in Perf); weights GB + free
# RAM live in the perf JSON (shown in Quality). Keyed by model dir.
qmap = {r["model"]: r for r in qual_rows}
pmap = {r["model"]: r for r in perf_rows}
for r in perf_rows:
r["eval_wall_s"] = (qmap.get(r["model"]) or {}).get("eval_wall_s")
for r in qual_rows:
p = pmap.get(r["model"]) or {}
r["weight_gb"] = p.get("weight_gb")
r["min_mem_mb"] = p.get("min_mem_mb")
# Q/R per the operator's formula (2026-07-16): mutates each quality row and mirrors
# ctx/deg/bfcl/Q/W/R onto its perf row so both tables can show them.
for r in qual_rows:
compute_qr(r, pmap.get(r["model"]), a.results)
# Rank by R (points/min) desc; rows without a computable R (partial fleet) fall to
# the legacy quality mean. Perf rows follow the quality order so BOTH tables match.
def _rank(r):
return (1, r["R"]) if r.get("R") is not None else \
(0, ((r.get("ifeval_raw") or 0) + (r.get("humaneval_raw") or 0)) / 2)
qual_rows.sort(key=_rank, reverse=True)
order = {m: i for i, m in enumerate(r["model"] for r in qual_rows)}
perf_rows.sort(key=lambda r: order.get(r["model"], 1_000_000))
rollup = {"perf": perf_rows, "quality": qual_rows}
with open(a.out_json, "w") as fh:
json.dump(rollup, fh, indent=2)
suites = sorted({r.get("suite") for r in qual_rows if r.get("suite")})
suite_lbl = "/".join(suites) if suites else "unknown"
print(f"## Cost + Performance (suite {suite_lbl}; sorted by R = Q/wall, points/min)\n")
print(md_table(perf_rows, [
("model", "model"), ("engine", "engine"),
("window", "window"), ("input_18of18", "input @18/18"), ("output_cap", "output-cap"),
("needle_gen_tok", "needle gen-tok"), ("decode_out_tps", "decode-out (t/s)"),
("concurrency", "conc agg (t/s ×)"),
("cold_pp", "pp (t/s)"), ("cold_wall_s", "cold wall (s)"), ("warm_wall_s", "warm wall (s)"),
("eval_wall_s", "eval wall (s)"), ("Q", "Q"), ("R", "R (pts/min)"),
]))
judges = sorted({r.get("judge") for r in qual_rows if r.get("judge")})
print(f"\n## Quality (suite {suite_lbl}; judge = {'; '.join(judges) if judges else 'unknown'}; sorted by R)\n")
print(md_table(qual_rows, [
("model", "model"), ("weight_gb", "weights GB"), ("min_mem_mb", "free RAM (MB)"),
("ifeval", "IFEval"), ("humaneval", "HumanEval"), ("deg", "Degrad"),
("bfcl", "BFCL tool"), ("mmlu_deep", "MMLU (deep)"), ("ctx", "CTX"),
("jmt_disp", "JSON/MT/Tool"), ("metrics_ok", "metrics ok"),
("Q", "Q"), ("R", "R (pts/min)"),
]))
deg_srcs = sorted({r.get("deg_source") for r in qual_rows if r.get("deg_source")})
print("\n_R = Q / summed-suite-wall(min), the promotion ratio (operator formula 2026-07-16): "
"Q = 2·HumanEval + 3·Degradation + CTX + IFEval + 2·BFCL, all test scores 0-100, "
"CTX = the FULL-EXTENT 18/18 needle ceiling ÷ 1000 (dedicated bench_levels probe at "
"each model's max YaRN window — see TRUE_CTX_CEILING; the Cost table's 'input @18/18' "
"is the START-CLAMPED gate value, informational only). Degrad = quality RETAINED at the "
"common 200k cap = W9 curve baseline_mean (bench_context_curve.py, DeepSeek-judged, "
"temp-0, per-task) where a curve.json exists — else the legacy v5 kimi degradation "
"(deg_source column flags which; the two are NOT comparable across rows). BFCL = "
"tool-calling (Berkeley Function Calling Leaderboard AST). "
"JUDGE SEPARATION: HumanEval (execution pass@1), IFEval (rule), BFCL (offline AST) are "
"judge-INDEPENDENT; the only LLM-judged Q term is Degradation, so a W9-sourced Q depends "
"on exactly one judge (DeepSeek). IFEval n=100, HumanEval n=157 pass@1 (deepeval's full "
"task enum). MMLU (deep) informational, never a criterion. JSON/MT/Tool = "
"json_correctness / mean(multiturn) / tool_correctness (kimi-judged, informational)._")
if len(deg_srcs) > 1:
print(f"\n**WARNING: mixed degradation sources across rows {deg_srcs} — the Degrad/Q/R "
"of W9 rows and kimi rows are NOT comparable; do not rank them in one table.**")
if len(judges) > 1:
print("\n**WARNING: results from more than one judge — judged scores are not comparable across rows.**")
print(f"\nrollup -> {a.out_json}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Summarize the concurrency-limits fleet run — reads every
``<results>/<dir>.concurrency.json`` emitted by ``bench_concurrency.py`` (via
``run_concurrency_fleet.sh``) and prints one ranked Markdown table.
python3 aggregate_concurrency.py [results_dir] # default /tmp/gx10-bench-results
Per model it surfaces: the util it ran at (U*), KV pool, tested window, the
**max sustained concurrent sessions** at that full window, the best aggregate
throughput seen + the per-agent rate at that point, recall-under-load, the
low-water free memory, and any note (rejected by the 180k gate / errored /
single-stream engine). Ranked by max-sustained then aggregate t/s.
"""
import glob
import json
import os
import sys
RESULTS = sys.argv[1] if len(sys.argv) > 1 else "/tmp/gx10-bench-results"
def load(path):
try:
with open(path) as fh:
return json.load(fh)
except Exception as e:
return {"_load_error": repr(e)}
def best_ramp(ramp):
"""The ramp step with the highest aggregate t/s (best sustained throughput)."""
ok = [s for s in (ramp or []) if s.get("n_success") and s.get("aggregate_tok_per_s")]
return max(ok, key=lambda s: s["aggregate_tok_per_s"]) if ok else None
def row(path):
d = load(path)
dirname = os.path.basename(path).replace(".concurrency.json", "")
if d.get("_load_error"):
return {"model": dirname, "note": f"load error: {d['_load_error']}", "_max": -1}
if d.get("rejected"):
return {"model": dirname, "window": d.get("max_model_len", "?"),
"note": f"rejected: {d['rejected']}", "_max": -1}
sat = d.get("saturation") or {}
max_n = sat.get("max_sustained_n", 0) or 0
reason = sat.get("reason") or sat.get("note") or ""
b = best_ramp(d.get("ramp"))
# low-water free memory across all ramp steps
frees = [s.get("min_free_mb") for s in (d.get("ramp") or []) if s.get("min_free_mb") is not None]
return {
"model": dirname,
"util": d.get("util", "-"),
"kv_pool": d.get("kv_pool_tokens", 0),
"window": d.get("max_model_len", "?"),
"max_n": max_n,
"agg": (b or {}).get("aggregate_tok_per_s"),
"per_agent": (b or {}).get("per_req_tok_per_s_mean"),
"at_n": (b or {}).get("n"),
"recall": (b or {}).get("recall", "-"),
"min_free": min(frees) if frees else None,
"note": reason,
"_max": max_n,
}
def fmt(v, dash="-"):
return dash if v is None else v
def main():
paths = sorted(glob.glob(os.path.join(RESULTS, "*.concurrency.json")))
if not paths:
print(f"(no *.concurrency.json in {RESULTS})")
return
rows = sorted((row(p) for p in paths), key=lambda r: r.get("_max", -1), reverse=True)
print(f"# Concurrency-limits — {len(rows)} model(s) ({RESULTS})\n")
hdr = ("| Model | util | KV pool (tok) | window | **max concurrent** | "
"best agg t/s (@N) | per-agent t/s | recall@load | min free MB | note |")
sep = "|" + "---|" * 11
print(hdr)
print(sep)
for r in rows:
if r.get("_max", 0) < 0: # rejected / error
print(f"| {r['model']} | - | - | {fmt(r.get('window'))} | — | — | — | — | — | {r.get('note','')} |")
continue
kv = f"{r['kv_pool']:,}" if r.get("kv_pool") else "-"
agg = f"{fmt(r['agg'])}" + (f" (@{r['at_n']})" if r.get("at_n") else "")
print(f"| {r['model']} | {fmt(r['util'])} | {kv} | {fmt(r['window'])} | "
f"**{r['max_n']}** | {agg} | {fmt(r['per_agent'])} | {fmt(r['recall'])} | "
f"{fmt(r['min_free'])} | {fmt(r['note'],'')} |")
print("\n_max concurrent = largest N sustained at the full window before "
"saturation (memory floor / request error / KV wall-spread / throughput "
"plateau). Single-stream engines (llama.cpp --parallel 1, ds4) cap at ~1 "
"by design. recall@load uses gen=256 → unreliable for heavy-thinker "
"models (think-budget artifact, not concurrency corruption)._")
if __name__ == "__main__":
main()
{
"needles": [
{
"id": "canary",
"question": "What is the exact canary phrase for this knowledge base?",
"expected": "MARMOT-LANTERN-4471",
"bucket": "early",
"char_pos": 31686
},
{
"id": "cache_ttl",
"question": "What default TTL (in seconds) does the obsidian-cache library use?",
"expected": "6143",
"bucket": "early",
"char_pos": 95266
},
{
"id": "shards",
"question": "How many primary shards does the glacier_idx_7 index have?",
"expected": "11",
"bucket": "early",
"char_pos": 149429
},
{
"id": "slot",
"question": "What replication slot does reference design RD-0042 name?",
"expected": "slot_meadowlark_3",
"bucket": "early",
"char_pos": 232501
},
{
"id": "rate",
"question": "What per-minute rate limit does the quartz-relay gateway enforce?",
"expected": "2570",
"bucket": "early",
"char_pos": 277621
},
{
"id": "checkpoint",
"question": "Every how many iterations does Lighthouse.Beacon checkpoint?",
"expected": "1750",
"bucket": "early",
"char_pos": 321106
},
{
"id": "zone",
"question": "Which availability zone is cluster nimbus-4 pinned to?",
"expected": "zone-delta-2",
"bucket": "mid",
"char_pos": 420630
},
{
"id": "approver",
"question": "Which group must approve the falcon-ingest pipeline?",
"expected": "crew-emerald",
"bucket": "mid",
"char_pos": 439679
},
{
"id": "debounce",
"question": "By how many milliseconds does the RippleToggle component debounce input?",
"expected": "285",
"bucket": "mid",
"char_pos": 496367
},
{
"id": "queue_alarm",
"question": "At what queue depth does worker-lynx-7 alarm?",
"expected": "64000",
"bucket": "mid",
"char_pos": 583853
},
{
"id": "collation",
"question": "What collation was the archive_v4 dataset migrated to?",
"expected": "ascii_quail_nx",
"bucket": "mid",
"char_pos": 613618
},
{
"id": "index_ms",
"question": "To what duration (ms) did idx_lattice_open cut the report query?",
"expected": "47",
"bucket": "mid",
"char_pos": 678769
},
{
"id": "maxlen",
"question": "What MAXLEN cap is set on the pulse:events stream?",
"expected": "2500000",
"bucket": "late",
"char_pos": 731111
},
{
"id": "analyzer",
"question": "What max_gram value does the prism_edge analyzer use?",
"expected": "21",
"bucket": "late",
"char_pos": 791607
},
{
"id": "toolchain",
"question": "What toolchain release does the emberforge runtime pin to?",
"expected": "33",
"bucket": "late",
"char_pos": 868580
},
{
"id": "machine",
"question": "What host class runs the demo search hot tier?",
"expected": "granite-highmem-19",
"bucket": "late",
"char_pos": 904762
},
{
"id": "job_queue",
"question": "On which queue is RebuildAtlasJob enqueued?",
"expected": "slow",
"bucket": "late",
"char_pos": 963782
},
{
"id": "sla",
"question": "What is the p99 latency target (ms) for the aurora-read path?",
"expected": "312",
"bucket": "late",
"char_pos": 1025935
}
]
}
#!/usr/bin/env python3
"""Concurrency benchmark at a model's FULL context window. Two modes:
SATURATION RAMP (default) — find the MAX concurrent sessions the KV pool
sustains: uncapped, saturation-driven ramp N = 1, 2, 4, 8, +8 … (staggered
starts, memory-floor gated). Reports max_sustained_n and why it saturated.
LEVEL SWEEP (--levels 1,2,3,4) — run EVERY listed level regardless of
saturation, recording per level: aggregate + per-request t/s, wall spread
(queueing), recall under load, memory low-water, and vLLM KV usage from
/metrics. Use --gen 1000 here: at 200k+ the prefill serializes on the GPU, so a
tiny gen budget hides the only thing that actually batches (decode).
python3 bench_concurrency.py --model <served-id> --url http://localhost:80
python3 bench_concurrency.py --model <served-id> --levels 1,2,3,4 --gen 1000
Design (grounded on a no-swap unified-memory box — see BENCHMARK.md):
- Test context = the model's full window (max_model_len from /v1/models minus
the gen budget). Eligibility gate: reject windows < --context-floor.
- Distinct full-window prompts per worker (unique marker from byte 0) so each
holds INDEPENDENT KV — prefix caching would collapse them into one prefill
and fake the concurrency. Per worker a unique needle -> recall under load.
- COLD ramp from N=1 upward, staggered, NEVER a jump-to-knee burst: firing tens
of full-window prefills as one synchronized spike trips the kernel OOM-killer
on a no-swap pool.
- Memory telemetry is REQUIRED (local /proc or a memwatch feed) — the memory
floor is this harness's safety model; it refuses to run without it.
- Prompt sizing self-calibrates against the server's /tokenize (fixes the
chars-per-token mismatch that overflowed non-default tokenizers), falling
back to the CPT estimate with the caller expected to leave window margin.
"""
import argparse
import json
import os
import re
import sys
import threading
import time
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import benchlib
CPT = benchlib.DEFAULT_CPT # fallback chars/token when /tokenize is unavailable
def _haystack():
with open(os.path.join(HERE, "prompt.txt")) as fh:
return fh.read()
def calibrate_cpt(url, model, hay):
"""Measure this model's chars/token on a haystack sample via /tokenize.
Returns (cpt, source): the measured ratio, or the default estimate."""
sample = hay[:40000]
n = benchlib.server_tokenize(url, model, sample)
if n:
return round(len(sample) / n, 3), "tokenize"
return CPT, "estimate"
def build_prompt(i, target_tokens, hay, cpt):
"""One DISTINCT full-window prompt for worker i, with a unique needle.
- Unique leading marker (worker id + salt) → defeats prefix-cache sharing,
so each session holds its own KV block.
- A `MAGIC_<i>` needle injected mid-haystack → per-session recall check.
"""
target_chars = int(target_tokens * cpt)
if len(hay) < target_chars:
hay = (hay * (target_chars // len(hay) + 1))
hay = hay[:target_chars]
needle_val = 100000 + i * 7919 # distinct per worker
needle = f"\n> NOTE MAGIC_{i}: the secret code for session {i} is {needle_val}.\n"
mid = len(hay) // 2
hay = hay[:mid] + needle + hay[mid:]
marker = f"SESSION-{i} SALT-{i * 2654435761 & 0xFFFFFFFF}\n" # unique from byte 0
question = (f"\n\nQuestion: what is the exact secret code for session {i} "
f"(the value of MAGIC_{i})? Answer with just the number.")
return marker + hay + question, str(needle_val)
def kv_usage(url):
"""vLLM /metrics gpu_cache_usage_perc (0..1) — None for other engines."""
root = benchlib.api_base(url)[:-3].rstrip("/")
try:
req = urllib.request.Request(root + "/metrics", headers={"User-Agent": benchlib.UA})
with urllib.request.urlopen(req, timeout=10) as r:
text = r.read().decode("utf-8", "replace")
vals = [float(m.group(1)) for m in
re.finditer(r'gpu_cache_usage_perc(?:{[^}]*})?\s+([0-9.eE+-]+)', text)]
return max(vals) if vals else None
except Exception:
return None
def fire(url, model, prompts_vals, gen, timeout, source, stagger=0.1, track_kv=False):
"""Fire len(prompts_vals) requests concurrently; return per-request results,
wall, min available memory (MiB), and (if tracked) max KV usage during the burst.
`stagger`: seconds between successive thread starts, so N concurrent
full-window prefills are admitted in sequence rather than as one
synchronized activation spike (the burst that OOM-kills a no-swap box)."""
results = [None] * len(prompts_vals)
def worker(k):
prompt, val = prompts_vals[k]
r = benchlib.call(url, model, prompt, gen, 0.6, timeout)
r["needle_ok"] = (val in ((r.get("content") or "") + (r.get("reasoning_content") or "")))
results[k] = r
threads = [threading.Thread(target=worker, args=(k,)) for k in range(len(prompts_vals))]
sampler = benchlib.MemSampler(source, interval=0.25)
kv_max = [None]
stop_flag = threading.Event()
def kv_poll():
while not stop_flag.is_set():
v = kv_usage(url)
if v is not None and (kv_max[0] is None or v > kv_max[0]):
kv_max[0] = v
stop_flag.wait(2)
kv_thread = threading.Thread(target=kv_poll, daemon=True) if track_kv else None
t0 = time.perf_counter()
sampler.start()
if kv_thread:
kv_thread.start()
for t in threads:
t.start()
if stagger:
time.sleep(stagger)
for t in threads:
t.join()
stop_flag.set()
mem = sampler.stop()
wall = time.perf_counter() - t0
return results, wall, mem["min_mem_available_mb"], kv_max[0]
def summarize(results, wall, n, min_free_mb=None, kv_usage_max=None):
ok = [r for r in results if r and r.get("http_status") == 200]
errs = [r for r in results if r and r.get("http_status") != 200]
comp = sum((r["usage"].get("completion_tokens") or 0) for r in ok)
per_req = [r["overall_tok_per_s"] for r in ok if r.get("overall_tok_per_s")]
walls = [r["wall_s"] for r in ok]
recall = sum(1 for r in ok if r.get("needle_ok"))
return {
"n": n,
"n_success": len(ok),
"n_error": len(errs),
"wall_s": round(wall, 2),
"min_free_mb": min_free_mb, # no-swap headroom low-water mark for this step
"kv_usage_max": kv_usage_max, # vLLM gpu_cache_usage_perc peak (None elsewhere)
"aggregate_tok_per_s": round(comp / wall, 2) if wall > 0 else None,
"per_req_tok_per_s_mean": round(sum(per_req) / len(per_req), 2) if per_req else None,
"per_req_wall_min": min(walls) if walls else None,
"per_req_wall_max": max(walls) if walls else None,
# queueing signal: waves of requests waiting for KV → wall spread widens
"wall_spread": round(max(walls) / min(walls), 2) if walls and min(walls) > 0 else None,
"recall": f"{recall}/{len(ok)}" if ok else "0/0",
"sample_error": (errs[0].get("error") if errs else None),
}
def saturated(cur, prev, mem_floor_mb=0):
"""Saturation = memory floor breached, requests failed, KV-queueing
wall-spread, or aggregate plateau. Memory is checked FIRST: on a no-swap
box a breached floor is the hard ceiling (any further step risks the
OOM-killer)."""
if mem_floor_mb and cur.get("min_free_mb") is not None and cur["min_free_mb"] < mem_floor_mb:
return True, f"memory floor breached (min {cur['min_free_mb']} MB < {mem_floor_mb} MB free)"
if cur["n_error"] > 0:
return True, "requests errored (KV/scheduler refused)"
if (cur.get("wall_spread") or 1) > 1.5:
return True, f"wall spread {cur['wall_spread']}x (queued in waves)"
if prev and prev.get("aggregate_tok_per_s") and cur.get("aggregate_tok_per_s"):
if cur["aggregate_tok_per_s"] <= prev["aggregate_tok_per_s"] * 1.05:
return True, "aggregate plateaued (<5% gain)"
return False, ""
def ramp_schedule(seed_knee):
"""1, 2, 4, 8, then +8 — extends past the predicted knee (uncapped)."""
base = [1, 2, 4, 8]
n = 16
top = max(seed_knee + 16, 24) # always probe a couple steps past the prediction
while n <= top:
base.append(n)
n += 8
return base
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model", required=True)
ap.add_argument("--url", default="http://localhost:80")
ap.add_argument("--max-model-len", type=int, default=0)
ap.add_argument("--kv-pool-tokens", type=int, default=0,
help="GPU KV cache size (tokens) from the vLLM boot log; seeds the knee")
ap.add_argument("--context-floor", type=int, default=180000)
ap.add_argument("--test-context", type=int, default=0,
help="fixed per-session context in TOKENS to test at; 0 = full window "
"(default). Use 333000 for the cross-model 333k concurrency point.")
ap.add_argument("--levels", default="",
help="comma-separated Ns for the LEVEL SWEEP mode (e.g. 1,2,3,4); "
"runs every level, no early stop except the memory floor")
ap.add_argument("--gen", type=int, default=256)
ap.add_argument("--timeout", type=int, default=3600)
ap.add_argument("--mem-floor-mb", type=int, default=3000,
help="stop before/when the serving host's MemAvailable drops below this "
"(no-swap OOM-killer protection; 0 disables — NOT recommended)")
ap.add_argument("--stagger", type=float, default=0.1,
help="seconds between concurrent request starts (de-synchronize prefill spikes)")
ap.add_argument("--memwatch-feed", help="memwatch JSONL path for remote runs ($MEMWATCH_FEED)")
ap.add_argument("--probe", action="store_true",
help="fire ONE full-window request, print PROBE_OK/PROBE_FAIL, exit "
"(used by the util-ceiling search to catch prefill-OOM)")
ap.add_argument("--out", default="")
a = ap.parse_args()
# SAFETY: concurrent full-window prefills without memory telemetry can
# OOM-kill a no-swap box — refuse rather than fly blind. (--probe is a
# single request and exempt.)
source = benchlib.MemSource.choose(a.url, feed=a.memwatch_feed)
if source.mode == "none" and not a.probe:
print("[fatal] no memory telemetry for the serving host: run this on the host, "
"or point --memwatch-feed/$MEMWATCH_FEED at its memwatch JSONL "
"(see BENCHMARK.md — ramps refuse to run without telemetry)")
raise SystemExit(2)
# window from /v1/models unless overridden
mml = a.max_model_len
if not mml:
try:
with urllib.request.urlopen(benchlib.api_base(a.url) + "/models", timeout=30) as r:
mml = (json.loads(r.read())["data"][0].get("max_model_len") or 0)
except Exception as e:
print(f"[fatal] could not read max_model_len: {e}")
mml = 0
out = {"model": a.model, "max_model_len": mml, "context_floor": a.context_floor,
"kv_pool_tokens": a.kv_pool_tokens, "gen": a.gen,
"mem_telemetry": source.mode}
# eligibility gate
if not mml or mml < a.context_floor:
out["rejected"] = f"window {mml} < {a.context_floor}"
print(f"[reject] {a.model}: {out['rejected']}")
if a.out:
json.dump(out, open(a.out, "w"), indent=2)
return
test_ctx = mml - a.gen # full window, minus gen budget
if a.test_context: # fixed per-session context (e.g. 333k, comparable)
if a.test_context > mml - a.gen:
print(f"[cap] --test-context {a.test_context} > window {mml}-gen{a.gen}; "
f"capping to {mml - a.gen}", flush=True)
else:
test_ctx = a.test_context
hay = _haystack()
cpt, cpt_src = calibrate_cpt(a.url, a.model, hay)
out["cpt"] = {"chars_per_token": cpt, "source": cpt_src}
print(f"[{a.model}] cpt={cpt} ({cpt_src})", flush=True)
if a.probe: # util-ceiling search: 1 full-window request
prompt, val = build_prompt(0, test_ctx, hay, cpt)
r = benchlib.call(a.url, a.model, prompt, a.gen, 0.6, a.timeout)
if r.get("http_status") == 200:
got = val in ((r.get("content") or "") + (r.get("reasoning_content") or ""))
print(f"PROBE_OK tok/s={r.get('overall_tok_per_s')} recall={'1' if got else '0'}")
return
print(f"PROBE_FAIL status={r.get('http_status')} {r.get('error', '')}")
raise SystemExit(1)
# ---------------------------------------------------------------- LEVEL SWEEP
if a.levels:
levels = [int(x) for x in a.levels.split(",") if x.strip()]
out["sweep"] = []
print(f"[{a.model}] SWEEP levels={levels} window={mml} test_ctx={test_ctx} "
f"gen={a.gen} mem_floor={a.mem_floor_mb}MB", flush=True)
prompts = [build_prompt(i, test_ctx, hay, cpt) for i in range(max(levels))]
for n in levels:
free = source.available_mb()
if a.mem_floor_mb and free is not None and free < a.mem_floor_mb:
out["sweep_halt"] = f"pre-level memory floor ({free} MB < {a.mem_floor_mb} MB) before N={n}"
print(f"[halt] before N={n}: only {free} MB free — stop", flush=True)
break
res, wall, minfree, kvmax = fire(a.url, a.model, prompts[:n], a.gen,
a.timeout, source, stagger=a.stagger, track_kv=True)
s = summarize(res, wall, n, minfree, kvmax)
out["sweep"].append(s)
print(f" sweep N={n}: agg={s['aggregate_tok_per_s']} t/s per-req={s['per_req_tok_per_s_mean']} "
f"spread={s['wall_spread']} recall={s['recall']} err={s['n_error']} "
f"min_free={minfree}MB kv_max={kvmax}", flush=True)
if a.out:
os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
json.dump(out, open(a.out, "w"), indent=2)
print(f"\nresult -> {a.out}")
return
# ---------------------------------------------------------------- SATURATION RAMP
knee = (a.kv_pool_tokens // mml) if a.kv_pool_tokens else 8
print(f"[{a.model}] window={mml} test_ctx={test_ctx} predicted_knee={knee} "
f"mem_floor={a.mem_floor_mb}MB stagger={a.stagger}s", flush=True)
# pre-build distinct prompts up to a generous ceiling (knee + slack)
ceiling = max(knee + 16, 24)
print(f"[build] {ceiling} distinct {test_ctx}-token prompts ...", flush=True)
prompts = [build_prompt(i, test_ctx, hay, cpt) for i in range(ceiling)]
# COLD ramp from N=1 upward — NEVER a jump-to-knee burst. Each step fires n
# concurrent full-window prompts (staggered); a pre-step memory gate refuses
# to fire if headroom is already thin, and saturated() (memory floor first)
# stops at the real ceiling. The knee only bounds the schedule.
out["ramp"] = []
prev = None
sat_at = None
for n in ramp_schedule(knee):
if n > ceiling:
break
free = source.available_mb()
if a.mem_floor_mb and free is not None and free < a.mem_floor_mb:
sat_at = {"first_saturated_n": n, "max_sustained_n": prev["n"] if prev else 0,
"reason": f"pre-step memory floor ({free} MB < {a.mem_floor_mb} MB) — did not fire N={n}"}
print(f"[halt] before N={n}: only {free} MB free (< {a.mem_floor_mb}) — stop", flush=True)
break
res, wall, minfree, _ = fire(a.url, a.model, prompts[:n], a.gen, a.timeout,
source, stagger=a.stagger)
s = summarize(res, wall, n, minfree)
out["ramp"].append(s)
print(f" ramp N={n}: agg={s['aggregate_tok_per_s']} t/s per-agent={s['per_req_tok_per_s_mean']} "
f"wall_spread={s['wall_spread']} recall={s['recall']} err={s['n_error']} min_free={minfree}MB", flush=True)
is_sat, why = saturated(s, prev, a.mem_floor_mb)
if is_sat:
sat_at = {"first_saturated_n": n, "max_sustained_n": prev["n"] if prev else n, "reason": why}
print(f"[saturated] at N={n}: {why} -> max sustained ~{sat_at['max_sustained_n']}", flush=True)
break
prev = s
out["saturation"] = sat_at or {"note": "ramp exhausted ceiling without saturating",
"max_sustained_n": prev["n"] if prev else knee}
if a.out:
os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
json.dump(out, open(a.out, "w"), indent=2)
print(f"\nresult -> {a.out}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""W9 usable-context ceiling — baseline-relative bisection with repeated attempts.
Finds the largest context (in TOKENS) at which a model still RELIABLY holds the quality
it delivers at a 200k baseline. Not needle retrieval — real degradation tasks, judge-scored.
Design (2026-07-22, operator-driven):
- PROMPT: one DETERMINISTIC algorithm interleaves the answer-relevant "must-know" source
(priority-1) at evenly-spaced positions among lower-value fill, growing the fill to hit
any token target up to 1M. The needles stay put; only the haystack between them grows —
a true long-context attention test, not a front-loaded one. Deterministic => byte-identical
prompt for a given (task,length) => the repeats hit the server prefix/prompt cache.
- SUBJECT temp > 0 (these tasks are creative) => each repeat is a distinct sample.
- REPEATS + WARM CACHE: each length is tested `repeats` (5) times. Execution is TASK-MAJOR
(a task's 5 attempts run consecutively) because at these context sizes the KV cache holds
~one prompt: attempt 1 is a cold prefill, attempts 2-5 are WARM (prefill reused, decode
only). Attempt-means are reconstructed across tasks afterwards.
- VERDICT: a length HOLDS iff its MEAN over the `repeats` attempts >= threshold, where
threshold = baseline_mean - --tolerance (7). The repeats exist to average temp>0 variance
into that mean (operator: "the attempts are there to introduce a mean value"); the
per-attempt pass-count (n_pass) is recorded but INFORMATIONAL only, not the verdict.
- JUDGE is env-driven (JUDGE_MODEL/JUDGE_TEMPERATURE/JUDGE_THINKING_OFF) — W9 uses
DeepSeek-V4-Flash at temperature 0 with thinking disabled (deterministic scoring).
- BORDERS-ONLY (DEFAULT): measure quality at just the two borders of the context range —
the `baseline` (min) and the needle/window ceiling `hi` (max) — and report the degradation
SIZE between them (drop in points and %). No probes in between: we report HOW MUCH it
degrades across the full range, not WHERE. `--bisect` re-enables the transition-point
search over (baseline, hi] (ceiling = largest holding length; resolution-bounded).
- RESUMABLE: every run is persisted; a killed multi-day run continues where it stopped.
Usage:
bench_context_curve.py --model M --url http://localhost --ceiling 1000213 \
[--baseline 200000] [--tolerance 7] [--repeats 5] [--bisect [--resolution 100000]] \
[--chunks 8] [--temperature 0.7] [--tasks 1,2,3,4,5] [--max-tokens 12000] \
[--timeout 14400] --out <json>
"""
import argparse, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import benchlib
import bench_degradation as bd
# W9 judges ALL five tasks (keyword scoring rewards verbosity, which the ranking penalizes).
# 0-3 rubrics for tasks 1/2/3 (4/5 already carry judge rubrics in bench_degradation).
RUBRICS = {
'1': ("Score 0-3: 0 = wrong or no answer; 1 = names Pluginator::Group but vague; "
"2 = correct purpose plus some key methods; 3 = correct purpose, key methods, "
"AND how Group interacts with pluginator (autodetect/finder/extendable)."),
'2': ("Score 0-3: 0 = wrong; 1 = mentions Autodetect only, no chain; 2 = partial "
"delegation chain; 3 = correct order Autodetect -> Finder -> formatted_finder "
"-> extension/load with the key methods named."),
'3': ("Score 0-3: 0 = wrong or no fix; 1 = identifies the on(:post_edited) handler but "
"no correct fix; 2 = adds a guardian/category permission check with minor gaps; "
"3 = correct fix: a guardian can_see?/category-permission check in the handler, "
"shown as before->after code."),
}
_FILL_GLOBS = ['**/*.rb', '**/*.ex', '**/*.exs', '**/*.vue', '**/*.js', '**/*.ts']
_ALL_PROJECTS = ['pluginator', 'discourse', 'phoenix', 'chatwoot']
def load_sources(task_def):
"""Read the corpus once for a task: priority-1 'must-know' text (the needles) +
a large fill pool (same-project priority-2, then other projects) + the question."""
p1_files = bd.collect_files(task_def['priority1_project'], task_def['priority1_globs'])
p1_text, _ = bd.read_files(p1_files)
p2_files = [f for f in bd.collect_files(task_def['priority1_project'],
task_def.get('priority2_globs', []))
if f not in p1_files]
p2_text, _ = bd.read_files(p2_files)
other = []
for proj in _ALL_PROJECTS:
if proj == task_def['priority1_project']:
continue
other.extend(bd.collect_files(proj, _FILL_GLOBS))
other_text, _ = bd.read_files(other)
q_text = "\n\n" + "=" * 60 + "\n\n" + task_def['question']
return {'p1': p1_text, 'fill': p2_text + other_text, 'q': q_text}
def _split(s, k):
"""Split a string into k roughly-equal contiguous parts (deterministic)."""
if k <= 1:
return [s]
step = len(s) // k
parts = [s[i * step:(i + 1) * step] for i in range(k - 1)]
parts.append(s[(k - 1) * step:])
return parts
def build_interleaved(src, target_tok, cpt, chunks=8):
"""Deterministic prompt of ~target_tok tokens: the must-know p1 split into `chunks`
needles placed at evenly-spaced positions among the fill, question last. Growing the
target grows only the fill BETWEEN the fixed needles. Deterministic => identical bytes
for a given target => warm prefix-cache across the repeats."""
p1, fill, q = src['p1'], src['fill'], src['q']
target_chars = int(target_tok * cpt)
fill_budget = target_chars - len(p1) - len(q)
if fill_budget <= 0: # target smaller than the needles themselves
body = p1[:max(0, target_chars - len(q))]
return body + q, int((len(body) + len(q)) / cpt)
fill_slice = fill[:fill_budget] # deterministic head of the fill pool
needles = _split(p1, max(1, chunks))
gaps = _split(fill_slice, len(needles) + 1)
out = [gaps[0]]
for i, needle in enumerate(needles):
out.append(needle)
out.append(gaps[i + 1])
out.append(q)
prompt = ''.join(out)
return prompt, int(len(prompt) / cpt)
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--model", required=True)
ap.add_argument("--url", default="http://localhost")
ap.add_argument("--ceiling", type=int, required=True, help="needle-retrieval ceiling (hi anchor)")
ap.add_argument("--baseline", type=int, default=200000,
help="reference length; its mean sets this model's per-model bar")
ap.add_argument("--tolerance", type=float, default=7.0,
help="a length holds iff attempt-mean >= baseline_mean - tolerance (0-100 pts)")
ap.add_argument("--resolution", type=int, default=100000, help="[--bisect only] stop when hi-lo <= this")
ap.add_argument("--bisect", action="store_true",
help="locate WHERE quality drops (transition point) by bisecting (baseline, hi]. "
"DEFAULT OFF: measure only the two borders (baseline + hi) and report the "
"degradation SIZE between them — no in-between probes.")
ap.add_argument("--repeats", type=int, default=5, help="attempts per length (task-major, warm)")
ap.add_argument("--min-pass", type=int, default=4,
help="INFORMATIONAL only (n_pass display); verdict uses the mean, not this")
ap.add_argument("--chunks", type=int, default=8, help="needle chunks interleaved through the fill")
ap.add_argument("--temperature", type=float, default=0.7, help="SUBJECT sampling temperature")
ap.add_argument("--tasks", default="1,2,3,4,5")
ap.add_argument("--cpt", type=float, default=4.1)
ap.add_argument("--max-tokens", type=int, default=12000)
ap.add_argument("--timeout", type=int, default=14400)
ap.add_argument("--out", required=True)
a = ap.parse_args()
tasks = [t.strip() for t in a.tasks.split(",") if t.strip()]
sources, cpts = {}, {}
for tk in tasks:
sources[tk] = load_sources(bd.TASK_DEFS[tk])
sample = (sources[tk]['p1'] + sources[tk]['fill'])[:200000]
n = benchlib.server_tokenize(a.url, a.model, sample)
cpts[tk] = round(len(sample) / n, 3) if n else a.cpt
out = {"model": a.model, "method": "baseline-relative-repeats",
"baseline_length": a.baseline, "tolerance": a.tolerance, "resolution": a.resolution,
"repeats": a.repeats, "min_pass": a.min_pass, "temperature": a.temperature,
"needle_ceiling": a.ceiling, "baseline_mean": None, "threshold": None,
"lengths": {}, "path": [], "ceiling_tokens": None}
memo = {}
if os.path.exists(a.out):
try:
prev = json.load(open(a.out))
out["lengths"] = prev.get("lengths", {})
out["path"] = prev.get("path", [])
for k, v in out["lengths"].items():
memo[int(k)] = v
if memo:
print(f"[resume] {len(memo)} lengths on disk", flush=True)
except Exception:
pass
def flush():
with open(a.out, 'w') as f:
json.dump(out, f, indent=2)
def run_length(length, threshold):
"""Task-major: each task's `repeats` attempts run consecutively (attempt 1 cold,
rest warm). Reconstruct attempt-means; a length holds iff its MEAN >= threshold
(n_pass over attempts is recorded but informational, not the verdict).
threshold=None for the baseline (compute mean only, no pass/fail)."""
length = int(length)
rec = memo.get(length)
if rec is None:
rec = {"target": length, "tasks": {tk: [] for tk in tasks}}
memo[length] = rec
out["lengths"][str(length)] = rec
out["path"].append(length)
# resume fast-path: a fully-run length recomputes its verdict from disk (no server calls)
need_run = not (rec.get("final") and
all(len(rec["tasks"].get(tk, [])) >= a.repeats for tk in tasks))
if need_run:
# build the (deterministic) prompt per task once; reused across that task's repeats
prompts = {}
for tk in tasks:
p, _ = build_interleaved(sources[tk], length, cpts[tk], a.chunks)
p, real = bd.clamp_to_tokens(a.url, a.model, p, sources[tk]['q'], length)
prompts[tk] = (p, real)
rec["actual_tokens"] = round(sum(r for _, r in prompts.values()) / len(prompts))
# task-major run loop (resumable: only runs the attempts not yet on disk)
for tk in tasks if need_run else []:
task = bd.TASK_DEFS[tk]
rubric = task.get('judge_rubric') or RUBRICS.get(tk)
prompt, _ = prompts[tk]
runs = rec["tasks"].setdefault(tk, [])
while len(runs) < a.repeats:
warm = len(runs) > 0
res = bd.call_model(a.url, a.model, prompt, a.max_tokens, a.temperature, a.timeout)
jscore = None
if rubric and not res.get('error'):
jscore, _ = benchlib.judge_score_json(
task['question'],
(res.get('content', '') + ' ' + res.get('reasoning_content', '')), rubric)
kw = round(bd.score_answer(res.get('content', ''), res.get('reasoning_content', ''),
task.get('answer_key', [])), 3) if task.get('answer_key') else None
sc = round(jscore / 3.0 * 100, 1) if jscore is not None else (
round(kw * 100, 1) if kw is not None else 0.0)
runs.append({"attempt": len(runs) + 1, "warm": warm, "score": sc,
"judge_0_3": jscore, "keyword": kw,
"wall_s": res.get('wall_s'), "error": res.get('error'),
"resp_tokens": res.get('completion_tokens'), # total generated (reasoning+answer) = verbosity cost
"answer_chars": len(res.get('content') or ''),
"reasoning_chars": len(res.get('reasoning_content') or '')})
flush()
print(f"[{length} t{tk} a{len(runs)}{'W' if warm else 'C'}] "
f"score={sc} wall={res.get('wall_s')}s err={res.get('error')}", flush=True)
# reconstruct attempt-means (i-th run of each task = one sample of the length-mean)
ams = []
for i in range(a.repeats):
scores = [rec["tasks"][tk][i]["score"] for tk in tasks]
ams.append(round(sum(scores) / len(scores), 1))
rec["attempt_means"] = ams
rec["mean"] = round(sum(ams) / len(ams), 1)
cold = [rec["tasks"][tk][0]["wall_s"] for tk in tasks if rec["tasks"][tk] and rec["tasks"][tk][0].get("wall_s")]
warm = [r["wall_s"] for tk in tasks for r in rec["tasks"][tk][1:] if r.get("wall_s")]
rec["cold_wall_avg"] = round(sum(cold) / len(cold), 1) if cold else None
rec["warm_wall_avg"] = round(sum(warm) / len(warm), 1) if warm else None
rtoks = [r["resp_tokens"] for tk in tasks for r in rec["tasks"][tk] if r.get("resp_tokens")]
rec["resp_tokens_avg"] = round(sum(rtoks) / len(rtoks)) if rtoks else None # avg generated length
if threshold is None:
rec["pass"] = None
else:
rec["n_pass"] = sum(1 for m in ams if m >= threshold) # informational only
rec["pass"] = rec["mean"] >= threshold # VERDICT = mean vs threshold
rec["final"] = True
flush()
verdict = "baseline" if threshold is None else ("HOLD" if rec["pass"] else "DROP")
print(f"[length {length}] actual={rec['actual_tokens']} attempt_means={ams} "
f"mean={rec['mean']} {verdict} n_pass={rec.get('n_pass')} "
f"cold~{rec['cold_wall_avg']}s warm~{rec['warm_wall_avg']}s", flush=True)
return rec
# ── baseline sets the per-model bar ──────────────────────────────────────
base = run_length(a.baseline, None)
baseline_mean = base["mean"]
threshold = round(baseline_mean - a.tolerance, 4)
out["baseline_mean"] = baseline_mean
out["threshold"] = threshold
flush()
def holds(length):
return run_length(length, threshold)["pass"]
hi = a.ceiling
# ── single-border edge case: needle ceiling at/below the baseline ─────────
if a.baseline >= hi:
out["max_border"] = {"target": a.baseline, "tokens": base["actual_tokens"],
"mean": baseline_mean, "holds": True}
out["degradation_drop_pts"] = 0.0
out["degradation_drop_pct"] = 0.0
out["ceiling_tokens"] = base["actual_tokens"]
out["borders_only"] = True
out["done"] = True
out["note"] = f"baseline {a.baseline} >= needle ceiling {hi} — single border (baseline only)"
flush(); print(f"\n-> borders-only: baseline>=ceiling, drop 0 {a.out}", flush=True); return
# ── the max border: quality at the needle/window ceiling ─────────────────
top = run_length(hi, threshold)
top_mean = top["mean"]
drop_pts = round(baseline_mean - top_mean, 1)
drop_pct = round((baseline_mean - top_mean) / baseline_mean * 100, 1) if baseline_mean else None
out["max_border"] = {"target": hi, "tokens": top["actual_tokens"],
"mean": top_mean, "holds": bool(top["pass"])}
out["degradation_drop_pts"] = drop_pts
out["degradation_drop_pct"] = drop_pct
# ── DEFAULT: borders only — report the min→max degradation SIZE, stop ─────
if not a.bisect:
# ceiling_tokens keeps its "largest length that still holds" meaning:
# holds at max ⇒ ceiling = max; drops ⇒ transition not localized ⇒ null.
out["ceiling_tokens"] = top["actual_tokens"] if top["pass"] else None
out["borders_only"] = True
out["done"] = True
out["note"] = (f"borders-only: {a.baseline}-tok baseline mean {baseline_mean} -> max {hi} "
f"(act {top['actual_tokens']}) mean {top_mean}; degradation {drop_pts} pts "
f"({drop_pct}%); max {'HOLDS' if top['pass'] else 'DROPS'} vs thr {threshold}. "
f"Transition point NOT measured (bisection off; pass --bisect to locate it).")
flush(); print(f"\n-> borders-only: {a.baseline}={baseline_mean} max({hi})={top_mean} "
f"drop={drop_pts}pts/{drop_pct}% {a.out}", flush=True); return
# ── --bisect: locate WHERE quality drops (transition point) ──────────────
if top["pass"]:
out["ceiling_tokens"] = top["actual_tokens"]
out["done"] = True
out["note"] = (f"quality at the needle ceiling (mean {top_mean}) holds within "
f"{a.tolerance} of the {a.baseline}-tok baseline ({baseline_mean}, thr {threshold}) "
f"— usable to the retrieval limit")
flush(); print(f"\n-> ceiling {out['ceiling_tokens']} (=needle ceiling, quality holds) {a.out}", flush=True); return
lo, hib = a.baseline, hi # lo holds by construction; hib does not
while hib - lo > a.resolution:
mid = (lo + hib) // 2
if holds(mid):
lo = mid
else:
hib = mid
out["ceiling_tokens"] = memo[lo]["actual_tokens"]
out["ceiling_target"] = lo
out["done"] = True
out["note"] = (f"largest length within {a.resolution} of where quality drops >{a.tolerance} "
f"below the {a.baseline}-tok baseline (mean {baseline_mean}, thr {threshold})")
flush()
print(f"\n-> ceiling {out['ceiling_tokens']} tokens (bracket lo={lo} hi={hib}, "
f"baseline {baseline_mean} thr {threshold}) {a.out}", flush=True)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""Degradation curve filler engine — builds prompts at increasing context levels
from real project sources, fires them at a model API, and scores answers.
Usage:
python3 bench_degradation.py --model <name> --task <1|2|3> --url <base> \\
--max-ctx <tokens> --out <json> --cpt <chars_per_token>
Context fill hierarchy (per level):
1. Task-relevant source files (the buggy file + direct deps)
2. Same-project tests, docs, remaining files
3. Other-project files (proportional sampling)
Levels: minimal (priority 1 only) → +20% × (max − min) → … → max.
Corpus: SHA-pinned OSS checkouts provisioned by setup_corpus.sh into
$GX10_CORPUS (default ~/.cache/gx10-bench-corpus); a drifting checkout changes
prompt content and breaks level-to-level comparability. Tasks 4-5 are scored by
the independent judge (benchlib, env JUDGE_MODEL/JUDGE_BASE_URL) against their
rubric; the keyword score is kept as a secondary column.
"""
import argparse, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import benchlib
# ── Task definitions ──────────────────────────────────────────────────────
CORPUS = os.environ.get('GX10_CORPUS', os.path.expanduser('~/.cache/gx10-bench-corpus'))
_CORPUS_SUBPATH = {'pluginator': 'lib', 'discourse': '', 'phoenix': 'lib',
'chatwoot': 'app/javascript'}
_LEGACY_ROOTS = {
'pluginator': '~/projects/rvm/pluginator/lib',
'discourse': '~/projects/oss/discourse',
'phoenix': '~/projects/oss/phoenix/lib',
'chatwoot': '~/projects/oss/chatwoot/app/javascript',
}
def project_root(proj):
"""Pinned corpus first (setup_corpus.sh), workstation checkout as fallback."""
c = os.path.join(CORPUS, proj, _CORPUS_SUBPATH[proj]).rstrip('/')
if os.path.isdir(c):
return c
return os.path.expanduser(_LEGACY_ROOTS[proj])
PROJECT_ROOTS = {p: project_root(p) for p in _LEGACY_ROOTS}
TASK_DEFS = {
'1': {
'name': 'pluginator-retrieval',
'question': (
"You are reviewing the pluginator Ruby gem. "
"Read the source code above and answer:\n\n"
"What does the `Pluginator::Group` class do? "
"Describe its purpose, key methods, and how it interacts with the rest of pluginator.\n\n"
"Answer concisely in 2-3 sentences."
),
'answer_key': [
"group", "extendable", "autodetect", "finder",
"plugin", "name", "type", "version",
],
'priority1_project': 'pluginator',
'priority1_globs': ['**/*.rb'],
'priority2_globs': [],
},
'2': {
'name': 'pluginator-cross-trace',
'question': (
"You are reviewing the pluginator Ruby gem. "
"Read the source code above and trace the delegation chain:\n\n"
"Starting from `Pluginator::Autodetect`, how does a plugin get discovered "
"and loaded? Trace through Finder → formatted_finder → the extension system.\n\n"
"List the key classes and methods in order of invocation."
),
'answer_key': [
"autodetect", "finder", "formatted", "extension",
"load", "plugin", "group", "configured",
],
'priority1_project': 'pluginator',
'priority1_globs': ['**/*.rb'],
'priority2_globs': [],
},
'3': {
'name': 'discourse-bug-fix',
'question': (
"You are reviewing Discourse (Ruby on Rails forum). "
"The file `plugins/discourse-presence/plugin.rb` has a security bug: "
"it leaks editor identities via the wiki edit presence API to unauthorized users. "
"The issue is in the `on(:post_edited)` handler — it does not check whether "
"the requesting user has permission to view the topic's category.\n\n"
"Read the source code above and propose a fix. "
"Show the exact code change (before → after) with file path and line context."
),
'answer_key': [
"guardian", "can_see?", "category", "security",
"group", "permission", "topic", "allowed_group",
"intersect", "authorized", "presence", "edit",
],
'priority1_project': 'discourse',
'priority1_globs': [
'plugins/discourse-presence/plugin.rb',
'app/controllers/presence_controller.rb',
'lib/guardian.rb',
'app/models/group.rb',
'app/models/category.rb',
],
'priority2_globs': [
'plugins/discourse-presence/**/*.rb',
'app/**/*.rb',
'lib/**/*.rb',
],
},
'4': {
'name': 'cross-stack-vue-phoenix',
'question': (
"You are a full-stack engineer reviewing a real-time chat application. "
"The frontend is Vue (Chatwoot widget) and the backend is Elixir/Phoenix.\n\n"
"Given the Chatwoot Vue component `ChatMessage.vue` and the Phoenix channel "
"infrastructure, design the Phoenix channel + broadcast system that would "
"deliver real-time messages to this component. Include:\n\n"
"1. The Phoenix channel module (module name, join logic, message handling)\n"
"2. The broadcast call from the controller/service\n"
"3. How the Vue component subscribes and handles incoming messages\n\n"
"Show key code snippets for each layer."
),
'answer_key': [
"channel", "broadcast", "socket", "subscribe",
"handle_in", "handle_out", "push", "message",
],
'judge_rubric': (
"Score 0-3:\n"
"0: No relevant answer or completely wrong\n"
"1: Mentions Phoenix channel concept but no concrete code\n"
"2: Correct channel + broadcast design, minor omissions\n"
"3: Complete cross-stack design with correct code for all 3 layers"
),
'priority1_project': 'chatwoot',
'priority1_globs': [
'widget/components/ChatMessage.vue',
'widget/components/AgentMessage.vue',
'widget/store/modules/conversation/actions.js',
],
'priority2_globs': [
'widget/**/*.vue',
'widget/**/*.js',
],
},
'5': {
'name': 'hot-reload-plugin-design',
'question': (
"You are designing a plugin system for the pluginator Ruby gem "
"(a lightweight dependency injection / plugin framework).\n\n"
"Propose a hot-reload system: plugins should be reloadable at runtime "
"without restarting the host application. Consider:\n\n"
"1. How to detect file changes (which mechanism?)\n"
"2. How to safely unload and reload a plugin module\n"
"3. How to handle plugin state and dependencies during reload\n"
"4. What changes to pluginator's existing Autodetect/Group API are needed\n\n"
"Show example code for the key interfaces."
),
'answer_key': [
"reload", "unload", "watch", "file",
"remove_const", "load", "module", "state",
],
'judge_rubric': (
"Score 0-3:\n"
"0: No relevant design or completely wrong\n"
"1: Mentions file watching or reload but no concrete integration\n"
"2: Workable design integrated with pluginator APIs, minor gaps\n"
"3: Complete design with correct Ruby code, state handling, and API changes"
),
'priority1_project': 'pluginator',
'priority1_globs': ['**/*.rb'],
'priority2_globs': [],
},
}
# ── File collection ───────────────────────────────────────────────────────
def collect_files(project, globs, root=None):
"""Collect file contents matching globs within a project root."""
import glob as gmod
if root is None:
root = PROJECT_ROOTS.get(project, '.')
files = []
for pat in globs:
full_pat = os.path.join(root, pat)
for f in sorted(gmod.glob(full_pat, recursive=True)):
if os.path.isfile(f):
files.append(f)
return files
def read_files(files):
"""Read and concatenate file contents with path headers."""
chunks = []
total_chars = 0
for f in files:
try:
content = open(f).read()
header = f"\n\n── {os.path.relpath(f)} ──\n\n"
chunks.append(header + content)
total_chars += len(header) + len(content)
except Exception:
continue
return ''.join(chunks), total_chars
# ── Level builder ─────────────────────────────────────────────────────────
def build_levels(task_def, max_ctx, cpt=4.1):
"""Build prompt files at each degradation level.
Returns list of (level_name, total_tokens_estimate, prompt_text).
"""
# Collect priority 1 files (task-relevant)
p1_files = collect_files(
task_def['priority1_project'],
task_def['priority1_globs'],
)
p1_text, p1_chars = read_files(p1_files)
p1_tokens = int(p1_chars / cpt)
# Collect priority 2 files (same-project context)
p2_files = collect_files(
task_def['priority1_project'],
task_def.get('priority2_globs', []),
)
# Deduplicate
p2_files = [f for f in p2_files if f not in p1_files]
p2_text, p2_chars = read_files(p2_files)
p2_tokens = int(p2_chars / cpt)
# Collect priority 3 files (other projects)
other_files = {}
for proj in ['pluginator', 'discourse', 'phoenix', 'chatwoot']:
if proj == task_def['priority1_project']:
continue
other_files[proj] = collect_files(proj, ['**/*.rb', '**/*.ex', '**/*.vue', '**/*.js'])
# Flatten other-project files with proportional interleaving
all_other = []
for proj, flist in sorted(other_files.items()):
all_other.extend(flist)
other_text, other_chars = read_files(all_other)
other_tokens = int(other_chars / cpt)
# Question text
q_text = "\n\n" + "="*60 + "\n\n" + task_def['question']
q_tokens = int(len(q_text) / cpt)
# Minimal context = priority 1 only + question
minimal_tokens = p1_tokens + q_tokens
# Build levels
levels = []
gap = max_ctx - minimal_tokens
if gap <= 0:
# Minimal already exceeds max — just one level
levels.append(('minimal', minimal_tokens, p1_text + q_text))
return levels
for pct in [0, 20, 40, 60, 80, 100]:
target = minimal_tokens + int(gap * pct / 100)
if target > max_ctx:
target = max_ctx
parts = [p1_text]
budget = target - minimal_tokens
if budget <= 0:
pass # No room for anything beyond priority 1
elif budget <= p2_tokens:
# Fill with priority 2, truncated
trunc = int(len(p2_text) * budget / p2_tokens) if p2_tokens else 0
parts.append(p2_text[:trunc])
else:
# All priority 2, then priority 3
parts.append(p2_text)
remaining = budget - p2_tokens
if remaining > 0 and other_tokens > 0:
trunc = int(len(other_text) * remaining / other_tokens)
parts.append(other_text[:trunc])
parts.append(q_text)
prompt = ''.join(parts)
est_tokens = int(len(prompt) / cpt)
label = f"L{pct//20}" if pct > 0 else "L0"
levels.append((label, est_tokens, prompt))
return levels
# ── API call ──────────────────────────────────────────────────────────────
def call_model(url, model, prompt, max_tokens, temperature, timeout):
r = benchlib.call(url, model, prompt, max_tokens, temperature, timeout)
usage = r.get("usage") or {}
return {
"wall_s": r["wall_s"],
"http_status": r["http_status"],
"error": r.get("error"),
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
"content": r["content"],
"reasoning_content": r["reasoning_content"],
"finish_reason": r["finish_reason"],
}
def score_answer(content, reasoning, answer_key):
"""Simple keyword-match scoring. Checks both content and reasoning_content.
Returns fraction of keywords found."""
text = (content + ' ' + reasoning).lower()
hits = sum(1 for kw in answer_key if kw.lower() in text)
return hits / len(answer_key) if answer_key else 0
def clamp_to_tokens(url, model, prompt, tail, target):
"""Guarantee the SERVER-measured prompt fits `target` tokens, trimming the body
(everything before `tail`, i.e. the question) when the CPT estimate undershot the
real tokenizer. Without this the top level overshoots its target on token-dense
code (Elixir/Vue/JS tokenize denser than the pluginator sample the CPT is calibrated
on) and vLLM rejects it (prompt+max_tokens > max_model_len → HTTP 400, a bogus 0).
Returns (prompt, server_tokens). No-op (returns est via None) if /tokenize is
unavailable — never trims blind on a CPT guess."""
n = benchlib.server_tokenize(url, model, prompt)
if not n or n <= target:
return prompt, n
body = prompt[: len(prompt) - len(tail)] if tail else prompt
# Proportional shrink toward target (3% under to absorb chat-template wrapping),
# then verify against the real tokenizer and iterate — char/token density is not
# uniform, so one proportional cut is a starting point, not the answer.
for _ in range(6):
keep = int(len(body) * (target / n) * 0.97)
if keep >= len(body) or keep <= 0:
break
body = body[:keep]
prompt = body + tail
n2 = benchlib.server_tokenize(url, model, prompt)
if not n2:
break
n = n2
if n <= target:
break
return prompt, n
# ── Main ──────────────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--model", required=True)
ap.add_argument("--task", required=True, choices=['1', '2', '3', '4', '5'])
ap.add_argument("--url", default="http://localhost")
ap.add_argument("--max-ctx", type=int, required=True)
ap.add_argument("--cpt", type=float, default=4.1)
ap.add_argument("--out", required=True)
ap.add_argument("--max-tokens", type=int, default=8192)
ap.add_argument("--timeout", type=int, default=3600)
a = ap.parse_args()
task = TASK_DEFS[a.task]
levels = build_levels(task, a.max_ctx, a.cpt)
# Calibrate chars/token against the server's tokenizer and rebuild if it
# deviates — the fixed estimate overflowed non-default tokenizers at the
# top levels (every ✗ cell at 100% was this, not model failure).
sample = levels[-1][2][:200000]
n = benchlib.server_tokenize(a.url, a.model, sample)
if n:
real_cpt = len(sample) / n
if abs(real_cpt - a.cpt) / a.cpt > 0.02:
print(f"[cpt] calibrated {a.cpt} -> {real_cpt:.3f} (server tokenizer); rebuilding levels")
a.cpt = round(real_cpt, 3)
levels = build_levels(task, a.max_ctx, a.cpt)
# Hard token-ceiling clamp: CPT calibration samples only the head, so a token-dense
# tail still overshoots the top level's target -> vLLM 400 (bogus 0). Measure every
# level against the server tokenizer and trim the body until it truly fits max_ctx
# (max_ctx already reserves >max_tokens headroom below the window). No-op for the
# small lower levels; only the top level(s) get trimmed. Skipped if /tokenize is down.
q_text = "\n\n" + "=" * 60 + "\n\n" + task['question']
clamped = []
for label, est_tok, prompt in levels:
p, real_n = clamp_to_tokens(a.url, a.model, prompt, q_text, a.max_ctx)
if real_n and real_n != est_tok:
print(f"[clamp] {label}: est {est_tok} -> server {real_n} tok (max_ctx {a.max_ctx})")
clamped.append((label, real_n or est_tok, p))
levels = clamped
results = {
"model": a.model,
"task": task['name'],
"max_ctx": a.max_ctx,
"cpt": a.cpt,
"levels": [],
}
for label, est_tok, prompt in levels:
sys.stdout.write(f" [{label}] ~{est_tok} tok ... ")
sys.stdout.flush()
res = call_model(a.url, a.model, prompt, a.max_tokens, 0.0, a.timeout)
# keyword match is always computed (secondary signal / legacy comparability)
kw = None
if task.get('answer_key'):
kw = round(score_answer(res.get('content', ''),
res.get('reasoning_content', ''),
task['answer_key']), 3)
res['keyword_score'] = kw
# the judge is PRIMARY whenever the task carries a rubric (design tasks):
# keyword-matching design answers rewards verbosity, not correctness
if task.get('judge_rubric') and not res.get('error'):
jscore, jreason = benchlib.judge_score_json(
task['question'],
(res.get('content', '') + ' ' + res.get('reasoning_content', '')),
task['judge_rubric'])
res['judge_score'] = jscore
res['judge_reasoning'] = jreason
res['score'] = jscore if jscore is not None else kw
res['score_kind'] = 'judge' if jscore is not None else 'keyword-fallback'
else:
res['score'] = kw
res['score_kind'] = 'keyword'
res['level'] = label
res['est_tokens'] = est_tok
results['levels'].append(res)
shown = res['score'] if res['score'] is not None else 0
status = (f"{res['score_kind']}={shown} kw={kw} "
f"wall={res.get('wall_s',0):.0f}s tok={res.get('prompt_tokens',0)}")
if res.get('error'):
status += f" ERR={str(res['error'])[:80]}"
print(status, flush=True)
with open(a.out, 'w') as f:
json.dump(results, f, indent=2)
print(f"\n{a.out}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""HTTP performance + recall harness for one RUNNING OpenAI-compatible server.
Stdlib only (via benchlib). Connects over HTTP exactly as a client would; the
endpoint is a parameter (--url), so the client can run on the serving host or a
remote machine (memory telemetry then comes from a memwatch feed — see
BENCHMARK.md). Emits ONE result JSON covering:
warmup one tiny generation to page weights / capture CUDA graphs
decode probe short prompt, long generation -> decode tok/s (low context).
Run first; its rate is reused to derive prefill below.
needle levels 18-needle recall, descending INPUT size until 18/18:
start = min(--start-input, max_model_len - output_budget),
step down --step (default 0.9 = -10%) on any non-18/18,
down to --floor-input. Each level measures wall, memory
low-water, overall tok/s, and a DERIVED prefill rate (pp):
prefill_s = wall - completion/decode_rate
pp_tok_per_s = prompt_tokens / prefill_s
plus a reasoned diagnosis when a level is not 18/18.
warm repeat re-run the FIRST passing level (KV/prefix cache warm) and
report the cold->warm wall / pp delta.
concurrency N concurrent medium requests -> aggregate vs single tok/s.
Needle values are RANDOMIZED per run (gen_needle.py); each level embeds its
answer key + seed in the result JSON. Prompt sizing asks the server's /tokenize
when available, else falls back to the CPT estimate.
Why derived, not streamed: urllib buffers SSE, so a streamed TTFT lands near
end-of-wall and is meaningless. completion/wall and a decode-rate-derived prefill
are robust. pp is labeled approximate (decode rate is measured at low context).
python3 bench_levels.py --model <served-id> --max-model-len 333000 \
--out ~/.cache/gx10-bench-results/model.perf.json
"""
import argparse
import json
import os
import sys
import threading
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import benchlib
import gen_needle
import score_answers
def derive_pp(res, decode_rate):
"""Derive prefill throughput: prefill_s = wall - completion/decode_rate."""
ptok = res["usage"].get("prompt_tokens")
ctok = res["usage"].get("completion_tokens")
if not (ptok and ctok and decode_rate and res["wall_s"]):
return None, None
prefill_s = res["wall_s"] - ctok / decode_rate
if prefill_s <= 0.05:
return None, round(prefill_s, 2) # decode-dominated; prefill too small to isolate
return round(ptok / prefill_s, 1), round(prefill_s, 2)
# ----------------------------------------------------------------------------- needle
def timed_needle(args, target_tokens, decode_rate, seed=None):
prompt, run_key = gen_needle.build(target_tokens, args.cpt, seed=seed)
est_prompt = benchlib.server_tokenize(args.url, args.model, prompt) \
or int(target_tokens * 1.04) # tokenizer-exact when the server offers it
max_tokens = min(args.output_budget, max(256, args.max_model_len - est_prompt - 512))
sampler = benchlib.MemSampler(args.mem_source)
sampler.start()
res = benchlib.call(args.url, args.model, prompt, max_tokens, 0.0, args.timeout)
mem = sampler.stop()
verdict = score_answers.score(res, run_key["needles"])
pp, prefill_s = derive_pp(res, decode_rate)
ok = verdict["needle_score"] == 1.0 and not verdict["degenerate"]
lvl = {
"target_tokens": target_tokens,
"needle_seed": run_key["seed"],
"needle_key": run_key,
"est_prompt_tokens": est_prompt,
"max_tokens_allowed": max_tokens,
"prompt_tokens": res["usage"].get("prompt_tokens"),
"completion_tokens": res["usage"].get("completion_tokens"),
"http_status": res["http_status"],
"error": res["error"],
"finish_reason": res["finish_reason"],
"wall_s": res["wall_s"],
"prefill_s": prefill_s,
"pp_tok_per_s": pp,
"overall_tok_per_s": res["overall_tok_per_s"],
"needle_recall": verdict["needle_recall"],
"needle_score": verdict["needle_score"],
"by_bucket": verdict["by_bucket"],
"degenerate": verdict["degenerate"],
"answers_parsed": verdict["answers_parsed"],
"part_b_present": verdict["part_b_present"],
"memory": mem,
"diagnosis": None if ok else diagnose(res, verdict),
}
return lvl, ok
def diagnose(res, v):
st = res.get("http_status")
if st and (st < 0 or st >= 400):
return f"server error/capacity (http {st}): {(res.get('error') or '')[:160]}"
if v["degenerate"]:
return "degenerate output (BOS/repeat spam) at this context length"
fr = res.get("finish_reason")
if fr == "length" and v["answers_parsed"] == 0:
return "output budget exhausted before any answer (heavy reasoner never closed <think>)"
if fr == "length":
return f"output truncated at max_tokens with only {v['answers_parsed']}/18 answers emitted"
return (f"recall {v['needle_recall']} (by_bucket {v['by_bucket']}) — "
f"context retention miss, not a capacity/format problem")
# ----------------------------------------------------------------------------- phases
def phase_warmup(args):
t0 = time.perf_counter()
res = benchlib.call(args.url, args.model, "Reply with the single word: ready.", 16, 0.0, 300)
return {"wall_s": round(time.perf_counter() - t0, 2), "http_status": res["http_status"],
"steady_memory": benchlib.mem_snapshot(args.mem_source)}
def phase_decode_probe(args):
sampler = benchlib.MemSampler(args.mem_source)
sampler.start()
res = benchlib.call(args.url, args.model,
"Write a detailed 1000-word essay on the history of the Ruby programming language.",
1000, 0.6, 600)
mem = sampler.stop()
ctok = res["usage"].get("completion_tokens")
rate = round(ctok / res["wall_s"], 2) if (ctok and res["wall_s"] > 0) else None
return {"decode_tok_per_s": rate, "completion_tokens": ctok, "wall_s": res["wall_s"],
"finish_reason": res["finish_reason"], "memory": mem}
def phase_levels(args, decode_rate):
start = min(args.start_input, args.max_model_len - args.output_budget)
note = None
if args.start_input > args.max_model_len - args.output_budget:
note = (f"requested {args.start_input} input + {args.output_budget} output = "
f"{args.start_input + args.output_budget} > max_model_len {args.max_model_len}; "
f"max feasible input at {args.output_budget} output = {args.max_model_len - args.output_budget}")
levels = []
passed = None
target = start
n = 0
while target >= args.floor_input and n < args.max_levels:
lvl, ok = timed_needle(args, target, decode_rate)
levels.append(lvl)
status = "OK" if ok else ("MISS: " + str(lvl["diagnosis"]))[:90]
print(f" [level {n}] in~{target} prompt_tok={lvl['prompt_tokens']} "
f"recall={lvl['needle_recall']} pp~{lvl['pp_tok_per_s']} dec={lvl['overall_tok_per_s']} "
f"wall={lvl['wall_s']}s minMem={lvl['memory']['min_mem_available_mb']}MB {status}",
flush=True)
if ok:
passed = lvl
break
target = int(target * args.step)
n += 1
return {"start_note": note, "levels": levels,
"max_passing_input_tokens": (passed or {}).get("prompt_tokens"),
"max_passing_target": (passed or {}).get("target_tokens"),
"max_passing_seed": (passed or {}).get("needle_seed")}
def phase_warm_repeat(args, passed_target, passed_seed, decode_rate):
if not passed_target:
return None
# same seed -> byte-identical prompt -> a true prefix-cache hit
lvl, _ = timed_needle(args, passed_target, decode_rate, seed=passed_seed)
return {"target_tokens": passed_target, "warm": {
"pp_tok_per_s": lvl["pp_tok_per_s"], "prefill_s": lvl["prefill_s"],
"overall_tok_per_s": lvl["overall_tok_per_s"], "wall_s": lvl["wall_s"],
"needle_recall": lvl["needle_recall"]}}
def phase_concurrency(args):
if args.concurrency < 2:
return None
base = ("Summarize the key design tradeoffs between an LRU and an LFU cache, "
"then give a short Ruby sketch of each. Request id ")
results = [None] * args.concurrency
sampler = benchlib.MemSampler(args.mem_source)
sampler.start()
def worker(i):
results[i] = benchlib.call(args.url, args.model, base + str(i) + " " + ("x" * i), 400, 0.6, 600)
threads = [threading.Thread(target=worker, args=(i,)) for i in range(args.concurrency)]
t0 = time.perf_counter()
for t in threads:
t.start()
for t in threads:
t.join()
wall = time.perf_counter() - t0
mem = sampler.stop()
ok = [r for r in results if r and r["http_status"] == 200]
total_completion = sum((r["usage"].get("completion_tokens") or 0) for r in ok)
per_req_tps = [r["overall_tok_per_s"] for r in ok if r["overall_tok_per_s"]]
single = max(per_req_tps) if per_req_tps else None
aggregate = round(total_completion / wall, 2) if wall > 0 else None
return {
"concurrency": args.concurrency, "n_success": len(ok), "wall_s": round(wall, 2),
"aggregate_tok_per_s": aggregate, "best_single_req_tok_per_s": single,
"speedup_vs_single": round(aggregate / single, 2) if (aggregate and single) else None,
"per_req_wall_s": [r["wall_s"] for r in ok], "memory": mem,
}
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--model", required=True)
ap.add_argument("--max-model-len", type=int, required=True)
ap.add_argument("--url", default="http://localhost/v1")
ap.add_argument("--out", required=True)
ap.add_argument("--output-budget", type=int, default=64000)
ap.add_argument("--start-input", type=int, default=333000)
ap.add_argument("--floor-input", type=int, default=16000)
ap.add_argument("--step", type=float, default=0.9)
ap.add_argument("--max-levels", type=int, default=10)
ap.add_argument("--concurrency", type=int, default=4)
ap.add_argument("--cpt", type=float, default=gen_needle.DEFAULT_CPT)
ap.add_argument("--timeout", type=int, default=3600)
ap.add_argument("--skip-concurrency", action="store_true")
ap.add_argument("--memwatch-feed", help="memwatch JSONL path for remote runs ($MEMWATCH_FEED)")
a = ap.parse_args()
if a.max_model_len <= a.output_budget:
raise SystemExit(f"max_model_len {a.max_model_len} <= output budget "
f"{a.output_budget} — pass the model's real window")
a.mem_source = benchlib.MemSource.choose(a.url, feed=a.memwatch_feed)
out = {"model": a.model, "suite": "v4", "max_model_len": a.max_model_len,
"output_budget": a.output_budget, "url": a.url,
"mem_telemetry": a.mem_source.mode,
"boot_memory": benchlib.mem_snapshot(a.mem_source)}
print(f"[{a.model}] warmup ...", flush=True)
out["warmup"] = phase_warmup(a)
print(f"[{a.model}] decode probe ...", flush=True)
out["decode_probe"] = phase_decode_probe(a)
decode_rate = out["decode_probe"]["decode_tok_per_s"]
print(f"[{a.model}] needle levels (start {min(a.start_input, a.max_model_len - a.output_budget)}, "
f"decode_rate~{decode_rate}) ...", flush=True)
out["needle"] = phase_levels(a, decode_rate)
print(f"[{a.model}] warm-cache repeat ...", flush=True)
out["needle_warm"] = phase_warm_repeat(a, out["needle"]["max_passing_target"],
out["needle"]["max_passing_seed"], decode_rate)
if not a.skip_concurrency:
print(f"[{a.model}] concurrency x{a.concurrency} ...", flush=True)
out["concurrency"] = phase_concurrency(a)
walls = [(out.get("warmup") or {}).get("wall_s"),
(out.get("decode_probe") or {}).get("wall_s"),
((out.get("needle_warm") or {}).get("warm") or {}).get("wall_s"),
(out.get("concurrency") or {}).get("wall_s"),
*[l.get("wall_s") for l in (out.get("needle") or {}).get("levels") or []]]
out["total_wall_s"] = round(sum(w for w in walls if w), 1)
with open(a.out, "w") as fh:
json.dump(out, fh, indent=2)
print(f"[{a.model}] DONE -> {a.out}", flush=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Shared primitives for the benchmark suite. Stdlib only.
- call(): one /chat/completions request -> normalized result dict
- server_tokenize(): exact token count via the server's tokenize endpoint
- MemSource: where memory telemetry comes from (local /proc, a memwatch
feed file, or none). Metrics are endpoint-agnostic; SAFETY
GATING IS NOT — ramps must refuse to run without telemetry.
- MemSampler: background min-available / max-gpu tracker over a MemSource
- judge_call() / judge_score_json() / judge_preflight():
the independent judge (env JUDGE_MODEL / JUDGE_BASE_URL /
OPENCODE_API_KEY; never printed)
- results_dir(): persistent result location (never /tmp)
"""
import json
import os
import re
import subprocess
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
UA = "GX10-Benchmark/1.0"
DEFAULT_CPT = 4.1 # chars/token fallback (Qwen tokenizer) when /tokenize is unavailable
JUDGE_MODEL = os.environ.get("JUDGE_MODEL", "kimi-k2.7-code")
JUDGE_BASE = os.environ.get("JUDGE_BASE_URL", "https://opencode.ai/zen/go/v1")
# Judge sampling knobs (env-driven so the harness stays model-agnostic):
# JUDGE_TEMPERATURE unset => omit temperature (kimi 400s on any value);
# "0" => deterministic score (DeepSeek/MiMo, thinking off).
# JUDGE_THINKING_OFF "1" => send {"thinking":{"type":"disabled"}} so temperature
# actually takes effect (DeepSeek/MiMo silently ignore temp=0
# while thinking mode is on, which is their default).
JUDGE_TEMPERATURE = os.environ.get("JUDGE_TEMPERATURE")
JUDGE_THINKING_OFF = os.environ.get("JUDGE_THINKING_OFF", "").lower() in ("1", "true", "yes")
def api_base(url):
"""Normalize an endpoint to its OpenAI base (…/v1)."""
u = url.rstrip("/")
return u if u.endswith("/v1") else u + "/v1"
def _post(base, path, body, timeout, headers=None):
req = urllib.request.Request(
base.rstrip("/") + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", "User-Agent": UA, **(headers or {})})
t0 = time.perf_counter()
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.load(r), time.perf_counter() - t0
def call(url, model, content=None, max_tokens=1000, temperature=0.0, timeout=3600,
messages=None):
"""One non-streaming /chat/completions call. Never raises — errors land in
the result's http_status/error fields (a 400 at the window boundary is data,
not a crash). Non-streaming is deliberate: urllib buffers SSE, so a streamed
TTFT is meaningless — completion/wall is the robust measure (see BENCHMARK.md)."""
body = {"model": model,
"messages": messages or [{"role": "user", "content": content}],
"max_tokens": max_tokens, "temperature": temperature, "stream": False}
t0 = time.perf_counter()
status, error, data = 200, None, None
try:
data, _ = _post(api_base(url), "/chat/completions", body, timeout)
except urllib.error.HTTPError as e:
status = e.code
try:
error = e.read().decode("utf-8", "replace")[:500]
except Exception:
error = str(e)
except Exception as e: # noqa: BLE001
status = -1
error = repr(e)[:500]
wall = time.perf_counter() - t0
content_out, reasoning, finish, usage = "", "", None, {}
if data:
choice = (data.get("choices") or [{}])[0]
msg = choice.get("message") or {}
content_out = msg.get("content") or ""
reasoning = msg.get("reasoning_content") or msg.get("reasoning") or ""
finish = choice.get("finish_reason")
usage = data.get("usage") or {}
comp = usage.get("completion_tokens")
return {
"model": model,
"http_status": status,
"error": error,
"wall_s": round(wall, 2),
"finish_reason": finish,
"usage": usage,
# answers may land in either field depending on the reasoning parser
"content": content_out,
"reasoning_content": reasoning,
"overall_tok_per_s": round(comp / wall, 2) if comp and wall > 0 else None,
}
def server_tokenize(url, model, text, timeout=300):
"""Exact token count from the server: vLLM `POST /tokenize {model, prompt}`,
llama.cpp `POST /tokenize {content}` (both live at the server root, not /v1).
Returns None when unsupported — callers fall back to a CPT estimate."""
root = api_base(url)[:-3].rstrip("/") # strip the /v1
attempts = (
({"model": model, "prompt": text},
lambda d: d.get("count") or len(d.get("tokens") or [])),
({"content": text},
lambda d: len(d.get("tokens") or [])),
)
for body, extract in attempts:
try:
data, _ = _post(root, "/tokenize", body, timeout)
n = extract(data)
if n:
return int(n)
except Exception:
continue
return None
# ------------------------------------------------------------------ memory telemetry
def _local_mem_available_mb():
try:
with open("/proc/meminfo") as fh:
for line in fh:
if line.startswith("MemAvailable:"):
return int(line.split()[1]) // 1024
except Exception:
pass
return None
def _local_gpu_used_mb():
try:
out = subprocess.run(
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
capture_output=True, text=True, timeout=5)
vals = [int(v) for v in out.stdout.split() if v.strip().isdigit()]
return max(vals) if vals else None
except Exception:
return None
class MemSource:
"""Memory telemetry for the machine SERVING the model.
mode 'local' — this process runs on the serving host: read /proc + nvidia-smi.
mode 'feed' — remote client: read the last line of a memwatch JSONL feed
(each line: {"t": epoch, "mem_available_mb": N, "gpu_used_mb": M}).
A stale feed (older than max_age_s) reads as None.
mode 'none' — no telemetry. Plain benches report memory N/A; RAMPS MUST
REFUSE TO RUN (their safety model is the memory floor).
"""
def __init__(self, mode, feed_path=None, max_age_s=30):
assert mode in ("local", "feed", "none")
self.mode = mode
self.feed_path = feed_path
self.max_age_s = max_age_s
@classmethod
def choose(cls, url, feed=None):
"""local when the endpoint is this host; else a feed if given (or
$MEMWATCH_FEED); else none."""
feed = feed or os.environ.get("MEMWATCH_FEED")
host = urllib.parse.urlparse(api_base(url)).hostname or ""
if host in ("localhost", "127.0.0.1", "::1"):
return cls("local")
if feed and os.path.exists(feed):
return cls("feed", feed_path=feed)
return cls("none")
def _feed_last(self):
try:
with open(self.feed_path, "rb") as fh:
try:
fh.seek(-4096, os.SEEK_END)
except OSError:
fh.seek(0)
lines = fh.read().decode(errors="replace").strip().splitlines()
rec = json.loads(lines[-1])
if time.time() - rec.get("t", 0) > self.max_age_s:
return None
return rec
except Exception:
return None
def available_mb(self):
if self.mode == "local":
return _local_mem_available_mb()
if self.mode == "feed":
rec = self._feed_last()
return rec.get("mem_available_mb") if rec else None
return None
def gpu_used_mb(self):
if self.mode == "local":
return _local_gpu_used_mb()
if self.mode == "feed":
rec = self._feed_last()
return rec.get("gpu_used_mb") if rec else None
return None
class MemSampler(threading.Thread):
"""Track worst-case memory during a window: min MemAvailable + max GPU used."""
def __init__(self, source=None, interval=0.5):
super().__init__(daemon=True)
self.source = source or MemSource("local")
self.interval = interval
self._halt = threading.Event()
self.min_avail = None
self.max_gpu = None
def run(self):
while not self._halt.is_set():
avail = self.source.available_mb()
if avail is not None:
self.min_avail = avail if self.min_avail is None else min(self.min_avail, avail)
gpu = self.source.gpu_used_mb()
if gpu is not None:
self.max_gpu = gpu if self.max_gpu is None else max(self.max_gpu, gpu)
self._halt.wait(self.interval)
def stop(self):
self._halt.set()
self.join(timeout=3)
return {"min_mem_available_mb": self.min_avail, "max_gpu_used_mb": self.max_gpu}
def mem_snapshot(source):
"""One-off snapshot from a MemSource (schema matches the result JSONs)."""
return {"mem_available_mb": source.available_mb(), "gpu_used_mb": source.gpu_used_mb()}
# ------------------------------------------------------------------ judge
def judge_call(prompt, max_tokens=256, temperature=None, timeout=90, retries=2, backoff=15):
"""One judge /chat/completions call. Returns the content string; raises the
last error after `retries` retried attempts (429/400 back off first).
Sampling is env-driven (JUDGE_TEMPERATURE / JUDGE_THINKING_OFF, see top of file):
an explicit `temperature` arg wins; else JUDGE_TEMPERATURE; else the field is
omitted (kimi 400s on any temperature → provider default). JUDGE_THINKING_OFF
disables thinking so temperature=0 is honored (DeepSeek/MiMo)."""
key = os.environ.get("OPENCODE_API_KEY", "")
if not key:
raise RuntimeError("no OPENCODE_API_KEY in env")
if temperature is None and JUDGE_TEMPERATURE not in (None, ""):
temperature = float(JUDGE_TEMPERATURE)
body = {"model": JUDGE_MODEL,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens}
if temperature is not None:
body["temperature"] = temperature
if JUDGE_THINKING_OFF:
body["thinking"] = {"type": "disabled"}
last = None
for attempt in range(retries + 1):
try:
data, _ = _post(JUDGE_BASE, "/chat/completions", body, timeout,
headers={"Authorization": f"Bearer {key}"})
msg = data["choices"][0].get("message", {})
return msg.get("content") or msg.get("reasoning_content") or ""
except Exception as e: # noqa: BLE001 — judge errors are data, not crashes
last = e
if attempt < retries:
time.sleep(backoff * (attempt + 1))
raise last
def extract_json(text):
"""Best-effort JSON object from model text: strips <think> blocks and markdown
fences (reasoning judges fence their verdict nondeterministically), then scans
for the last complete {...} via raw_decode (nested-safe). Returns dict|None."""
clean = re.sub(r"<think>.*?</think>", "", text or "", flags=re.S).strip()
fence = re.search(r"```(?:json)?\s*(.*?)```", clean, re.S)
if fence:
clean = fence.group(1).strip()
elif clean.startswith("```"): # unterminated fence (truncated reply)
clean = clean.split("\n", 1)[-1].strip()
try:
obj = json.loads(clean)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
pass
dec = json.JSONDecoder()
for m in reversed(list(re.finditer(r"\{", clean))):
try:
obj, _ = dec.raw_decode(clean[m.start():])
if isinstance(obj, dict):
return obj
except json.JSONDecodeError:
continue
return None
def judge_score_json(question, answer, rubric):
"""Score an answer 0-3 against a rubric. Returns (score|None, reasoning)."""
prompt = (
f"You are evaluating a coding assistant's answer. "
f"Question: {question}\n\n"
f"Answer: {answer[:8000]}\n\n"
f"Rubric:\n{rubric}\n\n"
f'Return ONLY a JSON object: {{"score": <0-3>, "reasoning": "<one sentence>"}}'
)
try:
content = judge_call(prompt)
except Exception as e: # noqa: BLE001
return None, str(e)[:200]
parsed = extract_json(content)
if isinstance(parsed, dict) and "score" in parsed:
return parsed.get("score", 0), parsed.get("reasoning", "")
m = re.search(r"\b([0-3])\b", content)
return (int(m.group(1)) if m else 0), content[:200]
def judge_preflight(n=3):
"""Validate the judge before a suite run: n trivial scoring calls must parse.
Returns (ok, info)."""
ok = 0
errs = []
for i in range(n):
score, why = judge_score_json(
"What is 2+2?", "4",
"Score 0-3:\n0: wrong\n3: exactly correct")
if score is not None:
ok += 1
else:
errs.append(why)
return ok == n, {"judge": JUDGE_MODEL, "base": JUDGE_BASE, "ok": ok, "of": n,
"errors": errs[:3]}
# ------------------------------------------------------------------ results
def results_dir():
"""Persistent result location — never /tmp (wiped on reboot; results were
lost to that once)."""
d = os.environ.get("RESULTS") or os.path.expanduser("~/.cache/gx10-bench-results")
os.makedirs(d, exist_ok=True)
return d

Benchmark suite

An HTTP benchmark suite for OpenAI-compatible LLM servers: long-context recall, prefill/decode/latency, concurrency, quality (instruction following, coding, tool use, multi-turn), and quality degradation as context fills. Everything runs over plain HTTP against a served endpoint — the way real clients use the model — and every script takes the endpoint as a parameter (--url), so the client can run on the serving host or any other machine.

Developed against a single-GPU 128 GB unified-memory box (NVIDIA GB10, no swap); the safety rules below call that host "the box". Results are recorded outside this package (for the gx10 deployment: the repo-root README.md; superseded results move to HISTORY.md). Result JSONs never live in this package — it is a driver, not a store.

Suite tiers

Tier When Contents Purpose
smoke every model, every bring-up needle @window · decode probe · warm repeat · IFEval-25 · tool/json sanity · multiturn sample catastrophic-failure detection: broken quant, runaway generation, no recall. Scores are pass/broken, not rankings.
gate promotion candidates IFEval-100 · HumanEval-157 (pass@1) · degradation tasks 1–5 at the common 250k cap (1–3 keyword, 4–5 judge) · BFCL tool-calling (offline AST) · full-window concurrency sweep the numbers promotions are decided on — ranked by R = Q/wall (see Suite version)
deep on demand MMLU-100 general-knowledge axis; informational, not a promotion criterion

Scores are recorded as k/N and rendered as %. Do not rank models on any metric with N < 50 — at small N the confidence intervals are wider than the differences being read (exact binomial: 3/10 → 95% CI [7%, 65%]). Small-N metrics only detect catastrophic failure.

Performance phases (bench_levels.py)

One running server; emits one *.perf.json.

  • warmup — one tiny generation to page weights / capture CUDA graphs.
  • decode probe — short prompt, 1000-token generation → clean output-token decode t/s at low context (the headline decode rate). Run first; its rate derives prefill below.
  • needle levels — 18-needle recall over descending input size until 18/18: start min(--start-input, max_model_len − output_budget), step --step (default 0.9, −10%) on any non-18/18, down to --floor-input. Per level: wall, memory low-water, overall_tok_per_s, and a derived prefill pp = prompt_tokens / (wall − completion/decode_rate).
  • warm repeat — re-run the first passing level with the KV/prefix cache hot; report cold and warm wall. A fully-cached warm prefill reads as a huge/instant pp — a cache-hit artifact, not a real prefill speed.
  • concurrency phase — N concurrent medium requests (default 4) → aggregate t/s and speedup vs single-stream, at the served config.

Why derived, not streamed: urllib buffers SSE, so a streamed TTFT lands near end-of-wall and is meaningless; non-streaming completion/wall plus a decode-rate-derived prefill is robust (pp is approximate — the decode rate is measured at low context).

Reading the numbers

  • Window = input + output. The served --max-model-len is the window; "input @18/18" is needle input only, with the output budget held in reserve.
  • decode-out t/s is the clean decode rate; the at-max-context overall rate is prefill-dominated — never read it as decode speed.
  • cold→warm wall is the real prefix-cache benefit, decode-floored (only prefill KV is cached): it measures repeated-request latency, not prefill alone. Cache misses read as warm ≈ cold.
  • Concurrency buys session capacity, not throughput. Long-context prefill is compute-bound on one GPU: aggregate t/s flattens past N≈2–4 while the pool may admit tens of sessions. Decode batching at low context is where the ×N gains come from.

18-needle recall (gen_needle.py + score_answers.py)

  • 18 needles = 17 > NOTE [id]: … lines + 1 canary prose sentence; 6 early / 6 mid / 6 late thirds of the haystack, snapped to blank-line boundaries between source files.
  • Haystack: prompt.txt, public Ruby-OSS source (~1.06 M chars). Size-agnostic — the generator tiles filler to any target input size.
  • Needle values are randomized per run (--seed reproduces a run). The needle format is public, the answer key of a given run is not — a fixed public key would let future models memorize it.
  • Sizing: ask the server's /tokenize endpoint when available; otherwise chars-per-token estimate (--cpt, default 4.1 — measured on the Qwen tokenizer; other tokenizers differ ~±1%, which matters at the window boundary — leave margin or supply the model's CPT). The server-reported prompt_tokens labels each level.
  • Scoring: normalized substring/exact match of A<n>: answers, searched in both content and reasoning_content. Part B (synthesis) is judge-scored separately and is not part of the 18/18. Degeneration (BOS-spam) is flagged.
  • Reasoning models need a ≥8k output budget or they run out mid-think and read as a false 0/18.

Degradation curve (bench_degradation.py)

Real-code tasks at 6 context levels (L0 = task-relevant files only, then +20% steps of the remaining window up to 100%). Context fill priority: task-relevant source → same- project files → other-project filler.

Tasks: (1) Ruby gem class comprehension, (2) Ruby delegation-chain trace, (3) Rails security-bug fix, (4) cross-stack Vue→Phoenix channel design, (5) hot-reload plugin design. Tasks 1–3 are keyword-scored (0–1); tasks 4–5 are judge-scored on a 0–3 rubric (keyword score kept as a secondary column).

  • Corpus: public OSS checkouts pinned by commit SHA (a drifting checkout changes prompt content and breaks level-to-level comparability). make corpus provisions them to ~/.cache/gx10-bench-corpus; PROJECT_ROOTS env overrides. Pinned SHAs are recorded next to the corpus target.
  • Contamination caveat: these projects are in every model's training data, so absolute L0 scores partly reflect memory. The shape of the curve across levels is the signal, not the absolute score.
  • L-boundary failures (HTTP 400 at 100%) are a sizing artifact, not model failure — see the CPT note above.

Concurrency (bench_concurrency.py)

Two modes:

  • Saturation ramp (default): at the model's full window, climb N = 1, 2, 4, 8, +8 … with staggered starts until saturation (error, memory floor, or queueing wall-spread). Reports max_sustained_n.
  • Level sweep (--levels 1,2,3,4): run every level regardless of saturation, with per-level aggregate t/s, per-request t/s, wall-spread, recall, memory low-water, and vLLM KV usage from /metrics. Use --gen 1000 — a tiny gen budget hides the only thing that batches (decode).

Safety rules (hard-learned; they are the harness, not advice):

  • Never fire a cold burst at the predicted knee. On a no-swap unified pool, tens of simultaneous full-window prefills spike activation memory past the OOM-killer line. Ramp from N=1, staggered.
  • Memory floor (--mem-floor-mb, default 3000): the bench samples available memory during every step and refuses to push past the floor. Ramps refuse to run without a memory-telemetry source (see below).
  • Distinct prompts per worker (unique leading marker): prefix caching would otherwise collapse concurrent prefills into one and fake the concurrency. Sanity-check that wall scales ~linearly with N.
  • Prompt sizing near the window needs margin (serve 333k → bench 320k): the CPT estimate overshoots ~0.6% at the boundary and the server 400s.

Quality suite (deepeval_full.py)

Two roles, never conflated:

  • SUBJECT — the served model under test; generates every answer.
  • JUDGE — an independent external model that only scores (deepeval's evaluation_model). Configured by env: JUDGE_MODEL + JUDGE_BASE_URL + key in .env (never committed, never printed). A preflight validates the judge with a handful of scoring calls before the suite runs; on failure it falls back to the configured fallback judge or runs the no-judge subset. The judge identity is recorded in every result JSON; scores from different judges are never comparable — one judge per fleet run.
    • Judge sampling is env-driven so the judge can be pinned deterministic where the provider allows it: JUDGE_TEMPERATURE (unset ⇒ temperature omitted / provider default; 0 ⇒ deterministic) and JUDGE_THINKING_OFF=1 (sends {"thinking":{"type":"disabled"}}). Some models silently ignore temperature=0 while thinking-mode is on (their default) and must have thinking disabled for temp-0 to take effect; others (Moonshot kimi) 400 on any temperature and can only run at provider default. Because a temp-0 judge is not comparable to a provider-default one, this is a scoring-semantics knob — changing it bumps the Suite version.

Groups: agentic (tool_correctness), multiturn (relevancy/completeness/retention/ role-adherence/GEval + turn-level RAG metrics), mcp, nonllm (exact/pattern/json), others (summarization, prompt-alignment, hallucination — a rate, lower is better). Trace-only metrics (argument-correctness, task-completion, plan-adherence, conversational-DAG) are not measurable over plain HTTP and are skipped.

Benchmarks (--bench): IFEval (n=100) and HumanEval (n=157, pass@1, functional-v2 scorer) — deepeval's HumanEvalTask enum carries 157 of the canonical 164 problems; the suite runs the full enum, so scores are comparable within the suite but not against published HumanEval-164 numbers. IFEval doubles as the quantization-damage canary — instruction following degrades 5–9+ points under quant damage that leaves MCQ accuracy flat. --deep adds MMLU (n=100), informational only. Reasoning-model adapter: answers arrive as prose/letters, <think> blocks are stripped, and the raw answer is coerced into the expected schema by type before scoring.

HumanEval functional-v2 scorer — the harness replaces the library's scorer, which defines the dataset's check() but never calls it (any completion that merely executes counts as a pass) and whose restricted-builtins sandbox rejects import / typing annotations (correct code scores 0). functional-v2 appends check(entry_point) and runs the asserts in a real interpreter (subprocess, full builtins + common stdlib imports, 15 s timeout) — official pass@1 semantics. Each result JSON tags "scorer": "functional-v2"; scores from the unpatched scorer are not comparable and must never share a table with these.

Operational rules

  • One model at a time; sequential only. One serving process, one benchmark client; no parallel models, tests, or downloads. Parallel runs poison each other's results.
  • Clean restart between models; wait for /v1/models before benching.
  • Silent OOM: on a no-swap box an OOM-kill leaves no traceback. Check docker inspect <ctr> --format '{{.State.OOMKilled}}' before blaming anything else.
  • Memory telemetry: metrics are endpoint-agnostic, safety gating is not. When the client runs on the serving host it samples /proc/meminfo directly; when remote, it reads the memwatch feed (a small sampler the orchestrator starts on the box, writing to the results dir). No telemetry → plain benches report memory N/A and ramps refuse to run.
  • Results land in a persistent box-local dir (~/.cache/gx10-bench-results — never /tmp, which is wiped on reboot, and never inside this package).
  • Wall time is a metric — record it per test; it captures the real cost of model switching + loading + inference.
  • Restore the standing default after a benchmark session.

Harness index

Script Measures / does
benchlib.py shared: HTTP call, memory telemetry source, tokenizer-aware sizing, scoring, judge client
gen_needle.py build a needle haystack at any input size (randomized values, --seed)
score_answers.py score a needle result (recall, degeneration, Part B extraction)
run_needle.py one-shot needle run + decode probe against a running server (make bench)
bench_levels.py the perf phases: warmup, decode probe, needle levels, warm repeat, concurrency phase
bench_degradation.py degradation curve (5 tasks × 6 levels)
bench_concurrency.py full-window saturation ramp / level sweep
deepeval_full.py quality suite + benchmarks (subject/judge separation)
memwatch.py box-side memory sampler feeding remote clients + ramps
run_fleet.sh serial fleet orchestrator: stop-all → start → health → perf → quality → stop; restores the default
aggregate.py / aggregate_concurrency.py roll result JSONs into ranked Markdown tables
bench_2bit.py / bench_mhc.py vq2-plugin in-process benches (see the vq2 appendix in README.md)

Suite version

Current: v5 (gate) · v5.1 (context-curve harness) · v5.2 (W9 degradation in the ranking). Past suite definitions and their results live in the deployment repo's HISTORY.md. Results from different suite versions never share a table.

v5.2 (2026-07-24) — W9 degradation folded into the ranking:

  • The Degradation term of Q is now the W9 context-curve baseline_mean @200k (DeepSeek temp-0, per-task) instead of the v5 kimi-judged gate degradation, for any model with a curve.json. aggregate.py (load_w9_degradation) prefers W9, falls back to the kimi degradation otherwise, and emits a deg_source column + a mixed-source WARNING (never rank W9 and kimi rows in one table). The operator formula and W = deepeval+degradation+bfcl wall are otherwise unchanged.
  • Because HumanEval (execution), IFEval (rule) and BFCL (offline AST) are judge-independent, a W9-sourced Q depends on exactly one judge (DeepSeek, via degradation) — supersedes v5.1's "gate tables stay kimi-judged" for the ranking. kimi JSON/multiturn/tool metrics are dropped from the README tables (informational). v5.2 Q/R are not comparable to v5 kimi-Degrad Q/R.

v5.1 (2026-07-22) — context-curve harness + temp-0 judge:

  • Adds the usable-context-ceiling harness (bench_context_curve.py / run_context_curve.sh): baseline-relative bisection with repeated warm-cache attempts; reports the ceiling in tokens (a separate axis, not part of the v5 Q/R tables).
  • Judge = DeepSeek-V4-Flash at temperature=0, thinking disabled (JUDGE_TEMPERATURE=0
    • JUDGE_THINKING_OFF=1) — deterministic scoring, replacing kimi (which 400s on any temperature). Context-curve numbers are judged by this temp-0 judge and are not comparable to the kimi-judged v5 gate tables (per the one-judge-per-run rule); the v5 gate results are unchanged and stay kimi-judged.

v5 (2026-07-16) — what changed vs v4:

  • Degradation runs IN the gate (was deep-tier), at a common absolute cap --max-ctx 250000 for every model so levels L0–L5 are byte-comparable across the field (clamped down to served_window − 8192 when a model's window is smaller). A model whose window exceeds ~333k may add ONE informational native-window curve (*.degradation.native.tN.json), excluded from the ranking.
  • BFCL (tool-calling) runs IN the gate — EvalScope offline AST, FC mode with a prompt-mode fallback (kept whichever scores higher, mode annotated).
  • Ranking is the quality-per-time ratio R (operator formula, all test scores 0-100): CTX = input@18/18 ÷ 1000; Q = 2·HumanEval + 3·Degradation + CTX + IFEval + 2·BFCL; W = (deepeval + degradation + BFCL) wall in minutes; R = Q / W. Both tables sort by R; both Q and R columns are shown. Prerequisites (agent-ready + 18/18 needle at the working window) are pass/fail gates; a model is flagged (never auto-removed) only if it is BOTH low-Q AND slow.
  • Max-context probe protocol: the needle descent is run with --output-budget 8192 and --start-input up to 500000 (windows extended via the vendor-validated YaRN factor) so input@18/18 is a measured ceiling, not a start-clamped lower bound.
  • HumanEval sample is n=157 (deepeval's full task enum; 7 of the canonical 164 absent upstream — not comparable to published HumanEval-164).
#!/usr/bin/env python3
"""Reduce an EvalScope BFCL-v4 report to the normalized `<dir>.bfcl.json` that
aggregate.py's load_bfcl() reads. Stdlib only; runs on the host (not in the container).
bfcl_extract.py <report_json> <out_json> [--wall SECONDS] [--fc-mode true|false]
Report shape (evalscope 1.9.0): top-level "score" (0-1 overall AST accuracy),
"metrics":[{"name":"acc","num":N,"score":..,"categories":[...]}]. We emit overall_pct
(0-100), num, best-effort per-category subsets, the run wall, and the FC/prompt mode."""
import argparse
import json
def main():
ap = argparse.ArgumentParser()
ap.add_argument("report")
ap.add_argument("out")
ap.add_argument("--wall", type=float, default=0.0)
ap.add_argument("--fc-mode", default="true")
a = ap.parse_args()
with open(a.report) as fh:
d = json.load(fh)
overall = d.get("score")
metrics = (d.get("metrics") or [{}])[0]
num = metrics.get("num") or d.get("num")
# best-effort per-category breakdown (structure varies; never load-bearing for Q)
subsets = {}
def walk(node):
if isinstance(node, dict):
n, s = node.get("name"), node.get("score")
if isinstance(n, str) and isinstance(s, (int, float)):
subsets[n] = round(s * 100, 1) if s <= 1 else round(s, 1)
for v in node.values():
walk(v)
elif isinstance(node, list):
for v in node:
walk(v)
walk(metrics.get("categories"))
out = {
"model": d.get("model_name"),
"overall": overall,
"overall_pct": round(overall * 100, 2) if isinstance(overall, (int, float)) else None,
"num": num,
"subsets": subsets,
"wall_s": round(a.wall, 1),
"fc_mode": a.fc_mode == "true",
"source": "evalscope bfcl_v4 offline AST",
}
with open(a.out, "w") as fh:
json.dump(out, fh, indent=2)
print(f"bfcl_extract: {out['overall_pct']}% n={num} fc={out['fc_mode']} wall={out['wall_s']}s")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""W9 rollup: collect the per-model context-degradation results
(bench_context_curve.py output) into a degradation-size table, IN TOKENS.
DEFAULT run is BORDERS-ONLY: a `baseline` (min, 200k) whose mean sets the
per-model bar (threshold = baseline_mean - tolerance) + the needle/window ceiling
`hi` (max). The metric is the DEGRADATION SIZE between the two borders:
drop = baseline_mean - max_border_mean (in points and %); the max border HOLDS iff
its mean >= threshold. WHERE it degrades is not measured unless the run used --bisect
(then extra in-between lengths appear, tagged as bonus). ceiling_tokens = the max border
when it holds (= the usable ceiling), else null.
Each length also records the warm-cache evidence the operator ranks on: cold_wall_avg
(attempt 1, full prefill) vs warm_wall_avg (attempts 2-5, prefill reused). This prints,
per model, the two borders + the degradation size + every tested length (mean, per-attempt
means, HOLD/DROP, per-task means, cold-vs-warm wall) so quality drop AND warm speedup are
visible, and writes a rollup JSON. Falls back to the largest tested length as the max border
for older JSONs that predate the borders-only schema.
Usage: context_curve_ceiling.py [--results DIR] [--out rollup_curve.json]
"""
import argparse, glob, json, os
def _per_task_means(rec):
"""Mean score per task across that task's attempts."""
out = {}
for tk, runs in (rec.get("tasks") or {}).items():
vals = [r["score"] for r in runs if r.get("score") is not None]
if vals:
out[tk] = round(sum(vals) / len(vals), 1)
return out
def _wall_sum(rec):
return sum(r["wall_s"] for runs in (rec.get("tasks") or {}).values()
for r in runs if r.get("wall_s"))
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--results", default=os.path.expanduser("~/.cache/gx10-bench-results"))
ap.add_argument("--out", default=None)
a = ap.parse_args()
files = sorted(glob.glob(os.path.join(a.results, "*.curve.json")))
if not files:
print(f"(no *.curve.json in {a.results})")
return
rollup = {"models": {}}
for f in files:
name = os.path.basename(f)[:-len(".curve.json")]
try:
c = json.load(open(f))
except Exception as e:
print(f"!! {name}: {e}")
continue
base_len = c.get("baseline_length")
lengths_raw = c.get("lengths", {})
rows = []
for k, rec in lengths_raw.items():
tok = rec.get("actual_tokens") or rec.get("target") or int(k)
rows.append({
"len_key": int(k),
"tok": tok,
"target": rec.get("target"),
"mean": rec.get("mean"),
"attempt_means": rec.get("attempt_means"),
"pass": rec.get("pass"),
"n_pass": rec.get("n_pass"),
"is_baseline": int(k) == base_len,
"per_task": _per_task_means(rec),
"cold_wall": rec.get("cold_wall_avg"),
"warm_wall": rec.get("warm_wall_avg"),
"resp_tokens": rec.get("resp_tokens_avg"), # avg generated length (verbosity)
"wall_s": round(_wall_sum(rec), 1),
})
rows.sort(key=lambda r: r["tok"])
# baseline warm-cache evidence (the disqualifying check + the speed axis)
brec = lengths_raw.get(str(base_len)) or {}
bc, bw = brec.get("cold_wall_avg"), brec.get("warm_wall_avg")
speedup = round(bc / bw, 1) if bc and bw else None
bm = c.get("baseline_mean")
# ── the MAX border = the model's max servable/needle context (hi) ──
# Prefer the explicit borders fields; fall back to the largest tested length
# (works for older bisection JSONs that predate the borders-only schema).
mb = c.get("max_border")
if mb:
max_tok, max_mean = mb.get("tokens"), mb.get("mean")
max_holds, max_tgt = mb.get("holds"), mb.get("target")
elif rows:
top_row = rows[-1] # rows are sorted by tok ascending
max_tok, max_mean = top_row["tok"], top_row["mean"]
max_holds, max_tgt = top_row["pass"], top_row["target"]
else:
max_tok = max_mean = max_holds = max_tgt = None
# degradation SIZE between the two borders (the metric the operator asked for)
drop_pts, drop_pct = c.get("degradation_drop_pts"), c.get("degradation_drop_pct")
if drop_pts is None and bm is not None and max_mean is not None:
drop_pts = round(bm - max_mean, 1)
drop_pct = round((bm - max_mean) / bm * 100, 1) if bm else None
# warm walls the operator ranks on: @200k (comparable) + @max-context (snappiness
# at the model's ceiling — if max HOLDS that ceiling is also its usable max).
mrec = lengths_raw.get(str(max_tgt)) if max_tgt is not None else None
if mrec is None and max_tok is not None:
mrec = next((L for L in lengths_raw.values()
if L.get("actual_tokens") == max_tok), None)
max_warm = mrec.get("warm_wall_avg") if mrec else None
max_cold = mrec.get("cold_wall_avg") if mrec else None
rollup["models"][name] = {
"method": c.get("method"),
"borders_only": bool(c.get("borders_only")),
"done": c.get("done"),
"ceiling_tokens": c.get("ceiling_tokens"), # holds@max => usable ceiling; else null
"ceiling_target": c.get("ceiling_target"),
"needle_ceiling": c.get("needle_ceiling"),
"baseline_length": base_len,
"baseline_mean": bm,
"threshold": c.get("threshold"),
"tolerance": c.get("tolerance"),
"repeats": c.get("repeats"),
"temperature": c.get("temperature"),
"note": c.get("note"),
"n_lengths": len(rows),
"wall_s": round(sum(r["wall_s"] for r in rows), 1),
"baseline_cold_wall": bc,
"baseline_warm_wall": bw,
"warm_speedup": speedup,
"warm_at_200k": bw, # comparable across models
# ── degradation-size endpoints ──
"max_border_tok": max_tok,
"max_border_mean": max_mean,
"max_border_holds": max_holds,
"degradation_drop_pts": drop_pts,
"degradation_drop_pct": drop_pct,
"warm_at_max": max_warm, # snappiness at the max context
"cold_at_max": max_cold,
"resp_at_200k": brec.get("resp_tokens_avg"), # verbosity (avg generated tokens/turn)
"resp_at_max": (mrec.get("resp_tokens_avg") if mrec else None),
"lengths": rows,
}
# sort by max-context reach (how far the model serves), desc
order = sorted(rollup["models"].items(),
key=lambda kv: (kv[1]["max_border_tok"] or 0), reverse=True)
print("\n=== W9 context degradation — min(baseline)->max borders, TOKENS ===\n")
for name, s in order:
mt = s["max_border_tok"]
mts = f"{mt:,}" if mt is not None else "PENDING"
nh = s["needle_ceiling"]
bm, mm = s["baseline_mean"], s["max_border_mean"]
thr, tol = s["threshold"], s["tolerance"]
dp, dpc = s["degradation_drop_pts"], s["degradation_drop_pct"]
holds = s["max_border_holds"]
hs = ("HOLDS" if holds else ("DROPS" if holds is False else "?"))
bl = s["baseline_length"]
print(f"{name:34s} max-context {mts:>13} tok (needle {nh:,}; "
f"{s['n_lengths']} pts, wall {s['wall_s']/3600:.1f}h)")
if bm is not None and mm is not None:
print(f" quality: {bl//1000}k baseline {bm} -> max {mm} "
f"DEGRADATION {dp} pts ({dpc}%) [max {hs} vs thr {thr} (tol {tol})]")
else:
print(f" quality: baseline {bm} -> max {mm} (in progress)")
w200 = f"{s['warm_at_200k']}s" if s['warm_at_200k'] else "n/a"
wmax = f"{s['warm_at_max']}s @{mt:,}tok" if s['warm_at_max'] and mt else "pending"
sp = f" (warm/cold x{s['warm_speedup']})" if s['warm_speedup'] else ""
print(f" warm walls: @{bl//1000}k={w200} (comparable) · @max-context={wmax} (snappiness){sp}")
r200 = f"{s['resp_at_200k']}tok" if s['resp_at_200k'] else "n/a"
rmax = f"{s['resp_at_max']}tok" if s['resp_at_max'] else "n/a"
print(f" response length (verbosity, avg generated/turn): @{bl//1000}k={r200} · @max-context={rmax}")
if s["note"]:
print(f" note: {s['note']}")
for r in s["lengths"]:
v = ("BASE" if r["is_baseline"] else
("HOLD" if r["pass"] else ("DROP" if r["pass"] is False else "----")))
npass = f"{r['n_pass']}/{s['repeats']}" if r["n_pass"] is not None else " - "
pt = " ".join(f"t{t}:{val:.0f}" for t, val in sorted(r["per_task"].items()))
am = r["attempt_means"] or []
cw = f"cold~{r['cold_wall']}s warm~{r['warm_wall']}s" if r["cold_wall"] else ""
rt = f" resp~{r['resp_tokens']}tok" if r.get("resp_tokens") else ""
mean = r["mean"] if r["mean"] is not None else float("nan")
tag = " <- in-between (bonus)" if (not r["is_baseline"] and r["tok"] != mt) else ""
print(f" {r['tok']:>9,} tok mean={mean:5.1f} {v} {npass}"
f" ams={am} [{pt}] {cw}{rt}{tag}")
print()
if a.out:
with open(a.out, 'w') as fh:
json.dump(rollup, fh, indent=2)
print(f"-> {a.out}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""Comprehensive deepeval 4.0.7 suite against a local OpenAI-compatible server.
Two distinct roles (do NOT conflate them — that was the old self-judge flaw):
SUBJECT = the served model under test. It GENERATES every answer: benchmark
answers, the non-LLM outputs, AND the responses the LLM metrics
score. This is the thing being measured.
JUDGE = an INDEPENDENT external evaluator. It only SCORES the subject's
outputs (deepeval's evaluation_model). Env: JUDGE_MODEL /
JUDGE_BASE_URL (+ JUDGE_FALLBACK_*), key OPENCODE_API_KEY (pass
-e OPENCODE_API_KEY to the container). A preflight probes the
judge and falls back on failure; the identity used is recorded in
the result JSON — never compare scores across judges.
Every metric runs inside timed(), so an API-shape mismatch on one metric is RECORDED
as an error and the suite continues. Each test stores its own wall_s; total_wall_s
sums them (+ warmup) for the whole eval-suite wall.
Groups (select with flags; default = all metrics, --bench adds the benchmarks):
agentic : ToolCorrectness (subject picks the tool; judge unused for scoring)
(ArgumentCorrectness/TaskCompletion/PlanAdherence skipped -- need an
@observe component trace; not measurable over plain HTTP)
multiturn : TurnRelevancy, ConversationCompleteness, KnowledgeRetention,
RoleAdherence, ConversationalGEval, and the turn-level RAG metrics
-- the SUBJECT generates each assistant turn, the judge scores them
(ConversationalDAG skipped -- needs a hand-authored DAG)
mcp : MCPUse, MCPTaskCompletion, MultiTurnMCPUse -- the SUBJECT answers from
a simulated tool result; the judge scores tool-use appropriateness
nonllm : ExactMatch, PatternMatch, JsonCorrectness (subject generates; scoring
is deterministic)
others : Summarization, PromptAlignment, Hallucination (subject generates; judge scores)
benchmarks : IFEval (n=100) + HumanEval (all 164, pass@1) — deterministic,
subject-only, sized so scores resolve real differences (--bench);
--deep adds MMLU (n=100) as an informational general-knowledge
probe, never a promotion criterion
Emits one `@@RESULT@@ {json}` line for machine capture.
python3 deepeval_full.py SUBJECT_MODEL [--bench] [--deep] [--groups a,b,...]
"""
import argparse
import json
import os
import re
import time
import typing
os.environ.setdefault("DEEPEVAL_TELEMETRY_OPT_OUT", "1")
os.environ.setdefault("DEEPEVAL_DISABLE_DOTENV", "1")
os.environ.setdefault("DEEPEVAL_UPDATE_WARNING_OPT_IN", "0")
import openai
from pydantic import BaseModel
import mcp.types as mcptypes
from deepeval.models import DeepEvalBaseLLM
from deepeval.test_case import (
LLMTestCase, ConversationalTestCase, Turn, ToolCall, MCPServer, MCPToolCall,
MultiTurnParams,
)
from deepeval.metrics import (
ToolCorrectnessMetric, ArgumentCorrectnessMetric, TaskCompletionMetric,
TurnRelevancyMetric, ConversationCompletenessMetric, KnowledgeRetentionMetric,
RoleAdherenceMetric, ConversationalGEval,
TurnContextualPrecisionMetric, TurnContextualRecallMetric,
TurnContextualRelevancyMetric, TurnFaithfulnessMetric,
MCPUseMetric, MCPTaskCompletionMetric, MultiTurnMCPUseMetric,
ExactMatchMetric, PatternMatchMetric, JsonCorrectnessMetric,
SummarizationMetric, PromptAlignmentMetric, HallucinationMetric,
)
BASE = os.environ.get("BASE_URL", "http://localhost:80/v1")
UA = "GX10-Benchmark/1.0" # Cloudflare drops UA-less requests at the judge gateway
# Independent judge (OpenCode Go, OpenAI-compatible). Overridable via env; a
# preflight probes it and falls back to the fallback judge on failure. The
# judge identity is recorded in the result JSON — never compare scores across
# different judges.
JUDGE_MODEL = os.environ.get("JUDGE_MODEL", "kimi-k2.7-code")
JUDGE_BASE = os.environ.get("JUDGE_BASE_URL", "https://opencode.ai/zen/go/v1")
JUDGE_FALLBACK_MODEL = os.environ.get("JUDGE_FALLBACK_MODEL", "deepseek-v4-flash-free")
JUDGE_FALLBACK_BASE = os.environ.get("JUDGE_FALLBACK_BASE_URL", "https://opencode.ai/zen/v1")
# usage is tracked per role so the report separates subject cost from judge cost.
USAGE = {"subject": {"calls": 0, "in": 0, "out": 0, "s": 0.0},
"judge": {"calls": 0, "in": 0, "out": 0, "s": 0.0}}
def _out_total():
return USAGE["subject"]["out"] + USAGE["judge"]["out"]
class VLLM(DeepEvalBaseLLM):
def __init__(self, name, max_tokens=6000, base_url=BASE, api_key="placeholder",
role="subject", temperature=0):
self.name = name
self.max_tokens = max_tokens
self.role = role
# None = omit the field (OpenCode Go's kimi upstream 400s on temperature=0,
# so judge instances must not send it; the local SUBJECT keeps 0).
self.temperature = temperature
self._c = openai.OpenAI(base_url=base_url, api_key=api_key, timeout=900,
default_headers={"User-Agent": UA})
self._ac = openai.AsyncOpenAI(base_url=base_url, api_key=api_key, timeout=900,
default_headers={"User-Agent": UA})
def load_model(self):
return self._c
def _track(self, r, dt):
u = USAGE[self.role]
u["calls"] += 1
u["s"] += dt
usage = getattr(r, "usage", None)
if usage:
u["in"] += usage.prompt_tokens or 0
u["out"] += usage.completion_tokens or 0
@staticmethod
def _strip_think(text):
# reasoning models wrap their chain-of-thought in <think>...</think>; remove it
# (and any leading text) so JSON parsing sees only the final answer object.
return re.sub(r"<think>.*?</think>", "", text, flags=re.S).strip()
@staticmethod
def _extract(text, ann):
"""Pull a value of type `ann` out of free-form model text (deepeval benchmark
schemas are single-field; the served reasoning models answer in prose/letters,
and the vLLM servers here don't enforce guided_json)."""
text = text.strip()
origin, targs = typing.get_origin(ann), typing.get_args(ann)
if origin is typing.Literal: # MCQ: pick the matching choice
choices = list(targs)
for ch in choices: # exact whole-string answer
if text == str(ch):
return ch
# explicit marker first: "Answer: B", "the correct option is C" -> that letter.
# (disambiguates verbose reasoners that enumerate options before answering.)
letters = [str(c) for c in choices if len(str(c)) == 1 and str(c).isalpha()]
if letters:
mk = re.findall(r"(?:answer|option|choice|correct)\b[^A-Za-z0-9]{0,6}(["
+ "".join(letters) + r"])\b", text, re.I)
if mk:
for ch in choices:
if str(ch).lower() == mk[-1].lower():
return ch
for ch in sorted(choices, key=lambda c: -len(str(c))): # standalone token: 'A' vs '(A)'
core = re.sub(r"[^A-Za-z0-9]", "", str(ch)) # BEFORE bare substring, so a letter
if core and re.search(r"(?<![A-Za-z0-9])" + re.escape(core) + r"(?![A-Za-z0-9])", text, re.I):
return ch # inside a word ('A' in "Answer") can't win
for ch in sorted(choices, key=lambda c: -len(str(c))): # bare substring (last resort)
if str(ch) in text:
return ch
low = text.lower()
for ch in choices: # yes/no/true/false, case-insensitive
if str(ch).lower() in low:
return ch
return choices[0] # give up -> wrong, but no crash
if origin in (list, typing.List):
inner = targs[0] if targs else str
if inner is int:
return [int(x) for x in re.findall(r"-?\d+", text)]
return [p.strip() for p in re.split(r"[,\n]", text) if p.strip()]
if ann is int:
m = re.search(r"-?\d+", text)
return int(m.group(0)) if m else 0
return text # str / dates / anything else
@classmethod
def _coerce(cls, text, schema):
if schema is None:
return text
clean = cls._strip_think(text)
# reasoning judges (kimi via Go) fence the verdict nondeterministically —
# ```json ... ``` — unwrap before parsing.
fence = re.search(r"```(?:json)?\s*(.*?)```", clean, re.S)
if fence:
clean = fence.group(1).strip()
elif clean.startswith("```"): # unterminated fence (truncated reply)
clean = clean.split("\n", 1)[-1].strip()
try:
return schema(**json.loads(clean))
except Exception:
pass
# fallback: scan for the LAST complete {...} — raw_decode is nested-safe
# (the old flat-object regex lost nested verdict lists -> JSONDecodeError)
_dec = json.JSONDecoder()
for m in reversed(list(re.finditer(r"\{", clean))):
try:
obj, _ = _dec.raw_decode(clean[m.start():])
if isinstance(obj, dict):
return schema(**obj)
except Exception:
continue
# not JSON (server didn't enforce guided decoding) -> adapt the raw answer into
# the schema's single 'answer' field by its declared type.
fields = getattr(schema, "model_fields", {})
if "answer" in fields:
return schema(answer=cls._extract(clean, fields["answer"].annotation))
return schema(**json.loads(clean)) # raise the original-style error if all fail
def _create(self, client_call, prompt, schema):
# vLLM guided decoding: constrain output to the schema so reasoning models can't
# leak <think> blocks into the parsed answer. Fall back to free-form if the
# server rejects guided_json (then _coerce strips/repairs).
base = dict(model=self.name, messages=[{"role": "user", "content": prompt}],
max_tokens=self.max_tokens)
if self.temperature is not None:
base["temperature"] = self.temperature
if schema is not None:
try:
js = schema.model_json_schema()
return self._retry(client_call, **base, extra_body={"guided_json": js})
except openai.RateLimitError:
raise # judge quota, not a guided_json rejection
except Exception:
pass
return self._retry(client_call, **base)
@staticmethod
def _retry(call, *args, **kwargs):
# The external judge (OpenCode Zen free tier) returns 429 FreeUsageLimitError
# when its rolling quota is hit; an unwrapped call crashes the whole suite. Back
# off and retry so a rolling/transient limit self-heals. (The localhost SUBJECT
# never 429s, so this is a no-op for it.)
delay = 8
for attempt in range(10):
try:
return call(*args, **kwargs)
except openai.RateLimitError:
if attempt == 9:
raise
time.sleep(delay)
delay = min(delay * 2, 90)
@staticmethod
def _msg_text(msg):
# reasoning judges (Zen deepseek-v4-flash) emit chain-of-thought in
# reasoning_content and the final JSON in content; when the token budget runs
# out mid-thought, content comes back empty. Fall back to reasoning_content so
# _coerce can still recover a trailing verdict object instead of raising
# JSONDecodeError on an empty string.
content = (getattr(msg, "content", None) or "").strip()
if content:
return content
return getattr(msg, "reasoning_content", None) or ""
def generate(self, prompt, schema=None):
t0 = time.perf_counter()
r = self._create(self._c.chat.completions.create, prompt, schema)
self._track(r, time.perf_counter() - t0)
return self._coerce(self._msg_text(r.choices[0].message), schema)
async def a_generate(self, prompt, schema=None):
t0 = time.perf_counter()
base = dict(model=self.name, messages=[{"role": "user", "content": prompt}],
max_tokens=self.max_tokens)
if self.temperature is not None:
base["temperature"] = self.temperature
try:
if schema is not None:
r = await self._ac.chat.completions.create(
**base, extra_body={"guided_json": schema.model_json_schema()})
else:
r = await self._ac.chat.completions.create(**base)
except Exception:
r = await self._ac.chat.completions.create(**base)
self._track(r, time.perf_counter() - t0)
return self._coerce(self._msg_text(r.choices[0].message), schema)
def chat(self, messages, max_tokens=None):
"""Raw multi-message completion -> assistant text. Used to make the SUBJECT
generate the multi-turn / MCP responses the LLM metrics then score."""
t0 = time.perf_counter()
kw = dict(model=self.name, messages=messages,
max_tokens=max_tokens or self.max_tokens)
if self.temperature is not None:
kw["temperature"] = self.temperature
r = self._retry(self._c.chat.completions.create, **kw)
self._track(r, time.perf_counter() - t0)
return self._msg_text(r.choices[0].message)
_CODE_FENCE = re.compile(r"```[a-zA-Z0-9_+-]*\s*\n(.*?)```", re.S)
def generate_samples(self, prompt, n, temperature): # HumanEval
# Full self.max_tokens budget (2048 pinned thinking models mid-<think> —
# truncated code scored 0 regardless of ability), and sanitize each sample:
# strip <think>, unwrap the first markdown code fence (the scorer execs
# the text verbatim; a fence line is a SyntaxError).
t0 = time.perf_counter()
r = self._c.chat.completions.create(
model=self.name, messages=[{"role": "user", "content": prompt}],
temperature=temperature, n=n, max_tokens=self.max_tokens)
self._track(r, time.perf_counter() - t0)
outs = []
for c in r.choices:
text = self._strip_think(self._msg_text(c.message))
m = self._CODE_FENCE.search(text)
outs.append((m.group(1) if m else text).strip())
return outs
def batch_generate(self, prompts, schemas=None): # MCQ benchmarks
if schemas is None:
return [self.generate(p) for p in prompts]
return [self.generate(p, s) for p, s in zip(prompts, schemas)]
def get_model_name(self):
return self.name
def timed(fn):
out0 = _out_total()
t0 = time.perf_counter()
try:
v = fn()
return {"ok": True, "result": v, "wall_s": round(time.perf_counter() - t0, 1),
"out_tokens": _out_total() - out0}
except Exception as e: # noqa: BLE001
return {"ok": False, "error": f"{type(e).__name__}: {str(e)[:300]}",
"wall_s": round(time.perf_counter() - t0, 1)}
def total_wall(out):
"""Sum every per-test wall_s (+ warmup) -> whole eval-suite wall. Resume-safe:
recomputed from the persisted per-test times each save()."""
tot = out.get("warmup_s") or 0.0
for g in (out.get("metrics") or {}).values():
for r in (g or {}).values():
w = r.get("wall_s") if isinstance(r, dict) else None
if isinstance(w, (int, float)):
tot += w
for r in (out.get("benchmarks") or {}).values():
w = r.get("wall_s") if isinstance(r, dict) else None
if isinstance(w, (int, float)):
tot += w
return round(tot, 1)
def run_group(group, specs):
"""Run [(name, fn), ...], printing each metric's outcome+wall as it finishes.
One retry per failed metric: the judge runs at provider-default temperature
(temperature=0 is rejected upstream), so a malformed verdict shape is a
re-rollable sample, not a deterministic failure."""
res = {}
for name, fn in specs:
r = timed(fn)
if not r.get("ok"):
print(f" [{group}] {name}: ERR:{str(r.get('error', ''))[:70]} "
f"({r['wall_s']}s) — retrying once", flush=True)
r2 = timed(fn)
if r2.get("ok"):
r = r2
tag = "ok" if r.get("ok") else "ERR:" + str(r.get("error", ""))[:70]
print(f" [{group}] {name}: {tag} ({r['wall_s']}s)", flush=True)
res[name] = r
return res
# ---------------------------------------------------------------- shared fixtures
def mcp_server():
return MCPServer(
server_name="ops-tools",
transport="stdio",
available_tools=[
mcptypes.Tool(name="get_weather", description="Current weather for a city",
inputSchema={"type": "object", "properties": {"city": {"type": "string"}}}),
mcptypes.Tool(name="get_stock_price", description="Latest stock price for a ticker",
inputSchema={"type": "object", "properties": {"ticker": {"type": "string"}}}),
])
# ---------------------------------------------------------------- groups
def run_agentic(out, subject, judge):
def tool_correctness():
txt = subject.generate(
'You can call exactly ONE tool:\n- get_weather(city)\n- get_stock_price(ticker)\n'
'User: "What is the weather in Paris right now?"\n'
'Reply with ONLY one JSON object with keys "tool" and "args". '
'Example: {"tool": "get_stock_price", "args": {"ticker": "AAPL"}}')
m = re.search(r'"tool"\s*:\s*"([^"]+)"', txt)
picked = m.group(1) if m else "none"
# ToolCorrectness is deterministic (compares tools_called vs expected_tools);
# model=judge only avoids deepeval spinning a default OpenAI client.
tc = LLMTestCase(input="weather in Paris", actual_output="(tool)",
tools_called=[ToolCall(name=picked)],
expected_tools=[ToolCall(name="get_weather")])
mm = ToolCorrectnessMetric(threshold=0.5, model=judge, include_reason=False)
mm.measure(tc)
return {"score": mm.score, "tool_picked": picked}
# ArgumentCorrectness / TaskCompletion / PlanAdherence all render templates from an
# @observe COMPONENT TRACE ('stringified_tools_called' / 'tools_called_formatted' are
# trace-derived); they raise MetricTemplateInterpolationError on a standalone HTTP
# test case. Not measurable over plain HTTP without instrumented tracing.
TRACE_ONLY = {"ok": False, "skipped": "requires @observe component trace (trace-derived "
"template vars); not measurable over plain HTTP"}
out["agentic"] = run_group("agentic", [("tool_correctness", tool_correctness)])
out["agentic"]["argument_correctness"] = dict(TRACE_ONLY)
out["agentic"]["task_completion"] = dict(TRACE_ONLY)
out["agentic"]["plan_adherence"] = dict(TRACE_ONLY)
# the multi-turn scenario: chatbot role, the USER turns, and the retrieval facts
# available at each assistant turn. The SUBJECT generates the assistant turns; the
# judge then scores them (incl. turn-level RAG against these retrieval_contexts).
CONV_ROLE = "a helpful, accurate account-support assistant"
CONV_SCENARIO = "A user asks a factual question and requests two account actions."
CONV_OUTCOME = "Answer the TTL correctly and confirm both account actions."
CONV_STEPS = [
("What's the TTL of the obsidian-cache library, and reset my password.",
["obsidian-cache uses a default entry TTL of 6143 seconds.",
"Password reset links expire 1 hour after issue."]),
("Good. Now what TTL did you say, and update my billing address to 10 Main St.",
["obsidian-cache uses a default entry TTL of 6143 seconds.",
"Billing address updates take effect immediately."]),
]
def build_conv(subject):
"""Drive the SUBJECT through CONV_STEPS, grounding each turn on its retrieval
facts, and return a ConversationalTestCase with the subject's real outputs."""
turns, msgs = [], [{"role": "system", "content": f"You are {CONV_ROLE}."}]
for user_msg, rc in CONV_STEPS:
msgs.append({"role": "user", "content": user_msg})
turns.append(Turn(role="user", content=user_msg))
# fold the per-turn retrieval facts into a SINGLE leading system message —
# appending a second system message mid-conversation is rejected by strict chat
# templates ("System message must be at the beginning.", e.g. Qwen3.5 on vLLM).
grounded = [{"role": "system",
"content": f"You are {CONV_ROLE}.\nRelevant facts you may use:\n- "
+ "\n- ".join(rc)}] + msgs[1:]
ans = subject.chat(grounded, max_tokens=1024)
msgs.append({"role": "assistant", "content": ans})
turns.append(Turn(role="assistant", content=ans, retrieval_context=rc))
return ConversationalTestCase(turns=turns, chatbot_role=CONV_ROLE,
scenario=CONV_SCENARIO, expected_outcome=CONV_OUTCOME)
def run_multiturn(out, subject, judge):
tc = build_conv(subject) # subject generates the assistant turns ONCE; reuse for all metrics
def mk(metric_factory):
def _():
m = metric_factory()
m.measure(tc)
return {"score": round(m.score, 3) if m.score is not None else None,
"reason": (getattr(m, "reason", "") or "")[:160]}
return _
out["multiturn"] = run_group("multiturn", [
("turn_relevancy", mk(lambda: TurnRelevancyMetric(model=judge, async_mode=False))),
("conversation_completeness", mk(lambda: ConversationCompletenessMetric(model=judge, async_mode=False))),
("knowledge_retention", mk(lambda: KnowledgeRetentionMetric(model=judge, async_mode=False))),
("role_adherence", mk(lambda: RoleAdherenceMetric(model=judge, async_mode=False))),
("conversational_geval", mk(lambda: ConversationalGEval(
name="Coherence", criteria="Assess whether the assistant stays coherent, on-topic, "
"and consistent across turns.",
evaluation_params=[MultiTurnParams.ROLE, MultiTurnParams.CONTENT],
model=judge, async_mode=False))),
("turn_contextual_precision", mk(lambda: TurnContextualPrecisionMetric(model=judge, async_mode=False))),
("turn_contextual_recall", mk(lambda: TurnContextualRecallMetric(model=judge, async_mode=False))),
("turn_contextual_relevancy", mk(lambda: TurnContextualRelevancyMetric(model=judge, async_mode=False))),
("turn_faithfulness", mk(lambda: TurnFaithfulnessMetric(model=judge, async_mode=False))),
])
out["multiturn"]["conversational_dag"] = {"ok": False, "skipped": "requires a hand-authored DeepAcyclicGraph"}
def run_mcp(out, subject, judge):
srv = mcp_server()
# simulate the environment: get_weather was called and returned a result; the
# SUBJECT must produce the user-facing answer from it. The judge then scores
# whether the subject used the tool result appropriately.
tool_result = "Paris: 18C, clear"
called = [MCPToolCall(name="get_weather", args={"city": "Paris"},
result=mcptypes.CallToolResult(
content=[mcptypes.TextContent(type="text", text=tool_result)]))]
user_q = "What's the weather in Paris?"
answer = subject.chat([
{"role": "system", "content": "You have MCP tools: get_weather(city), get_stock_price(ticker). "
"A tool was already called on your behalf; use its result to answer the user."},
{"role": "user", "content": user_q},
{"role": "assistant", "content": f"[called get_weather(city=\"Paris\") -> {tool_result}]"},
{"role": "user", "content": "Now give me the final answer to my question."},
], max_tokens=512)
def single():
tc = LLMTestCase(input=user_q, actual_output=answer,
mcp_servers=[srv], mcp_tools_called=called)
m = MCPUseMetric(model=judge, async_mode=False)
m.measure(tc)
return {"score": round(m.score, 3) if m.score is not None else None,
"reason": (getattr(m, "reason", "") or "")[:160], "answer": answer[:120]}
def conv_mcp(metric_factory):
def _():
turns = [
Turn(role="user", content=user_q),
Turn(role="assistant", content=answer, mcp_tools_called=called),
]
tc = ConversationalTestCase(turns=turns, mcp_servers=[srv])
m = metric_factory()
m.measure(tc)
return {"score": round(m.score, 3) if m.score is not None else None,
"reason": (getattr(m, "reason", "") or "")[:160]}
return _
out["mcp"] = run_group("mcp", [
("mcp_use", single),
("mcp_task_completion", conv_mcp(lambda: MCPTaskCompletionMetric(model=judge, async_mode=False))),
("multiturn_mcp_use", conv_mcp(lambda: MultiTurnMCPUseMetric(model=judge, async_mode=False))),
])
def run_nonllm(out, subject, judge):
def exact_match():
ans = subject.generate('Reply with EXACTLY this and nothing else: PONG')
clean = ans.strip().strip(".").strip()
tc = LLMTestCase(input="say PONG", actual_output=clean, expected_output="PONG")
m = ExactMatchMetric()
m.measure(tc)
return {"score": m.score, "got": clean[:40]}
def pattern_match():
ans = subject.generate('Output ONLY an ISO date (YYYY-MM-DD) for 5 March 2026. No prose.')
clean = (re.search(r"\d{4}-\d{2}-\d{2}", ans) or [None])
got = clean.group(0) if hasattr(clean, "group") else ans.strip()[:20]
tc = LLMTestCase(input="iso date", actual_output=got)
m = PatternMatchMetric(pattern=r"\d{4}-\d{2}-\d{2}")
m.measure(tc)
return {"score": m.score, "got": got}
class Weather(BaseModel):
city: str
temperature_celsius: float
condition: str
def json_correctness():
raw = subject.generate(
'Output ONLY minified JSON for Berlin weather with keys city (string), '
'temperature_celsius (number), condition (string). No prose, no code fence.')
m2 = re.search(r"\{[^{}]*\}", raw)
clean = m2.group(0) if m2 else raw
tc = LLMTestCase(input="berlin weather json", actual_output=clean)
m = JsonCorrectnessMetric(expected_schema=Weather, model=judge, include_reason=False)
m.measure(tc)
return {"score": m.score, "extracted": clean[:120]}
out["nonllm"] = run_group("nonllm", [
("exact_match", exact_match),
("pattern_match", pattern_match),
("json_correctness", json_correctness),
])
def run_others(out, subject, judge):
SOURCE = (
"The quartz-relay gateway enforces a rate limit of 2570 requests per minute. "
"When the limit is exceeded, requests are queued for up to 30 seconds before "
"being rejected with a 429 status. Administrators can raise the limit per API key.")
def summarization():
summ = subject.generate(f"Summarize in one sentence:\n{SOURCE}")
tc = LLMTestCase(input=SOURCE, actual_output=summ)
m = SummarizationMetric(threshold=0.5, n=5, model=judge, async_mode=False)
m.measure(tc)
return {"score": round(m.score, 3), "summary": summ[:160]}
def prompt_alignment():
instr = ["Respond in exactly two sentences.",
"Use a formal tone.",
"Do not use the word 'very'."]
resp = subject.generate("Describe the benefits of unit testing. " + " ".join(instr))
tc = LLMTestCase(input="Describe the benefits of unit testing.", actual_output=resp)
m = PromptAlignmentMetric(prompt_instructions=instr, model=judge, async_mode=False)
m.measure(tc)
return {"score": round(m.score, 3), "reason": (m.reason or "")[:160]}
def hallucination():
context = ["The quartz-relay gateway enforces a rate limit of 2570 requests per minute."]
ans = subject.generate("Based only on this fact, what rate limit does quartz-relay enforce? "
"Fact: quartz-relay enforces 2570 requests per minute.")
tc = LLMTestCase(input="quartz-relay rate limit?", actual_output=ans, context=context)
m = HallucinationMetric(threshold=0.5, model=judge, async_mode=False)
m.measure(tc)
return {"score": round(m.score, 3), "note": "lower is better (hallucination rate)",
"reason": (m.reason or "")[:160]}
out["others"] = run_group("others", [
("summarization", summarization),
("prompt_alignment", prompt_alignment),
("hallucination", hallucination),
])
def run_benchmarks(out, subject, judge, ifeval_n, humaneval_tasks, mmlu_n, save=None):
"""The gate-tier benchmarks. Both are deterministic (no judge) and sized so
the score resolves real differences (95% CI ~±8-10 points at these Ns —
never rank on N<50; see BENCHMARK.md). Scores are recorded as fraction + %.
IFEval doubles as the quantization-damage canary: instruction following
degrades 5-9+ points under quant damage that leaves MCQ accuracy flat.
mmlu_n > 0 adds the optional deep-tier MMLU probe (general-knowledge axis —
informational, not a promotion criterion)."""
def _pct(b, extra):
extra["overall_score"] = b.overall_score
extra["pct"] = round(100 * b.overall_score, 1) if b.overall_score is not None else None
return extra
def ifeval():
from deepeval.benchmarks import IFEval
b = IFEval(n_problems=ifeval_n)
b.evaluate(model=subject)
return _pct(b, {"n_problems": ifeval_n})
def humaneval():
import subprocess
import sys
import types
from deepeval.benchmarks import HumanEval
from deepeval.benchmarks.tasks import HumanEvalTask
from deepeval.benchmarks.human_eval.template import HumanEvalTemplate
# deepeval-4.0.7's scorer defines the dataset's check() but never CALLS it,
# and its secure_exec sandbox blocks imports/typing — wrong code scores 1,
# correct code with a List annotation scores 0 (verified empirically). Restore
# official pass@1: append check(entry_point) and run the asserts in a real
# interpreter under a wall-clock timeout ("functional-v2" scorer).
def _predict(self, model, task, golden, k):
c = self.c.get(task.value)
functions = self.functions.get(task.value)
if c is None:
prompt = HumanEvalTemplate.generate_output(input=golden.input, task=task)
functions = model.generate_samples(
prompt=prompt, n=self.n, temperature=self.temperature)
c = 0
for function in functions:
program = (
"from typing import *\n"
"import math, re, itertools, collections, heapq, functools, bisect\n"
+ function + "\n" + golden.expected_output + "\n"
+ f"check({task.value})\n")
try:
r = subprocess.run([sys.executable, "-c", program],
capture_output=True, timeout=15)
if r.returncode == 0:
c += 1
except Exception:
pass
self.c[task.value] = c
self.functions[task.value] = functions
return {"prediction": functions, "score": self.scorer.pass_at_k(self.n, c, k)}
tasks = list(HumanEvalTask)
if humaneval_tasks:
tasks = tasks[:humaneval_tasks]
b = HumanEval(tasks=tasks, n=1)
b.predict = types.MethodType(_predict, b)
b.evaluate(model=subject, k=1)
return _pct(b, {"tasks": len(tasks), "n": 1, "k": 1, "scorer": "functional-v2",
"gen_max_tokens": subject.max_tokens})
def mmlu():
from deepeval.benchmarks import MMLU
from deepeval.benchmarks.tasks import MMLUTask
tasks = [MMLUTask.HIGH_SCHOOL_PHYSICS, MMLUTask.BUSINESS_ETHICS,
MMLUTask.COLLEGE_COMPUTER_SCIENCE, MMLUTask.FORMAL_LOGIC,
MMLUTask.HIGH_SCHOOL_MATHEMATICS, MMLUTask.LOGICAL_FALLACIES,
MMLUTask.MACHINE_LEARNING, MMLUTask.PHILOSOPHY,
MMLUTask.PROFESSIONAL_LAW, MMLUTask.WORLD_RELIGIONS]
per = max(1, mmlu_n // len(tasks))
b = MMLU(tasks=tasks, n_shots=3, n_problems_per_task=per)
b.evaluate(model=subject)
return _pct(b, {"tasks": len(tasks), "n_per_task": per, "tier": "deep"})
bench = [("ifeval", ifeval), ("humaneval", humaneval)]
if mmlu_n:
bench.append(("mmlu", mmlu))
# checkpoint after EACH benchmark — the slow heavy-thinker models often time out
# mid-benchmarks; per-benchmark save() preserves the ones already finished.
res = out.setdefault("benchmarks", {})
for name, fn in bench:
if name in res:
print(f" [benchmarks] {name}: already done — skip", flush=True)
continue
r = timed(fn)
tag = "ok" if r.get("ok") else "ERR:" + str(r.get("error", ""))[:70]
print(f" [benchmarks] {name}: {tag} ({r['wall_s']}s)", flush=True)
res[name] = r
if save:
save()
ALL_GROUPS = ["agentic", "multiturn", "mcp", "nonllm", "others"]
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("model", help="the SUBJECT model under test (served on localhost)")
ap.add_argument("--url", default=BASE, help="subject endpoint (OpenAI base, …/v1)")
ap.add_argument("--bench", action="store_true", help="add the gate-tier benchmarks")
ap.add_argument("--ifeval-n", type=int, default=100, help="IFEval problems (gate tier)")
ap.add_argument("--humaneval-tasks", type=int, default=0,
help="HumanEval task count (0 = all 164, gate tier)")
ap.add_argument("--deep", action="store_true",
help="add the deep tier (MMLU --mmlu-n; informational only)")
ap.add_argument("--mmlu-n", type=int, default=100, help="deep-tier MMLU problems")
ap.add_argument("--groups", default=",".join(ALL_GROUPS),
help="comma list of metric groups to run")
ap.add_argument("--max-tokens", type=int, default=6000, help="subject generation budget")
ap.add_argument("--judge-max-tokens", type=int, default=6000, help="judge scoring budget")
ap.add_argument("--out", default=None,
help="write result JSON here, rewritten after each group (survives timeout)")
a = ap.parse_args()
judge_key = os.environ.get("OPENCODE_API_KEY")
if not judge_key:
raise SystemExit("OPENCODE_API_KEY not set in env — pass `-e OPENCODE_API_KEY` to the container")
subject = VLLM(a.model, max_tokens=a.max_tokens, base_url=a.url,
api_key="placeholder", role="subject")
judge = VLLM(JUDGE_MODEL, max_tokens=a.judge_max_tokens, base_url=JUDGE_BASE,
api_key=judge_key, role="judge", temperature=None)
judge_id = f"{JUDGE_MODEL} via {JUDGE_BASE} — independent external judge"
out = {"model": a.model, "subject": a.model, "suite": "v4",
"judge": judge_id,
"deepeval_version": "4.0.7", "metrics": {}}
# resume: a prior partial run (heavy-thinkers time out) left finished groups/benchmarks
# in --out; reload and skip them so a re-launch only runs what's missing.
if a.out and os.path.exists(a.out):
try:
prev = json.load(open(a.out))
if prev.get("model") == a.model and isinstance(prev.get("metrics"), dict):
out["metrics"] = prev["metrics"]
if isinstance(prev.get("benchmarks"), dict):
out["benchmarks"] = prev["benchmarks"]
if prev.get("judge"):
out["judge_prev"] = prev["judge"]
print(f"[deepeval:{a.model}] resume: {len(out['metrics'])} group(s), "
f"{len(out.get('benchmarks', {}))} benchmark(s) already done", flush=True)
except Exception:
pass
def save():
out["usage"] = {"subject": dict(USAGE["subject"]), "judge": dict(USAGE["judge"])}
out["total_wall_s"] = total_wall(out)
if a.out:
with open(a.out, "w") as fh:
json.dump(out, fh, indent=2)
# warm the SUBJECT (page weights / capture CUDA graphs) and fail fast if the
# external judge is unreachable / mis-keyed before running the whole suite.
t0 = time.perf_counter()
subject._c.chat.completions.create(model=a.model, messages=[{"role": "user", "content": "ping"}],
max_tokens=5, temperature=0)
out["warmup_s"] = round(time.perf_counter() - t0, 1)
# judge PREFLIGHT (direct calls, no _retry — fail fast): probe the primary
# judge; on failure fall back to the fallback judge; on double failure run
# no-judge (deterministic benchmarks + already-done groups still complete).
# One judge per run: the identity used is recorded in the result JSON, and
# scores from different judges are never comparable.
def _judge_probe(j, name, attempts=3, gap=20):
# transient network blips (APIConnectionError) shouldn't demote the judge
# for a whole multi-hour run — retry the probe before giving up.
for i in range(attempts):
try:
jr = j._c.chat.completions.create(model=name,
messages=[{"role": "user", "content": "Reply with: ok"}],
max_tokens=2000)
return bool((jr.choices[0].message.content or "").strip())
except Exception as e: # noqa: BLE001
print(f"[deepeval:{a.model}] judge {name} preflight failed "
f"({i + 1}/{attempts}): {type(e).__name__}: {e}"[:160], flush=True)
if i < attempts - 1:
time.sleep(gap)
return False
out["judge_warmup_ok"] = _judge_probe(judge, JUDGE_MODEL)
if not out["judge_warmup_ok"] and JUDGE_FALLBACK_MODEL and JUDGE_FALLBACK_MODEL != JUDGE_MODEL:
print(f"[deepeval:{a.model}] falling back to judge {JUDGE_FALLBACK_MODEL} "
f"@ {JUDGE_FALLBACK_BASE}", flush=True)
fb = VLLM(JUDGE_FALLBACK_MODEL, max_tokens=a.judge_max_tokens,
base_url=JUDGE_FALLBACK_BASE, api_key=judge_key, role="judge",
temperature=None)
if _judge_probe(fb, JUDGE_FALLBACK_MODEL):
judge = fb
judge_id = f"{JUDGE_FALLBACK_MODEL} via {JUDGE_FALLBACK_BASE} — fallback judge"
out["judge"] = judge_id
out["judge_warmup_ok"] = True
if out.get("judge_prev") and out["judge_prev"] != out["judge"]:
print(f"[deepeval:{a.model}] WARNING: resuming with a different judge than the "
f"partial run ({out['judge_prev']!r} -> {out['judge']!r}) — judged scores "
f"will be mixed-judge", flush=True)
print(f"[deepeval:{a.model}] subject warm {out['warmup_s']}s; judge={out['judge']} "
f"reachable={out['judge_warmup_ok']}", flush=True)
save()
groups = [g.strip() for g in a.groups.split(",") if g.strip()]
dispatch = {"agentic": run_agentic, "multiturn": run_multiturn, "mcp": run_mcp,
"nonllm": run_nonllm, "others": run_others}
for g in groups:
if g in dispatch:
if g in out["metrics"]:
print(f"[deepeval:{a.model}] group {g}: already done — skip", flush=True)
continue
print(f"[deepeval:{a.model}] group {g} ...", flush=True)
try:
dispatch[g](out["metrics"], subject, judge)
except Exception as e: # a group-level failure (e.g. build_conv) must not
out["metrics"][g] = {"ok": False, # abort the suite + benchmarks
"error": f"{type(e).__name__}: {e}"[:200]}
print(f"[deepeval:{a.model}] group {g} FAILED: {type(e).__name__}: {e}", flush=True)
save() # checkpoint: a later timeout still preserves finished groups
if a.bench:
print(f"[deepeval:{a.model}] benchmarks (ifeval n={a.ifeval_n}, "
f"humaneval tasks={a.humaneval_tasks or 'all'}"
f"{', mmlu n=%d' % a.mmlu_n if a.deep else ''}) ...", flush=True)
run_benchmarks(out, subject, judge, a.ifeval_n, a.humaneval_tasks,
a.mmlu_n if a.deep else 0, save=save)
save()
out["complete"] = True
save()
print("\n@@RESULT@@", json.dumps(out))
if __name__ == "__main__":
main()
# evalscope-runner — the BFCL (tool-calling) harness image for suite v5.
# Build ON THE BOX (like apex-llamacpp): docker build -f evalscope.Dockerfile -t evalscope-runner .
# run_fleet.sh mounts run_bfcl.py into it and runs BFCL-v4 offline AST vs the serving model.
#
# Pins that matter (learned the hard way, M6 2026-07-16):
# - bfcl-eval==2026.3.23 — the docs' 2025.10.27.1 is INCOMPATIBLE with evalscope 1.9.0
# (ImportError: _evaluate_single_agentic_entry).
# - libsndfile1 + soundfile — bfcl-eval's qwen handler imports qwen_agent -> soundfile.
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends libsndfile1 && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir evalscope bfcl-eval==2026.3.23 soundfile
ENV PYTHONUNBUFFERED=1
WORKDIR /work
#!/usr/bin/env python3
"""Generate a needle-in-haystack prompt at a target INPUT size (approx tokens).
PUBLIC + stdlib only. Reuses this dir's public Ruby-OSS haystack (prompt.txt) as
filler, the 18 embedded needle sentences, and the fixed Part A / Part B question
block. score_answers.py + answer_key.json are size-agnostic, so they work
UNCHANGED against any prompt this produces.
python3 gen_needle.py --target-tokens 333000 --out /tmp/needle_333k.txt
How sizing works: target chars = target_tokens * chars-per-token (default 4.1,
measured on the Qwen tokenizer: 1.06M chars -> ~255k tok). Filler is tiled to
reach the target; the server reports the ACTUAL prompt_tokens, which is what the
bench labels each level by -- this estimate only needs to be close.
Needle placement: the 6 `early` needles go in the first third of the haystack,
6 `mid` in the second, 6 `late` in the third (matching answer_key buckets), each
spread evenly within its third and snapped to a blank-line boundary so it lands
between source files, exactly like the original prompt.
"""
import argparse
import json
import os
import random
import re
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_CPT = 4.1 # chars per token, Qwen tokenizer (1,060,409 chars ~= 255,193 tok)
TAIL_MARKER = "# END OF KNOWLEDGE BASE"
# Needle VALUES are randomized per run (same shape as the originals) so a published
# answer key can never be memorized by a future model. --seed reproduces a run.
WORDS = ["amber", "basalt", "cobalt", "drift", "ember", "falcon", "garnet", "harbor",
"indigo", "juniper", "krypton", "lagoon", "marble", "nectar", "onyx", "prairie",
"quartz", "raven", "sable", "tundra", "umber", "velvet", "walnut", "yarrow",
"zephyr", "bison", "cedar", "dune", "flint", "grove", "heron", "iris",
"jade", "kelp", "lotus", "moss"]
def _mutate_token(tok, rng):
if tok.isdigit():
first = rng.choice("123456789") if len(tok) > 1 else rng.choice("123456789")
return first + "".join(rng.choice("0123456789") for _ in range(len(tok) - 1))
if tok.isalpha():
if len(tok) <= 2:
return "".join(rng.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(len(tok)))
w = rng.choice(WORDS)
return w.upper() if tok.isupper() else w
return tok
def _mutate_value(value, rng):
"""New value with the same shape: per [-_]-separated token, digits stay digits
(same length), words come from the wordlist (case preserved)."""
return re.sub(r"[A-Za-z]+|\d+", lambda m: _mutate_token(m.group(0), rng), value)
def randomize_key(key, seed):
"""Return a deep-copied key with fresh, unique expected values."""
rng = random.Random(seed)
used = set()
out = []
for n in key:
v = n["expected"]
for _ in range(100):
v = _mutate_value(n["expected"], rng)
if v not in used and v != n["expected"]:
break
used.add(v)
m = dict(n)
m["expected"] = v
out.append(m)
return out
def load_raw():
with open(os.path.join(HERE, "prompt.txt"), encoding="utf-8") as fh:
return fh.read()
def load_key():
with open(os.path.join(HERE, "answer_key.json"), encoding="utf-8") as fh:
return json.load(fh)["needles"]
def extract_needles(raw, key):
"""Return {id: needle_line}. 17 are `> NOTE [id]: ...` lines; canary is the
prose sentence carrying the canary phrase. Both pulled verbatim from prompt.txt."""
out = {}
for n in key:
nid = n["id"]
if nid == "canary":
m = re.search(r"[^\n]*canary phrase[^\n]*", raw)
else:
m = re.search(r">\s*NOTE \[" + re.escape(nid) + r"\][^\n]*", raw)
if not m:
raise SystemExit(f"needle {nid!r} not found in prompt.txt")
line = m.group(0).strip()
if n["expected"] not in line:
raise SystemExit(f"needle {nid!r} line missing expected {n['expected']!r}: {line!r}")
out[nid] = line
return out
def split_doc(raw):
"""Return (head, body, tasks): head = intro thru first `----`; body = the
source haystack; tasks = the `# END OF KNOWLEDGE BASE ...` question block."""
head_end = raw.index("----\n") + len("----\n")
tail_idx = raw.index(TAIL_MARKER)
head = raw[:head_end]
body = raw[head_end:tail_idx]
tasks = raw[tail_idx:]
return head, body, tasks
def strip_needles(body, needles):
for line in needles.values():
body = body.replace(line, "")
return body
def inject(seg, items):
"""Spread `items` evenly through `seg`, each snapped to the next blank line."""
if not items:
return seg
parts = []
prev = 0
for i, note in enumerate(items):
pos = int(len(seg) * (i + 1) / (len(items) + 1))
nl = seg.find("\n\n", pos)
if nl < 0:
nl = len(seg)
parts.append(seg[prev:nl])
parts.append(f"\n\n{note}\n")
prev = nl
parts.append(seg[prev:])
return "".join(parts)
def build(target_tokens=None, cpt=DEFAULT_CPT, seed=None):
"""Build a needle prompt at ~target_tokens input (None = the natural size of
the base haystack). Returns (text, run_key) where run_key = {"seed", "needles"}
with this run's randomized expected values — embed it in the result JSON so
scoring uses the right key."""
raw = load_raw()
base_key = load_key()
base_needles = extract_needles(raw, base_key)
if seed is None:
seed = random.SystemRandom().randrange(1 << 32)
key = randomize_key(base_key, seed)
needles = {new["id"]: base_needles[new["id"]].replace(old["expected"], new["expected"])
for old, new in zip(base_key, key)}
if target_tokens is None:
target_tokens = int(len(raw) / cpt)
head, body, tasks = split_doc(raw)
filler = strip_needles(body, base_needles)
by_bucket = {"early": [], "mid": [], "late": []}
for n in key:
by_bucket[n.get("bucket", "mid")].append(needles[n["id"]])
target_chars = int(target_tokens * cpt)
overhead = len(head) + len(tasks) + sum(len(v) for v in needles.values()) + 64
fill_chars = max(2000, target_chars - overhead)
if len(filler) < fill_chars:
filler = filler * (fill_chars // len(filler) + 1)
filler = filler[:fill_chars]
third = len(filler) // 3
segs = [filler[:third], filler[third:2 * third], filler[2 * third:]]
segs[0] = inject(segs[0], by_bucket["early"])
segs[1] = inject(segs[1], by_bucket["mid"])
segs[2] = inject(segs[2], by_bucket["late"])
text = head + "".join(segs) + "\n\n----\n\n" + tasks
return text, {"seed": seed, "needles": key}
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--target-tokens", type=int, required=True,
help="approximate INPUT token count to build for")
ap.add_argument("--out", required=True, help="output prompt file")
ap.add_argument("--out-key", help="write this run's answer key JSON here")
ap.add_argument("--seed", type=int, help="reproduce a previous run's values")
ap.add_argument("--chars-per-token", type=float, default=DEFAULT_CPT)
a = ap.parse_args()
text, run_key = build(a.target_tokens, a.chars_per_token, seed=a.seed)
with open(a.out, "w", encoding="utf-8") as fh:
fh.write(text)
if a.out_key:
with open(a.out_key, "w", encoding="utf-8") as fh:
json.dump(run_key, fh, indent=2)
print(json.dumps({
"target_tokens": a.target_tokens,
"chars": len(text),
"approx_tokens": round(len(text) / a.chars_per_token),
"seed": run_key["seed"],
"out": a.out,
}))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Box-side memory telemetry feed for remote benchmark clients. Stdlib only.
Appends one JSONL line per --interval seconds:
{"t": <epoch>, "mem_available_mb": <MiB>, "gpu_used_mb": <MiB|null>}
Remote clients point --memwatch-feed / $MEMWATCH_FEED at this file (over a
shared path, sshfs, or a synced copy) — see BENCHMARK.md: metrics are
endpoint-agnostic, safety gating is not; concurrency ramps refuse to run
without a telemetry source.
python3 memwatch.py # writes <results-dir>/memwatch.jsonl
python3 memwatch.py --out /path/x.jsonl --interval 2
"""
import argparse
import json
import os
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import benchlib
MAX_BYTES = 10 * 1024 * 1024 # rotate: keep the tail when the feed grows past this
def rotate_if_needed(path):
try:
if os.path.getsize(path) > MAX_BYTES:
with open(path) as fh:
tail = fh.readlines()[-1000:]
with open(path, "w") as fh:
fh.writelines(tail)
except OSError:
pass
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=os.path.join(benchlib.results_dir(), "memwatch.jsonl"))
ap.add_argument("--interval", type=float, default=2.0)
a = ap.parse_args()
src = benchlib.MemSource("local")
print(f"[memwatch] -> {a.out} every {a.interval}s", flush=True)
while True:
rec = {"t": round(time.time(), 1),
"mem_available_mb": src.available_mb(),
"gpu_used_mb": src.gpu_used_mb()}
with open(a.out, "a") as fh:
fh.write(json.dumps(rec) + "\n")
rotate_if_needed(a.out)
time.sleep(a.interval)
if __name__ == "__main__":
main()
This file has been truncated, but you can view the full file.
# Ruby OSS Ecosystem — Source Knowledge Base (concatenated public sources)
Below is a concatenation of source files from several PUBLIC Ruby open-source projects, used here purely as a long-context haystack. Read it carefully; a set of questions follows at the very end. Interspersed are a few `NOTE [id]:` lines carrying specific facts you may be asked to recall.
----
### rvm/pluginator/lib/pluginator.rb
```ruby
=begin
Copyright 2013-2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "pluginator/extendable_autodetect"
require "pluginator/rubygems_fixes"
require "pluginator/version"
# A simple plugin system based on Gem.find_files
module Pluginator
# Find plugins for the given group
#
# @param group [String] name of plugins group
# @param options [Hash] options to pass to creating Pluginator instance
# @option type [String] name of type to load
# @option prefix [String] a prefix for finding plugins if forcing,
# by default only `/lib` is checked,
# regexp notation is allowed, for example `/(lib|local_lib)`
# @option extend [Array<Symbol>|Symbol] list of extension to extend into pluginator instance
# @return [Pluginator::ExtendableAutodetect] instance of Pluginator
def self.find(group, options={})
Pluginator::ExtendableAutodetect.new(group, options)
end
def self.configured(options={})
Pluginator::Configured.new(options)
end
end
```
### rvm/pluginator/lib/pluginator/autodetect.rb
```ruby
=begin
Copyright 2013-2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "pluginator/errors"
require "pluginator/group"
require "pluginator/name_converter"
module Pluginator
class Autodetect < Group
end
end
require "pluginator/autodetect/formatted_finder"
module Pluginator
# Add autodetection capabilities to Group
# @see Group, FormattedFinder
class Autodetect
# Automatically load plugins for given group (and type)
#
# @param group [String] name of the plugins group
# @param options [Hash] options to pass to creating Pluginator instance
# @option type [String] name of the plugin type
# @option prefix [String] a prefix for finding plugins if forcing,
# by default only `/lib` is checked,
# regexp notation is allowed, for example `/(lib|local_lib)`
def initialize(group, options={})
super(group)
@force_prefix = options[:prefix]
@force_type = options[:type]
refresh
end
# Initiate another lookup for plugins
# - does not clean the state
# - does not resolve all gems, only the new ones
#
# Use it after gem list change, for example after `Gem.install("new_gem")`
def refresh
plugin_lists = FormattedFinder.new(@force_prefix, @group, @force_type)
register_plugins(plugin_lists.loaded_plugins_path)
load_plugins(plugin_lists.load_path_plugins_paths)
activate_plugins(plugin_lists.gem_plugins_paths)
end
# Return the forced type
def type
@plugins[@force_type] unless @force_type.nil?
end
private
include NameConverter
def register_plugins(plugins_to_register)
plugins_to_register.each do |name, type|
register_plugin(type, name2class(name))
end
end
def load_plugins(plugins_to_load)
plugins_to_load.each do |path, name, type|
require path
register_plugin(type, name2class(name))
end
end
def activate_plugins(plugins_to_activate)
selected = active_or_latest_gems_matching(plugins_to_activate.map(&:first).compact)
plugins_to_activate.each do |gemspec, path, name, type|
next unless selected.include?(gemspec)
gemspec.activate
require path
register_plugin(type, name2class(name))
end
end
# filter active / latest gem versions
def active_or_latest_gems_matching(specifications)
specifications.group_by(&:name).map do |_name, plugin_specifications|
active_or_latest_gemspec(plugin_specifications.sort)
end
end
# find active or latest gem in given set
def active_or_latest_gemspec(specifications)
specifications.find(&:activated) || specifications.last
end
end
end
```
### rvm/pluginator/lib/pluginator/configured.rb
```ruby
=begin
Copyright 2024 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator
module ConfigProviders
end
# iterate through all configurations plugins, and return only the ones that are configured
class Configured < ExtendableAutodetect
class ConfigurationMissing < StandardError; end
attr_reader :path, :default_config
def initialize(options={})
@path = options.delete(:path) || File.dirname(caller[0].split(":")[0])
group = Pluginator::ConfigProviders::Yaml.group_for(path)
raise ConfigurationMissing, "no configuration found for #{path}" unless group
super(group, options)
@default_config = Pluginator::ConfigProviders::Yaml.config
end
def configured(force_type = @force_type)
raise ArgumentError, "force_type required" unless force_type
end
private
def todo
end
end
end
```
### rvm/pluginator/lib/pluginator/errors.rb
```ruby
=begin
Copyright 2013-2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator
# base error for all Pluginator errors
class PluginatorError < RuntimeError
private
def list_to_s(list)
list.map { |plugin| plugin.to_s.inspect }.join(", ")
end
end
# raised when plugin can not be found, generated by `*!` methods
class MissingPlugin < PluginatorError
# initialize new error
# @param type [String] type of the loaded plugin
# @param name [String] name of the loaded plugin
# @param list [Array] list of available plugins
def initialize(type, name, list)
super("Can not find plugin #{name.inspect} in #{list_to_s(list)} for type #{type.inspect}.")
end
end
# raised when type can not be found, generated by `*!` methods
class MissingType < PluginatorError
# initialize new error
# @param type [String] type of the loaded plugin
# @param list [Array] list of available types
def initialize(type, list)
super("Can not find type #{type.inspect} in #{list_to_s(list)}.")
end
end
end
```
### rvm/pluginator/lib/pluginator/extendable_autodetect.rb
```ruby
=begin
Copyright 2013-2024 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "pluginator/autodetect"
module Pluginator
# Container for all Pluginator extensions,
# they are loaded in `ExtendableAutodetect`
# @see ExtendableAutodetect#extend_plugins
module Extensions
end
# Add extendability to Autodetect / Group
# @see Autodetect
# @see Group
class ExtendableAutodetect < Autodetect
# Automatically load plugins for given group (and type)
# Extend instance with extensions if given.
#
# @param group [String] name of the plugins group
# @param options [Hash] options to pass to creating Pluginator instance
# @option type [String] name of type to load
# @option extends [Array<Symbol>|Symbol] list of extension to extend into pluginator instance
def initialize(group, options={})
super(group, options)
extend_plugins(options[:extends] || [])
end
# Extend pluginator instance with given extensions
#
# @param extends [Array<Symbol>|Symbol] list of extension to extend into pluginator instance
def extend_plugins(extends)
extensions_matching(extends).each do |plugin|
extend plugin
end
end
private
def pluginator_plugins
@pluginator_plugins ||= begin
plugins = Pluginator::Autodetect.new("pluginator")
plugins.extend(Pluginator::Extensions::Matching)
plugins
end
end
def extensions_matching(extends)
extends = [extends].flatten.map(&:to_s)
pluginator_plugins.matching!("extensions", extends)
end
end
end
```
### rvm/pluginator/lib/pluginator/group.rb
```ruby
=begin
Copyright 2013 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator
# Initial data for pluginator, includes group name and plugins
class Group
# Group name used for plugins
attr_reader :group
# sets up new instance and initial configuration
# @param group [String] name of the plugins group
def initialize(group)
setup_group(group)
end
# @param [String] type of plugins to select
# @return [Array] list of plugins for type
def [](type)
@plugins[type.to_s]
end
# @return [Array] list of plugin types loaded
def types
@plugins.keys
end
# Register a new plugin, can be used to load custom plugins
#
# @param type [String] type for the klass
# @param klass [Class] klass of the plugin to add
def register_plugin(type, klass)
type = type.to_s
@plugins[type] ||= []
@plugins[type].push(klass) unless @plugins[type].include?(klass)
end
private
def setup_group(group)
@plugins = {}
@group = group
end
end
end
```
### rvm/pluginator/lib/pluginator/method.rb
```ruby
=begin
Copyright 2024 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator
module Method
def self.included(other)
other.extend ClassMethods
end
module ClassMethods
def pluginator(def_method, strategy_name, type, call_method)
path = File.dirname(caller[0].split(":")[0])
define_method(def_method) do
Pluginator::Method.call(type, strategy_name, call_method, path: path)
end
end
end
def self.call(type, strategy_name, call_method, path: nil, group: Pluginator::Configured.group(path))
raise ArgumentError, "group or path required" unless group
plugins = Pluginator.find(group, type: type, extends: :configured).configured
strategy(strategy_name).call(plugins, call_method)
end
private
def self.strategy(strategy_name)
@strategies ||= {}
@strategies[strategy_name] ||=
Pluginator.find('pluginator', type: 'method/strategies', extends: :first_ask).first_ask(:supports?, strategy_name)
end
end
end
```
### rvm/pluginator/lib/pluginator/name_converter.rb
```ruby
=begin
Copyright 2013-2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator
# a helper for handling name / file / class conversions
module NameConverter
private
# full_name => class
def name2class(name)
klass = Kernel
name.to_s.split(%r{/}).each do |part|
klass = klass.const_get(
part.capitalize.gsub(/[_-](.)/) { |match| match[1].upcase }
)
end
klass
end
end
end
```
### rvm/pluginator/lib/pluginator/rubygems_fixes.rb
```ruby
unless Gem.respond_to? :find_files_from_load_path
# older versions of rubygems od not have separate method for finding only on $LOAD_PATH
# this is copy/paste from rubygems 2.0.0 code
# :nocov: not testing as it runs only on old rubygems, it's not even our code
module Gem
def self.find_files_from_load_path(glob)
$LOAD_PATH.map do |load_path|
Dir["#{File.expand_path glob, load_path}#{Gem.suffix_pattern}"]
end.flatten.select do |file| # rubocop:disable Style/MultilineBlockChain
File.file? file.untaint
end
end
end
# :nocov:
end
```
### rvm/pluginator/lib/pluginator/version.rb
```ruby
=begin
Copyright 2013-2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator
# Version of Pluginator
VERSION = "1.5.0".freeze
end
```
### rvm/pluginator/lib/pluginator/autodetect/finder.rb
```ruby
=begin
Copyright 2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator
class Autodetect
# Find plugins
class Finder
attr_reader :loaded_plugins_path, :load_path_plugins_paths, :gem_plugins_paths
# Automatically load plugins for given group (and type)
#
# @param force_prefix [String] a prefix for finding plugins if forcing,
# by default only `/lib` is checked,
# regexp notation is allowed, for example `/[lib|]`
# @param group [String] name of the plugins group
# @param force_type [String] name of the plugin type if forcing
def initialize(force_prefix, group, force_type)
@force_prefix = force_prefix
@group = group
@force_type = force_type
@pattern = file_name_pattern
find_paths
end
private
# group => pattern
def file_name_pattern
"plugins/#{@group}/#{@force_type || "**"}/*.rb"
end
def find_paths
@loaded_plugins_path = find_loaded_plugins
@load_path_plugins_paths = find_load_path_plugins - @loaded_plugins_path
@gem_plugins_paths = find_gem_plugins - @load_path_plugins_paths - @loaded_plugins_path
end
def find_loaded_plugins
split_file_names(
$LOADED_FEATURES
).compact
end
def find_load_path_plugins
split_file_names(
Gem.find_files_from_load_path(@pattern)
)
end
def find_gem_plugins
split_file_names(
Gem.find_files(@pattern, false)
)
end
def split_file_names(file_names)
file_names.map do |file_name|
split_file_name(file_name)
end
end
# file_name => [ path, full_name, type ]
def split_file_name(file_name)
prefix = @force_prefix || "/lib"
type = @force_type || ".*"
match = file_name.match(%r{.*#{prefix}/(plugins/(#{@group}/(#{type})/[^/]*)\.rb)$})
match[-3..-1] if match
end
end
end
end
```
### rvm/pluginator/lib/pluginator/autodetect/formatted_finder.rb
```ruby
=begin
Copyright 2017-2024 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "pluginator/autodetect/finder"
module Pluginator
class Autodetect
# Categorize plugins
# @see: Finder
class FormattedFinder < Finder
# Reformat plugin lists
def initialize(force_prefix, group, force_type)
super
map_loaded_plugins
map_gem_plugins
end
private
def gem_has_method?(name)
Gem.methods.map(&:to_sym).include?(name)
end
def map_loaded_plugins
@loaded_plugins_path.map! do |_path, name, type|
[name, type]
end
end
def map_gem_plugins
gem_specifications = find_gem_specifications
@gem_plugins_paths.map! do |path, name, type|
gemspec = gemspec_for_path(path, gem_specifications)
[gemspec, path, name, type] if gemspec
end.compact
end
def find_gem_specifications
if gem_has_method?(:gemdeps) && Gem.gemdeps
then
# :nocov: only testable with using rubygems's gemdeps feature
Gem.loaded_specs.values.to_a
# :nocov:
else
specs = Gem::Specification._all.to_a
specs = (Gem.loaded_specs.values.to_a + specs).uniq if gem_has_method?(:loaded_specs)
specs
end
end
def gemspec_for_path(path, specifications)
gemspecs = gemspecs_for_path(path, specifications)
case
gemspecs.size
when 0
nil
when 1
gemspecs.first
else
find_latest_plugin_version(gemspecs, path)
end
end
def gemspecs_for_path(path, specifications)
specifications.reject do |spec|
Dir.glob( File.join( spec.lib_dirs_glob, path ) ).empty?
end
end
def find_latest_plugin_version(gemspecs, path)
active_or_latest_gemspec(gemspecs_sorted_by_metadata_and_version(gemspecs, path))
end
# find active or latest gem in given set
def active_or_latest_gemspec(specifications)
specifications.find(&:activated) || specifications.last
end
def gemspecs_sorted_by_metadata_and_version(gemspecs, path)
gemspecs.sort_by do |spec|
[calculate_plugin_version(spec.metadata, path), spec.name, spec.version]
end
end
def calculate_plugin_version(metadata, path)
( (metadata || {})[path] || "0" ).to_i
end
end
end
end
```
### rvm/pluginator/lib/plugins/pluginator/config_providers/yaml.rb
```ruby
=begin
Copyright 2024 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator::ConfigProviders
# Extension to check if plugin for given class name exist
class Yaml
CONFIG_FILES = %w[.pluginator.yml config/pluginator.yml].freeze
class << self
attr_reader :config
def group_for(path)
if defined?(@group)
@group
else
config_path, config_file = find_config_for_path(path)
if config_path && config_file
@config = YAML.load_file(File.join(config_path, config_file))
@group = @config['group']
end
end
end
private
# check each directory (longest to shortest) in path for .pluginator.yml or config/pluginator.yml
# @param path String path to search config for
# @return [String, String]|nil the found configuration path and file or nil if not found
def find_config_for_path(path)
current_path = path
current_path = File.dirname(current_path) if File.file?(current_path)
while current_path != "/"
CONFIG_FILES.each do |config_file|
return [current_path, config_file] if File.exist?(File.join(current_path, config_file))
end
current_path = File.dirname(current_path)
end
nil
end
end
def config_for(group, type)
self.class.config['plugins'][group][type]
end
end
end
```
### rvm/pluginator/lib/plugins/pluginator/extensions/class_exist.rb
```ruby
=begin
Copyright 2013 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "plugins/pluginator/extensions/plugins_map"
require "plugins/pluginator/extensions/conversions"
module Pluginator::Extensions
# Extension to check if plugin for given class name exist
module ClassExist
include PluginsMap
include Conversions
# Check if plugin for given name exists.
#
# @param type [String] name of type to search for plugins
# @param klass [Symbol or String] name of the searched class
# @return [Boolean] klass exists
def class_exist?(type, klass)
!!(plugins_map(type) || {})[string2class(klass)]
end
end
end
```
### rvm/pluginator/lib/plugins/pluginator/extensions/conversions.rb
```ruby
=begin
Copyright 2013-2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
module Pluginator::Extensions
# a placeholder for methods to convert strings
module Conversions
# converts class name to a file name
# @param klass [String] class like string
# @return [String] file like string
def class2string(klass)
klass.to_s.gsub(/([A-Z])/m) { |match| "_#{match.downcase}" }[1..-1]
end
# converts file name to a class name
# @param str [String] file like string
# @return [String] class like string
def string2class(str)
str.to_s.capitalize.gsub(/_(.)/) { |match| match[1].upcase }
end
# gets class name last part
# @param klass [Class] class to read
# @return [String] the class name only without nesting
def class2name(klass)
klass.name.split("::").last
end
end
end
```
### rvm/pluginator/lib/plugins/pluginator/extensions/first_ask.rb
```ruby
=begin
Copyright 2013-2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "plugins/pluginator/extensions/plugins_map"
module Pluginator::Extensions
# Extension to find first plugin that answers the question with true
module FirstAsk
include PluginsMap
# Call a method on plugin and return first one that returns `true`.
#
# @param type [String] name of type to search for plugins
# @param method_name [Symbol] name of the method to execute
# @param params [Array] params to pass to the called method
# @return [Class] The first plugin that method call returns true
def first_ask(type, method_name, *params)
@plugins[type] or return nil
try_to_find(type, method_name, params)
end
# Call a method on plugin and return first one that returns `true`.
# Behaves like `first_ask` but throws exceptions if can not find anything.
# @param type [String] name of type to search for plugins
# @param method_name [Symbol] name of the method to execute
# @param params [Array] params to pass to the called method
# @return [Class] The first plugin that method call returns true
# @raise [Pluginator::MissingPlugin] when can not find plugin
def first_ask!(type, method_name, *params)
@plugins[type] or raise Pluginator::MissingType.new(type, @plugins.keys)
try_to_find(type, method_name, params) or
raise Pluginator::MissingPlugin.new(type, "first_ask: #{method_name}", plugins_map(type).keys)
end
private
def try_to_find(type, method_name, params)
@plugins[type].detect do |plugin|
has_public_method?(plugin, method_name) and
plugin.send(method_name.to_sym, *params)
end
end
# need to use this trick because of old rubies support
def has_public_method?(plugin, method_name)
plugin.public_methods.map(&:to_sym).include?(method_name.to_sym)
end
end
end
```
> NOTE [canary]: Operational note: the canary phrase for this knowledge base is MARMOT-LANTERN-4471; quote it exactly when asked to confirm full-context access.
### rvm/pluginator/lib/plugins/pluginator/extensions/first_class.rb
```ruby
=begin
Copyright 2013 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "plugins/pluginator/extensions/plugins_map"
require "plugins/pluginator/extensions/conversions"
module Pluginator::Extensions
# Extension to find first plugin whose class matches the string
module FirstClass
include PluginsMap
include Conversions
# Find first plugin whose class matches the given name.
#
# @param type [String] name of type to search for plugins
# @param klass [Symbol|String] name of the searched class
# @return [Class] The first plugin that matches the klass
def first_class(type, klass)
(plugins_map(type) || {})[string2class(klass)]
end
# Find first plugin whose class matches the given name.
# Behaves like `first_class` but throws exceptions if can not find anything.
# @param type [String] name of type to search for plugins
# @param klass [Symbol|String] name of the searched class
# @return [Class] The first plugin that matches the klass
# @raise [Pluginator::MissingPlugin] when can not find plugin
def first_class!(type, klass)
@plugins[type] or raise Pluginator::MissingType.new(type, @plugins.keys)
klass = string2class(klass)
plugins_map(type)[klass] or
raise Pluginator::MissingPlugin.new(type, klass, plugins_map(type).keys)
end
end
end
```
### rvm/pluginator/lib/plugins/pluginator/extensions/matching.rb
```ruby
=begin
Copyright 2013 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "plugins/pluginator/extensions/plugins_map"
require "plugins/pluginator/extensions/conversions"
module Pluginator::Extensions
# Extension to select plugins that class name matches the list of string
module Matching
include PluginsMap
include Conversions
# Map array of names to available plugins.
#
# @param type [String] name of type to search for plugins
# @param list [Array] list of plugin names to load
# @return [Array] list of loaded plugins
def matching(type, list)
list.map do |plugin|
(plugins_map(type) || {})[string2class(plugin)]
end
end
# Map array of names to available plugins.
# Behaves like `matching` but throws exceptions if can not find anything.
# @param type [String] name of type to search for plugins
# @param list [Array] list of plugin names to load
# @return [Array] list of loaded plugins
# @raise [Pluginator::MissingPlugin] when can not find plugin
def matching!(type, list)
@plugins[type] or raise Pluginator::MissingType.new(type, @plugins.keys)
list.map do |plugin|
plugin = string2class(plugin)
plugins_map(type)[plugin] or
raise Pluginator::MissingPlugin.new(type, plugin, plugins_map(type).keys)
end
end
end
end
```
### rvm/pluginator/lib/plugins/pluginator/extensions/plugins_map.rb
```ruby
=begin
Copyright 2013-2017 an OSS contributor <dev@example.invalid>
This file is part of pluginator.
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
=end
require "plugins/pluginator/extensions/conversions"
module Pluginator::Extensions
# extend Pluginator with map of plugins: name => klass
module PluginsMap
include Conversions
# provide extra map of plugins with symbolized names as keys
#
# @param type [String] name of type to generate the map for
# @return [Hash] map of the names and plugin classes
def plugins_map(type)
@plugins_map ||= {}
type = type.to_s
@plugins_map[type] ||= Hash[
@plugins[type].map do |plugin|
[class2name(plugin), plugin]
end
]
end
end
end
```
### rvm/pluginator/README.md
```ruby
# Pluginator
[![Gem Version](https://badge.fury.io/rb/pluginator.png)](http://rubygems.org/gems/pluginator)
[![Code Climate](https://codeclimate.com/github/rvm/pluginator.png)](https://codeclimate.com/github/rvm/pluginator)
[![Coverage Status](https://coveralls.io/repos/rvm/pluginator/badge.png)](https://coveralls.io/r/rvm/pluginator)
[![Build Status](https://travis-ci.org/rvm/pluginator.png)](https://travis-ci.org/rvm/pluginator)
[![Dependency Status](https://gemnasium.com/rvm/pluginator.png)](https://gemnasium.com/rvm/pluginator)
[![Inline docs](http://inch-ci.org/github/rvm/pluginator.png)](http://inch-ci.org/github/rvm/pluginator)
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/rvm/pluginator/master/frames)
[![Github Code](http://img.shields.io/badge/github-code-blue.svg)](https://github.com/rvm/pluginator)
Gem plugin system management, detects plugins using `Gem.find_file`,
`$LOAD_PATH` and `$LOADED_FEATURES`.
Pluginator works with *ruby 1.9.3+* and *rubygems 2.0.0+*.
Pluginator tries to stay out of your way, you do not have to include or inherit anything.
Pluginator only finds and groups plugins, rest is up to you,
you decide what methods to define and how to find them.
## Defining plugins
create a gem with a path:
```ruby
lib/plugins/<group>/<type>/<name>.rb
```
with a class inside:
```ruby
<group>::<type>::<name>
```
where `<type>` can be nested
## Loading plugins
```ruby
rvm2plugins = Pluginator.find("<group>")
type_plugins = rvm2plugins["<type>"]
types = rvm2plugins.types
```
## Usage
```ruby
Pluginator.find("<group>") => Pluginator object
plugins = Pluginator.find("<group>", type: "<type>", prefix: "/(lib|local_lib)", extends: %i[<extensions>])
plugins["<type>"] => Array of plugins
plugins.type => Array of plugins for type defined with `type: "<type>"`
plugins.types => Array of types
```
- `"<group>"` - Load plugins for given group.
- `type: "<type>"` - Load plugins only of given type, optional, makes `type` method accessible.
- `prefix: "/prefix"` - Load plugins only from this paths, optional, default `/lib`.
- `extends: %i[<extensions>]` - Extend pluginator with given extensions.
## Extensions
Pluginator comes with few handful extensions.
### Class exist
Check if plugin with given class name exists.
```ruby
plugins = Pluginator.find("<group>", extends: %i[class_exist])
plugins.class_exist?("<type>", "<name>") => true or false
```
### First ask
Call a method on plugin and return first one that returns `true`.
```ruby
plugins = Pluginator.find("<group>", extends: %i[first_ask])
plugins.first_ask( "<type>", "method_to_call", *params) => plugin or nil
plugins.first_ask!("<type>", "method_to_call", *params) => plugin or exception PluginatorError
```
### First class
Find first plugin that class matches the given name.
```ruby
plugins = Pluginator.find("<group>", extends: %i[first_class])
plugins.first_class( "<type>", "<name>") => plugin or nil
plugins.first_class!("<type>", "<name>") => plugin or exception PluginatorError
```
### Matching
Map array of names to available plugins.
```ruby
plugins = Pluginator.find("<group>", extends: %i[matching])
plugins.matching( "<type>", [<array_of_names>]) => [plugins] # nil for missing ones
plugins.matching!("<type>", [<array_of_names>]) => [plugins] or exception PluginatorError
```
### Your own ones
You can define your own extensions for `pluginator`, for example:
```shell
plugins/pluginator/extensions/first_one.rb
```
with:
```ruby
module Pluginator::Extensions
class FirstOne
def first_one(type)
@plugins[type].first
end
end
end
```
And now you can use it:
```ruby
plugins = Pluginator.find("<group>", extends: %i[first_one])
plugins.first_one("<type>") => first_plugin # nil when none
```
## Exceptions
- `PluginatorError` - base error for all Pluginator errors
- `MissingPlugin` - raised when plugin can not be found, generated by `*!` methods
- `MissingType` - raised when type can not be found, generated by `*!` methods
## Versioning plugins
In case plugin gets moved to other gem you can specify which gem to
use for loading the plugin by specifying plugin version in gems gemspec
metadata:
```ruby
s.metadata = {
"plugins/v2test/stats/max.rb" => "1"
}
```
## Examples
### Example 1 - task plugins
`plugins/rvm2/cli/echo.rb`:
```ruby
class Rvm2::Cli::Echo
def self.question? command
command == "echo"
end
def answer param
puts param
end
end
```
where `question?` and `answer` are user defined methods
Now the plugin can be used:
```ruby
require "pluginator"
rvm2plugins = Pluginator.find("rvm2")
plugin = rvm2plugins["cli"].first{ |plugin|
plugin.question?("echo")
}
plugin.new.answer("Hello world")
```
Or using extensions:
```ruby
require "pluginator"
plugin = Pluginator.find("rvm2", extends: %i[first_ask]).first_ask("cli", &:question?, "echo")
plugin.new.answer("Hello world")
```
### Example 2 - hook plugins
`plugins/rvm2/hooks/after_install/show.rb`:
```ruby
class Rvm2::Hooks::AfterInstall::Show
def self.execute name, path
puts "Ruby #{name.inspect} was installed in #{path.inspect}."
end
end
```
and using hooks:
```ruby
require "pluginator"
Pluginator.find("rvm2", type: "hooks/after_install").type.each{ |plugin|
plugin.execute(name, path)
}
```
## Testing
```bash
NOEXEC_DISABLE=1 rake test
```
## License
Copyright 2013-2024 an OSS contributor <dev@example.invalid>
pluginator is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pluginator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with pluginator. If not, see <http://www.gnu.org/licenses/>.
For details on adding copyright visit:
https://www.gnu.org/licenses/gpl-howto.html
```
### oss/uncov/lib/uncov.rb
```ruby
# frozen_string_literal: true
Dir["#{File.dirname(__FILE__)}/**/*.rb"]
.sort_by { |f| f.count('/') }
.each { |f| require f unless f.end_with?('lib/uncov.rb') }
# uncover missing code coverage by tests
module Uncov
class << self
def configure(args = [])
yield(configuration) if block_given?
configuration.parse_cli(args) if args.any?
warn("{configuration: #{configuration.options_values.inspect}}") if configuration.debug
nil
end
def configuration
@configuration ||= Configuration.new
end
def configuration_reset!
@configuration = Configuration.new
end
def plugins
@plugins ||= Pluginator.find('uncov', extends: ['plugins_map'])
end
end
class Error < StandardError
def inspect = "#<#{self.class}: #{message}>"
end
class ConfigurationError < Error; end
class GitError < Error; end
class FinderError < Error; end
class SimplecovError < FinderError; end
class FormatterError < Error; end
class ReportError < Error; end
class OptionValueNotAllowed < ConfigurationError; end
class NotGitRepoError < GitError
attr_reader :path
def initialize(path) = @path = path
def message = "#{path.inspect} is not in a git working tree"
end
class NotGitObjectError < GitError
attr_reader :target_branch
def initialize(target_branch) = @target_branch = target_branch
def message = "Git target #{target_branch.inspect} not found locally"
end
class UnsupportedSimplecovTriggerError < FinderError
attr_reader :trigger
def initialize(trigger) = @trigger = trigger
def message = "#{trigger.inspect} is not a supported simplecov_trigger type"
end
class FailedToGenerateReport < SimplecovError
def message = cause.message
end
class MissingSimplecovReport < SimplecovError
attr_reader :coverage_path
def initialize(coverage_path) = @coverage_path = coverage_path
def message = "SimpleCov results not found at #{coverage_path.inspect}"
end
class AutodetectSimplecovPathError < SimplecovError
def message = 'Could not autodetect coverage report path'
end
class UnsupportedFormatterError < FormatterError
attr_reader :output_format
def initialize(output_format) = @output_format = output_format
def message = "#{output_format.inspect} is not a supported formatter"
end
class UnsupportedReportTypeError < ReportError
attr_reader :type
def initialize(type) = @type = type
def message = "#{type.inspect} is not a supported report type"
end
end
```
### oss/uncov/lib/plugins/uncov/formatter/terminal.rb
```ruby
# frozen_string_literal: true
require 'colorize'
# print report to terminal with colors
class Uncov::Formatter::Terminal
include Uncov::Cache
attr_reader :report
def initialize(report)
@report = report
end
def output
return puts final_message.green if report.files.empty? || !report.trigger?
output_header
output_files
output_summary
end
def output_header
puts "Files(#{report.display_files.size}) with uncovered changes:".yellow
end
def output_files
report.display_files.each do |file_coverage|
output_file(file_coverage)
end
end
def output_file(file_coverage)
puts
output_file_header(file_coverage)
max = number_length(file_coverage)
file_coverage.display_lines.each do |line|
output_line(line, max)
end
end
def output_file_header(file_coverage)
puts format(
'%<name>s -> %<coverage>.2f%% (%<covered_lines>d / %<relevant_lines>d) changes covered, uncovered lines:',
name: file_coverage.file_name,
coverage: file_coverage.coverage,
covered_lines: file_coverage.covered_lines_count,
relevant_lines: file_coverage.relevant_lines_count
).yellow
end
def output_line(line, max)
if line.uncov?
puts format_line(line, max).red
elsif line.nocov_covered?
puts format_line(line, max).blue
elsif line.context
puts format_line(line, max).green
else
# :nocov:
raise 'unknown display line' # unreachable code
# :nocov:
end
end
def format_line(line, max)
format("%#{max}d: %s", line.number, line.content)
end
def number_length(file_coverage)
file_coverage.display_lines.last.number.to_s.length
end
def output_summary
puts
puts final_message.yellow
end
def final_message
format(
'%<report_filter> coverage of files(%<files_count>): %<coverage>.2f%% (%<covered_lines>d / %<relevant_lines>d)',
report_filter: Uncov.configuration.report,
files_count: report.files.count,
coverage: report.coverage,
covered_lines: report.covered_lines_count,
relevant_lines: report.relevant_lines_count
)
end
end
```
### oss/uncov/lib/plugins/uncov/report/filters/diff_files.rb
```ruby
# frozen_string_literal: true
# report only files lines from the diff
module Uncov::Report::Filters::DiffFiles
class << self
def description = 'Report missing coverage on added/changed files in the git diff'
def simplecov_trigger = :git_diff
def files(finder)
finder.git_diff_files.file_names.map do |file_name|
Uncov::Report::File.new(
file_name:,
git: true,
lines: lines(finder, file_name)
)
end
end
private
def lines(finder, file_name)
lines_hash = file_lines(finder, file_name)
Uncov::Report::Context.add_context(finder, file_name, lines_hash)
lines_hash.sort.to_h.values
end
def file_lines(finder, file_name)
finder.file_system_files.lines(file_name).keys.to_h do |line_number|
[line_number, finder.build_line(file_name, line_number)]
end
end
end
end
```
### oss/uncov/lib/plugins/uncov/report/filters/diff_lines.rb
```ruby
# frozen_string_literal: true
# report only files lines from the diff
module Uncov::Report::Filters::DiffLines
class << self
def description = 'Report missing coverage on added lines in the git diff'
def simplecov_trigger = :git_diff
def files(finder)
finder.git_diff_files.file_names.map do |file_name|
Uncov::Report::File.new(
file_name:,
git: true,
lines: lines(finder, file_name)
)
end
end
private
def lines(finder, file_name)
lines_hash = git_diff_files_lines(finder, file_name)
Uncov::Report::Context.add_context(finder, file_name, lines_hash)
lines_hash.sort.to_h.values
end
def git_diff_files_lines(finder, file_name)
finder.git_diff_files.lines(file_name).keys.to_h do |line_number|
[line_number, finder.build_line(file_name, line_number)]
end
end
end
end
```
### oss/uncov/lib/plugins/uncov/report/filters/file_system.rb
```ruby
# frozen_string_literal: true
# report all files lines from the file system
module Uncov::Report::Filters::FileSystem
class << self
def description = 'Report missing coverage on file system'
def simplecov_trigger = :file_system
def files(finder)
finder.file_system_files.file_names.map do |file_name|
Uncov::Report::File.new(
file_name:,
git: finder.git_files.file?(file_name),
lines: lines(finder, file_name)
)
end
end
private
def lines(finder, file_name)
lines_hash = file_lines(finder, file_name)
Uncov::Report::Context.add_context(finder, file_name, lines_hash)
lines_hash.sort.to_h.values
end
def file_lines(finder, file_name)
finder.file_system_files.lines(file_name).keys.to_h do |line_number|
[line_number, finder.build_line(file_name, line_number)]
end
end
end
end
```
### oss/uncov/lib/plugins/uncov/report/filters/git_files.rb
```ruby
# frozen_string_literal: true
# report only files lines from the diff
module Uncov::Report::Filters::GitFiles
class << self
def description = 'Report missing coverage on files tracked with git'
def simplecov_trigger = :git
def files(finder)
finder.git_files.file_names.map do |file_name|
Uncov::Report::File.new(
file_name:,
git: true,
lines: lines(finder, file_name)
)
end
end
private
def lines(finder, file_name)
lines_hash = file_lines(finder, file_name)
Uncov::Report::Context.add_context(finder, file_name, lines_hash)
lines_hash.sort.to_h.values
end
def file_lines(finder, file_name)
finder.file_system_files.lines(file_name).keys.to_h do |line_number|
[line_number, finder.build_line(file_name, line_number)]
end
end
end
end
```
### oss/uncov/lib/plugins/uncov/report/filters/nocov_lines.rb
```ruby
# frozen_string_literal: true
# report only files lines from the diff
module Uncov::Report::Filters::NocovLines
class << self
def description = 'Report coverage on nocov lines, requires one or both: --nocov-ignore / --nocov-covered'
def simplecov_trigger = :file_system
def files(finder)
finder.nocov_files.file_names.filter_map do |file_name|
next if finder.nocov_files.lines(file_name).empty?
Uncov::Report::File.new(
file_name:,
git: finder.git_files.file?(file_name),
lines: lines(finder, file_name)
)
end
end
private
def lines(finder, file_name)
lines_hash = nocov_files_lines(finder, file_name)
Uncov::Report::Context.add_context(finder, file_name, lines_hash)
lines_hash.sort.to_h.values
end
def nocov_files_lines(finder, file_name)
finder.nocov_files.lines(file_name).keys.to_h do |line_number|
[line_number, finder.build_line(file_name, line_number)]
end
end
end
end
```
### oss/uncov/lib/plugins/uncov/report/filters/simplecov.rb
```ruby
# frozen_string_literal: true
# report only files lines from the simplecov
module Uncov::Report::Filters::Simplecov
class << self
def description = 'Report missing coverage on files tracked with simplecov'
def simplecov_trigger = :file_system
def files(finder)
finder.simplecov_files.file_names.map do |file_name|
Uncov::Report::File.new(
file_name:,
git: finder.git_files.file?(file_name),
lines: lines(finder, file_name)
)
end
end
private
def lines(finder, file_name)
lines_hash = file_lines(finder, file_name)
Uncov::Report::Context.add_context(finder, file_name, lines_hash)
lines_hash.sort.to_h.values
end
def file_lines(finder, file_name)
finder.file_system_files.lines(file_name).keys.to_h do |line_number|
[line_number, finder.build_line(file_name, line_number)]
end
end
end
end
```
### oss/uncov/lib/uncov/cache.rb
```ruby
# frozen_string_literal: true
# A caching helper for methods
module Uncov::Cache
protected
def cache(key)
@cache ||= {}
return @cache[key] if @cache.key?(key)
@cache[key] = yield
end
end
```
### oss/uncov/lib/uncov/cli.rb
```ruby
# frozen_string_literal: true
require 'optparse'
# provide terminal interface for uncov
class Uncov::CLI
def self.start(args)
Uncov.configure(args)
report = Uncov::Report.generate
Uncov::Formatter.output(report)
!report.trigger?
rescue StandardError => e
raise if Uncov.configuration.debug
warn e.message
nil
end
end
```
### oss/uncov/lib/uncov/configuration.rb
```ruby
# frozen_string_literal: true
require_relative 'formatter'
require_relative 'report/filters'
# handle configuration for uncov
class Uncov::Configuration
CONFIG_FILE = '.uncov'
# equivalent of `shopt -s extglob dotglob globstar` for testing with `bash` & `ls`
FILE_MATCH_FLAGS = File::FNM_EXTGLOB | File::FNM_PATHNAME | File::FNM_DOTMATCH
class << self
def option(name, description, options:, default:, allowed_values: nil, value_parse: ->(value) { value })
self.options << [name, description, options, default, allowed_values, value_parse]
define_method(name) { self.options[name].value }
define_method("#{name}=") { |value| self.options[name].value = value }
end
def options = @options ||= []
end
option 'target', 'Target branch for comparison', options: ['-t', '--target TARGET'], default: 'HEAD'
option 'report', 'Report filter to generate file/line list',
options: ['-r', '--report FILTER'], default: 'DiffLines', allowed_values: -> { Uncov::Report::Filters.filters.keys }
option 'output_format', 'Output format',
options: ['-o', '--output-format FORMAT'], default: 'Terminal', allowed_values: -> { Uncov::Formatter.formatters.keys }
option 'context', 'Additional lines context in output',
options: ['-C', '--context LINES_NUMBER'], default: 1, value_parse: lambda(&:to_i)
option 'test_command', 'Test command that generates SimpleCov',
options: '--test-command COMMAND', default: 'COVERAGE=true bundle exec rake test'
option 'simplecov_file', 'SimpleCov results file', options: '--simplecov-file PATH', default: 'autodetect'
option 'relevant_files', 'Only show uncov for matching code files AND trigger tests if matching code files are newer than the report',
options: '--relevant-files FN_GLOB', default: '{{bin,exe,exec}/*,{app,lib}/**/*.{rake,rb},Rakefile}'
option 'relevant_tests', 'Trigger tests if matching test files are newer than the report',
options: '--relevant-tests FN_GLOB', default: '{test,spec}/**/*_{test,spec}.rb'
option 'nocov_ignore', 'Ignore :nocov: markers - consider all lines',
options: '--nocov-ignore', default: false, value_parse: ->(_value) { true }
option 'nocov_covered', 'Report :nocov: lines that have coverage',
options: '--nocov-covered', default: false, value_parse: ->(_value) { true }
option 'debug', 'Get some insights', options: '--debug', default: false, value_parse: ->(_value) { true }
def initialize
define_options
parse_config
end
def parse_cli(args) = parser.parse!(args)
def options_values = options.to_h { |name, option| [name.to_sym, option.value] }
private
def define_options
self.class.options.each do |name, description, options, default, allowed_values, value_parse|
self.options[name] = Option.new(name, description, options, default, allowed_values, value_parse)
end
end
def parse_config
return unless File.exist?(CONFIG_FILE)
args = File.readlines(CONFIG_FILE).map(&:strip)
parse_cli(args)
end
def parser
@parser ||=
OptionParser.new do |parser|
parser_header(parser)
options.each_value { |option| option.on_parser(parser) }
parser_footer(parser)
end
end
def parser_header(parser) = parser.banner = 'Usage: uncov [options]'
def parser_footer(parser)
parser.on('-h', '--help', 'Print this help') do
puts parser.help
throw :exit, 0
end
footer_extras(parser)
end
def footer_extras(parser)
# TODO: the release workflow does not like ' in help, please avoid it - or fix the workflow
parser.separator <<~HELP
Report FILTERs:
#{footer_extras_types}
Report FILTERs take NOTICE:
git*/diff* - filters will not consider new files unless added to the git index with `git add`.
nocov* - filters/flags only work with coverage/.resultset.json SimpleCov files,
coverage.json does not provide the information needed.
FN_GLOB: shell filename globing -> https://ruby-doc.org/core-3.1.1/File.html#method-c-fnmatch
in bash: `shopt -s extglob dotglob globstar` and test with `ls {app,lib}/**/*.rb`
uncov #{Uncov::VERSION} by an OSS contributor <dev@example.invalid>
HELP
end
def footer_extras_types
report_type_length = Uncov::Report::Filters.filters.keys.map(&:length).max
Uncov::Report::Filters.filters.map do |name, filter|
format("%#{report_type_length}s - %s", name, filter.description)
end.join("\n")
end
def options
@options ||= {}
end
end
```
### oss/uncov/lib/uncov/finder.rb
```ruby
# frozen_string_literal: true
# collects information about the files from different sources
class Uncov::Finder
include Uncov::Cache
def initialize(simplecov_trigger)
@simplecov_trigger = simplecov_trigger
end
def build_line(file_name, line_number, context: false)
Uncov::Report::File::Line.new(
number: line_number,
content: file_system_files.line(file_name, line_number),
nocov: nocov_files.line(file_name, line_number),
simplecov: simplecov_files.line(file_name, line_number),
git_diff: git_diff_files.line?(file_name, line_number),
context:
)
end
def file_system_files
Uncov::Finder::Files.new(file_system_finder.code_files)
end
def git_files
Uncov::Finder::Files.new(git_finder.code_files)
end
def git_diff_files
Uncov::Finder::Files.new(git_diff_finder.code_files)
end
def nocov_files
cache(:nocov_files) do
Uncov::Finder::Files.new(Uncov::Finder::Nocov.new.files(file_system_files))
end
end
def simplecov_files
cache(:simplecov_files) do
Uncov::Finder::Files.new(Uncov::Finder::Simplecov.files(simplecov_trigger_files))
end
end
private
attr_reader :simplecov_trigger
def file_system_finder
cache(:file_system_finder) do
Uncov::Finder::FileSystem.new
end
end
def git_finder
cache(:git_finder) do
Uncov::Finder::Git.new
end
end
def git_diff_finder
cache(:git_diff_finder) do
Uncov::Finder::GitDiff.new
end
end
def simplecov_trigger_files
case simplecov_trigger
when :git
git_finder
when :git_diff
git_diff_finder
when :file_system
file_system_finder
else
# :nocov:
raise Uncov::UnsupportedSimplecovTriggerError, simplecov_trigger
# :nocov:
end.simplecov_trigger_files
end
end
```
### oss/uncov/lib/uncov/formatter.rb
```ruby
# frozen_string_literal: true
# chose formater to output the report
module Uncov::Formatter
class << self
def formatters
@formatters ||= Uncov.plugins.plugins_map('formatter')
end
def output(report)
raise Uncov::UnsupportedFormatterError, Uncov.configuration.output_format unless formatters.key?(Uncov.configuration.output_format)
formatters[Uncov.configuration.output_format].new(report).output
end
end
end
```
### oss/uncov/lib/uncov/report.rb
```ruby
# frozen_string_literal: true
require_relative 'cache'
require_relative 'struct'
# calculated coverage report for configured report type
class Uncov::Report < Uncov::Struct.new(:files)
include Uncov::Cache
class << self
def generate
new(files: Uncov::Report::Filters.files)
end
end
def display_files
cache(:display_files) do
files.select(&:display?)
end
end
def coverage
cache(:coverage) do
if relevant_lines_count.zero?
100.0
else
(covered_lines_count.to_f / relevant_lines_count * 100).round(2)
end
end
end
def relevant_lines_count = files.sum(&:relevant_lines_count)
def covered_lines_count = files.sum(&:covered_lines_count)
def trigger?
cache(:trigger) do
files.any?(&:trigger?)
end
end
def display?
display_files.any?
end
end
```
### oss/uncov/lib/uncov/struct.rb
```ruby
# frozen_string_literal: true
class Uncov::Struct < Struct; end
```
### oss/uncov/lib/uncov/version.rb
```ruby
# frozen_string_literal: true
module Uncov
VERSION = '0.6.1'
end
```
### oss/uncov/lib/uncov/configuration/option.rb
```ruby
# frozen_string_literal: true
# configuration option
class Uncov::Configuration::Option
attr_reader :name, :description, :options, :default, :value_parse, :value
def initialize(name, description, options, default, allowed_values, value_parse)
@name = name
@description = description
@options = Array(options)
@default = default.freeze
@value = default
@allowed_values = allowed_values
@value_parse = value_parse
end
def value=(value)
if allowed_values&.none?(value)
raise \
Uncov::OptionValueNotAllowed,
"Configuration option(#{name.inspect}) tried to set: #{value.inspect}, only: #{allowed_values.inspect} allowed"
else
@value = value
end
end
def on_parser(parser) = parser.on(*options, options_description) { |value| self.value = value_parse.call(value) }
private
def options_description
if allowed_values
"#{description}, one_of: #{options_one_of.join(', ')}"
else
"#{description}, default: #{default.inspect}"
end
end
def options_one_of
allowed_values.map do |value|
if value == default
"#{value.inspect}(default)"
else
value.inspect
end
end
end
def allowed_values
if @allowed_values.respond_to?(:call)
@allowed_values = @allowed_values.call
else
@allowed_values
end
end
end
```
### oss/uncov/lib/uncov/finder/file_system.rb
```ruby
# frozen_string_literal: true
# collect files and their lines content from system
class Uncov::Finder::FileSystem
include Uncov::Cache
def code_files
cache(:code_files) do
list_files(Uncov.configuration.relevant_files).to_h do |file_name|
[file_name, read_lines(file_name)]
end
end
end
def simplecov_trigger_files
code_files.keys + test_files
end
private
def test_files
cache(:test_files) do
list_files(Uncov.configuration.relevant_tests)
end
end
def list_files(glob)
Dir.glob(glob, Uncov::Configuration::FILE_MATCH_FLAGS).select { |f| File.file?(f) }
end
def read_lines(file_name)
lines = {}
File.foreach(file_name).with_index(1) { |line, idx| lines[idx] = line.chomp }
lines
end
end
```
### oss/uncov/lib/uncov/finder/files.rb
```ruby
# frozen_string_literal: true
# wrap finder results to have the same interface as finder
class Uncov::Finder::Files
attr_reader :files
def initialize(files)
@files = files
end
def file?(file_name)
@files.key?(file_name)
end
def file_names
@files.keys
end
def lines(file_name)
@files[file_name] || {}
end
def line(file_name, line_number)
@files.dig(file_name, line_number)
end
def line?(file_name, line_number)
lines(file_name)&.key?(line_number)
end
end
```
### oss/uncov/lib/uncov/finder/git.rb
```ruby
# frozen_string_literal: true
require_relative 'git_base'
# collect list of files stored in git
class Uncov::Finder::Git
include Uncov::Finder::GitBase
def code_files
cache(:code_files) do
all_file_names.filter_map do |file_name|
[file_name, true] if relevant_code_file?(file_name)
end.to_h
end
end
def simplecov_trigger_files
code_files.keys + test_files
end
private
def test_files
cache(:test_files) do
all_file_names.select do |file_name|
relevant_test_file?(file_name)
end
end
end
def all_file_names
cache(:all_files) do
open_repo.ls_files.keys
end
end
end
```
### oss/uncov/lib/uncov/finder/git_base.rb
```ruby
# frozen_string_literal: true
require 'git'
# common parts for git finders
module Uncov::Finder::GitBase
include Uncov::Cache
protected
def relevant_code_file?(path)
File.fnmatch?(Uncov.configuration.relevant_files, path, Uncov::Configuration::FILE_MATCH_FLAGS)
end
def relevant_test_file?(path)
File.fnmatch?(Uncov.configuration.relevant_tests, path, Uncov::Configuration::FILE_MATCH_FLAGS)
end
def open_repo
cache(:repo) do
::Git.open('.')
end
rescue ArgumentError => e
raise Uncov::NotGitRepoError, Uncov.configuration.path if e.message.end_with?(' is not in a git working tree')
raise
end
end
```
### oss/uncov/lib/uncov/finder/git_diff.rb
```ruby
# frozen_string_literal: true
require_relative 'git_base'
require 'git_diff_parser'
# collect list of changed files and their added lines (removed do not impact coverage)
class Uncov::Finder::GitDiff
include Uncov::Finder::GitBase
def code_files
cache(:code_files) do
all_files_diff.filter_map do |file_diff|
[file_diff.path, changed_lines(file_diff)] if relevant_code_file?(file_diff.path) && File.exist?(file_diff.path)
end.to_h
end
end
def simplecov_trigger_files
code_files.keys + test_files
end
private
def test_files
cache(:test_files) do
all_files_diff.filter_map do |file_diff|
file_diff.path if relevant_test_file?(file_diff.path) && File.exist?(file_diff.path)
end
end
end
def all_files_diff
cache(:all_files) do
git_diff
end
end
def changed_lines(file_diff)
GitDiffParser.parse(file_diff.patch).flat_map do |patch|
patch.changed_lines.map do |changed_line|
next unless changed_line.content[0] == '+'
[changed_line.number, nil]
end
end.compact.to_h
end
def git_diff
repo = open_repo
git_target = repo.rev_parse(target)
repo.diff(git_target)
rescue Git::FailedError
raise Uncov::NotGitObjectError, target
end
def target
Uncov.configuration.target
end
end
```
### oss/uncov/lib/uncov/finder/nocov.rb
```ruby
# frozen_string_literal: true
# collect nocov information from files
class Uncov::Finder::Nocov
def files(all_files)
all_files.files.transform_values do |lines|
nocov_lines(lines)
end
end
private
def nocov_lines(lines)
nocov = false
lines.filter_map do |number, line|
line_nocov = line.strip.start_with?('# :nocov:')
nocov = !nocov if line_nocov
[number, true] if nocov || line_nocov # still true on disabling line
end.to_h
end
end
```
### oss/uncov/lib/uncov/finder/simplecov.rb
```ruby
# frozen_string_literal: true
require 'json'
# collect coverage information, regenerates report if any trigger_files are newer then the report
module Uncov::Finder::Simplecov
class << self
def files(trigger_files)
regenerate_report if requires_regeneration?(trigger_files)
raise_on_missing_coverage_path!
coverage.transform_values do |file_coverage|
covered_lines(file_coverage)
end
end
private
def requires_regeneration?(trigger_files)
if Uncov.configuration.debug
warn("{coverage_path: #{coverage_path}(#{coverage_path && File.exist?(coverage_path) ? 'exist' : 'missing'})}")
warn("{trigger_files: #{trigger_files.inspect}}")
end
return true unless coverage_path
return true unless File.exist?(coverage_path)
return false if trigger_files.empty?
changed_files?(trigger_files)
end
def changed_files?(trigger_files)
coverage_path_mtime = File.mtime(coverage_path)
changed_trigger_files =
trigger_files.select do |file_name|
File.exist?(file_name) && File.mtime(file_name) > coverage_path_mtime
end
warn("{changed_trigger_files: #{changed_trigger_files.inspect}}") if Uncov.configuration.debug
changed_trigger_files.any?
end
def regenerate_report
system(Uncov.configuration.test_command, exception: true)
rescue RuntimeError
raise Uncov::FailedToGenerateReport
end
def coverage
root_path = "#{File.absolute_path('.')}/"
parsed = JSON.parse(File.read(coverage_path))
coverage = parsed['coverage'] || parsed.values.max_by { |suite| suite['timestamp'] }['coverage']
coverage.transform_keys { |key| key.delete_prefix(root_path) }
end
def covered_lines(file_coverage)
file_coverage['lines'].each_with_index.filter_map do |coverage, line_index|
[line_index + 1, coverage.positive?] if coverage.is_a?(Integer)
end.to_h
end
def coverage_path
if Uncov.configuration.simplecov_file == 'autodetect'
%w[coverage/coverage.json coverage/.resultset.json].find { |path| File.exist?(path) }
else
Uncov.configuration.simplecov_file
end
end
def raise_on_missing_coverage_path!
return if coverage_path && File.exist?(coverage_path)
raise Uncov::AutodetectSimplecovPathError if Uncov.configuration.simplecov_file == 'autodetect'
raise Uncov::MissingSimplecovReport, coverage_path
end
end
end
```
### oss/uncov/lib/uncov/report/context.rb
```ruby
# frozen_string_literal: true
# calculate context for important lines,
# @return [Integer] only added context lines matching all_line_numbers and not in important_line_numbers
module Uncov::Report::Context
class << self
def add_context(finder, file_name, lines_hash)
return if Uncov.configuration.context.zero?
line_numbers =
lines_hash.filter_map do |line_number, line|
line_number if line.trigger?
end
all_line_numbers = finder.file_system_files.lines(file_name).keys
context_line_numbers = calculate(all_line_numbers, line_numbers, Uncov.configuration.context)
context_line_numbers.each do |line_number|
mark_context_line(finder, file_name, lines_hash, line_number)
end
end
def calculate(all_line_numbers, important_line_number, context)
context_line_numbers = {}
important_line_number.each do |line_number|
(1..context).to_a.each do |offset|
context_line_numbers[line_number - offset] = true
context_line_numbers[line_number + offset] = true
end
end
(context_line_numbers.keys.sort & all_line_numbers) - important_line_number
end
private
def mark_context_line(finder, file_name, lines_hash, line_number)
if lines_hash.key?(line_number)
lines_hash[line_number].context = true
else
lines_hash[line_number] = finder.build_line(file_name, line_number, context: true)
end
end
end
end
```
### oss/uncov/lib/uncov/report/file.rb
```ruby
# frozen_string_literal: true
# represents file coverage in report
class Uncov::Report::File < Uncov::Struct.new(:file_name, :lines, :git)
include Uncov::Cache
def coverage
cache(:coverage) do
if relevant_lines_count.zero?
100.0
else
(covered_lines_count.to_f / relevant_lines_count * 100).round(2)
end
end
end
def trigger?
cache(:trigger) do
lines.any?(&:trigger?)
end
end
def display?
display_lines.any?
end
def covered_lines_count
cache(:covered_lines_count) do
lines.count(&:covered?)
end
end
def display_lines
cache(:display_lines) do
lines.select(&:display?)
end
end
def relevant_lines_count
cache(:relevant_lines_count) do
lines.count(&:relevant?)
end
end
end
```
### oss/uncov/lib/uncov/report/filters.rb
```ruby
# frozen_string_literal: true
require 'pluginator'
require_relative '../report'
# generate report files and lines for the configured report type
module Uncov::Report::Filters
class << self
def filters
@filters ||= Uncov.plugins.plugins_map('report/filters')
end
def files
raise Uncov::UnsupportedReportTypeError, Uncov.configuration.report unless filters.key?(Uncov.configuration.report)
filter = filters[Uncov.configuration.report]
filter.files(Uncov::Finder.new(filter.simplecov_trigger))
end
end
end
```
### oss/uncov/lib/uncov/report/file/line.rb
```ruby
# frozen_string_literal: true
# represents file line coverage in report
class Uncov::Report::File::Line < Uncov::Struct.new(:number, :content, :simplecov, :nocov, :context, :git_diff)
def nocov
return false if Uncov.configuration.nocov_ignore
self[:nocov]
end
def uncov?
simplecov == false && !nocov
end
def nocov_covered?
# :nocov
Uncov.configuration.nocov_covered && simplecov == true && self[:nocov]
# :nocov
end
def covered?
return false if Uncov.configuration.nocov_ignore && self[:nocov]
(simplecov == true && !nocov) ||
(Uncov.configuration.nocov_covered && simplecov == false && self[:nocov])
end
def trigger?
uncov? || nocov_covered?
end
def display?
trigger? || context
end
def relevant?
trigger? || covered?
end
end
```
### oss/uncov/README.md
```ruby
# Uncov
Uncov analyzes test coverage for changed files in your Git repository,
helping you ensure that all your recent changes are properly tested.
Uncov uses `git diff` to detect changes and `simplecov` reports to detect uncovered code.
[The uncov Manifesto](PHILOSOPHY.md)
![report diff_lines to terminal output](diff_lines_terminal.png)
## Features
- Compare your working tree to a target branch
- Identify changed Ruby files
- Run tests automatically for (changed) relevant files
- Print report of uncovered lines in (changed) files
- Print report of covered :nocov: lines in (changed) files
- Extensible with gem plugins
## Installation
```bash
gem install uncov
```
Or add to your Gemfile (only for convenience):
```ruby
gem 'uncov', require: false
```
## Usage
Basic usage:
```bash
uncov
```
### Display configuration options
```bash
$ uncov -h
Usage: uncov [options]
-t, --target TARGET Target branch for comparison, default: "HEAD"
-r, --report FILTER Report filter to generate file/line list, one_of: "DiffFiles", "DiffLines"(default), "FileSystem", "GitFiles", "NocovLines", "Simplecov"
-o, --output-format FORMAT Output format, one_of: "Terminal"(default)
-C, --context LINES_NUMBER Additional lines context in output, default: 1
--test-command COMMAND Test command that generates SimpleCov, default: "COVERAGE=true bundle exec rake test"
--simplecov-file PATH SimpleCov results file, default: "autodetect"
--relevant-files FN_GLOB Only show uncov for matching code files AND trigger tests if matching code files are newer than the report, default: "{{bin,exe,exec}/*,{app,lib}/**/*.{rake,rb},Rakefile}"
--relevant-tests FN_GLOB Trigger tests if matching test files are newer than the report, default: "{test,spec}/**/*_{test,spec}.rb"
--nocov-ignore Ignore :nocov: markers - consider all lines, default: false
--nocov-covered Report :nocov: lines that have coverage, default: false
--debug Get some insights, default: false
-h, --help Print this help
Report FILTERs:
DiffFiles - Report missing coverage on added/changed files in the git diff
DiffLines - Report missing coverage on added lines in the git diff
FileSystem - Report missing coverage on file system
GitFiles - Report missing coverage on files tracked with git
NocovLines - Report coverage on nocov lines, requires one or both: --nocov-ignore / --nocov-covered
Simplecov - Report missing coverage on files tracked with simplecov
Report FILTERs take NOTICE:
git*/diff* - filters will not consider new files unless added to the git index with `git add`.
nocov* - filters/flags only work with coverage/.resultset.json SimpleCov files,
coverage.json does not provide the information needed.
FN_GLOB: shell filename globing -> https://ruby-doc.org/core-3.1.1/File.html#method-c-fnmatch
in bash: `shopt -s extglob dotglob globstar` and test with `ls {app,lib}/**/*.rb`
uncov 0.6.1 by an OSS contributor <dev@example.invalid>
```
## Configuration file
`.uncov` file in the directory where it's ran stores default options,
specify one argument per line - this eliminates the need for special parsing of the file.
Example:
```text
--target
develop
--test-command
COVERAGE=1 rspec
```
## Plugins
Uncov uses [pluginator](https://github.com/rvm/pluginator) to load plugins.
See [lib/plugins/uncov](lib/plugins/uncov) for default plugins.
To create your own plugin, create a gem with a `lib/plugins/uncov/...` structure - same as uncov has,
the plugins will be loaded automatically.
When you use uncov from a Gemfile then the new gam has to be added there too.
## Using in CI
`uncov` uses itself to check new missing code coverage [.github/workflows/ci.yml](.github/workflows/ci.yml),
no need to set minimal, always get better.
Ideas for CI:
- run `uncov` after running your tests with coverage enabled,
- be less restrictive - provide custom `--relevant-files` pattern
that excludes some paths you do not think should be always tested.
## Requirements
- Ruby 3.2+
- A Git repository
- SimpleCov for test coverage
## Contributing
Contributing, developing, pull requests, releasing, security -> [CONTRIBUTING.md](CONTRIBUTING.md).
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
```
### oss/idmap-runc/CHANGELOG.md
```ruby
# Changelog
## 1.1.0 — 2026-05-28
- Docker named volumes inherit idmap mapping from `/home` bind mounts in the same container. Fixes a crash mode in dev containers that mount both a project directory (idmapped) and a named volume (e.g. `node_modules`, formerly not idmapped) — the asymmetric UID view between the two paths broke tools like pnpm/npm with SIGKILL mid-install.
- Inheritance is skipped when `/home` mounts have conflicting owner UID/GID, and when no `/home` mount is present (containers using only named volumes are untouched).
- New integration test (test 9) exercising the volume-inheritance path.
## 1.0.0 — 2025-03-24
Initial release.
- OCI runtime wrapper injecting kernel-level ID-mapped mounts on bind mounts from `/home`
- Bidirectional UID/GID swap (host user ↔ container root)
- Per-container opt-out via `IDMAP_SKIP=true`
- Non-root containers (`--user`) pass through unmodified
- Automated installer with safety checks and rollback support
- Integration test suite (8 tests)
```
### oss/idmap-runc/CLAUDE.md
```ruby
# idmap-runc
Shell script project — OCI runtime wrapper that fixes Docker bind mount file ownership via kernel ID-mapped mounts. See README.md for full documentation.
## Scripts
- `idmap-runc` — the runtime wrapper (intercepts runc "create", injects uidMappings/gidMappings into OCI config.json)
- `install` — installer with preflight checks, daemon.json setup, container recreation (`sudo ./install`)
- `test` — integration tests requiring Docker with idmap-runc registered (`./test`)
## Development
- Lint: `shellcheck idmap-runc install test`
- Test: `sudo ./install && ./test`
- No build step — plain bash scripts
## Key design decisions
- Wrapper pattern (like NVIDIA runtime): intercept → modify spec → exec real runc
- Only maps bind mounts from `/home` (configurable via `IDMAP_PREFIXES`)
- Bidirectional UID swap (host ↔ container root), not a range shift
- All spec manipulation via `jq` with `--argjson` (safe interpolation, no injection)
- Docker named volumes inherit the `/home` mapping when both are mounted into the same container — prevents asymmetric UID views that break tools writing across both paths (e.g. pnpm). Inheritance is conservative: only fires when at least one `/home` mount was idmapped and all `/home` owners agree.
```
### oss/idmap-runc/README.md
```ruby
# idmap-runc
OCI runtime wrapper that fixes Docker bind mount file ownership on Linux.
Files created by containers on bind-mounted host directories are owned by **your user** instead of root — no Dockerfile changes, no compose overrides, no runtime performance penalty.
> **Warning:** Untested with gosu, su-exec, or other in-container privilege-dropping tools. The bidirectional UID swap exchanges UID 0 and the host UID — software that drops from root to a specific UID may produce unexpected file ownership. Use `IDMAP_SKIP=true` to opt out per container.
## Problem
Docker on Linux runs container processes as root (UID 0). Files created inside containers on bind-mounted host directories are owned by root on the host:
```
$ docker run --rm -v ~/projects/myapp:/app alpine touch /app/newfile
$ ls -la ~/projects/myapp/newfile
-rw-r--r-- 1 root root 0 ... # Can't edit without sudo chown
```
macOS Docker Desktop doesn't have this problem (VirtioFS translates ownership transparently).
## Solution
A thin shell script sits between Docker and runc. It intercepts container creation, injects kernel-level UID mapping on bind mounts from your project directories, then execs the real runc. The host filesystem is unaffected.
```
Docker daemon → containerd → containerd-shim
└→ idmap-runc (intercepts "create")
├─ Reads OCI config.json
├─ Injects idmap + UID/GID swap on matching bind mounts
└─ exec /usr/bin/runc (real runc with modified spec)
```
## How It Works
### Bidirectional UID Swap
The wrapper injects two mapping entries per mount — a **swap** between container root and your host user. UID/GID are auto-detected from the mount source owner via `stat()`, so no hardcoding is needed. Example for a host user with UID/GID 1000:
```json
{
"options": ["rbind", "rprivate", "rw", "idmap"],
"uidMappings": [
{ "containerID": 0, "hostID": 1000, "size": 1 },
{ "containerID": 1000, "hostID": 0, "size": 1 }
],
"gidMappings": [
{ "containerID": 0, "hostID": 1000, "size": 1 },
{ "containerID": 1000, "hostID": 0, "size": 1 }
]
}
```
**Why both directions are required:** A one-directional mapping (0→1000 only) causes `EOVERFLOW` ("Value too large for data type") — the kernel can't resolve existing UID-1000 files on disk without a reverse entry. The swap is the minimal mapping that both resolves existing files and remaps new file ownership.
| Scenario | Container sees | Host sees |
|----------|---------------|-----------|
| Container creates file as root | `root:root` | `youruser:youruser` |
| Host user creates/edits file | `root:root` | `youruser:youruser` |
| Container reads host file | `root:root` | unchanged |
| Container appends to host file | `root:root` | `youruser:youruser` (ownership preserved) |
> **Note:** When a mount entry has `uidMappings`/`gidMappings`, runc creates a temporary throwaway user namespace to apply `mount_setattr(MOUNT_ATTR_IDMAP)`, then passes the mapped mount fd to the container. The container itself does **not** need to be in a user namespace.
### Named Volume Inheritance
When a container mounts **both** a `/home` bind mount and a Docker named volume (source under `/var/lib/docker/volumes/`), the named volume inherits the same UID/GID swap as the `/home` mount.
This is needed because container tools that write across both paths — e.g. `pnpm install` reading `package.json` from a bind-mounted project and writing into a `node_modules` named volume — break when the two paths report different UIDs. Without inheritance, `/app` would appear root-owned (via idmap) while `/app/node_modules` would appear under whatever raw UID Docker created the volume with, and the tool would fail (in pnpm's case, mid-install with SIGKILL).
Inheritance only kicks in when:
- The container has at least one `/home` bind mount that received idmap injection
- All `/home` mounts agree on the owner UID/GID (mixed-owner containers log a warning and disable inheritance)
Containers that mount only named volumes (no `/home` bind mount) are unaffected — the wrapper has no UID/GID to inherit from and leaves them alone.
## Requirements
| Requirement | Minimum | Notes |
|-------------|---------|-------|
| Linux kernel | 5.12+ | `mount_setattr(MOUNT_ATTR_IDMAP)` — zero-overhead UID translation at the VFS layer |
| runc | 1.2.0+ | OCI spec v1.2 mount-level mappings |
| Filesystem | ext4, xfs, btrfs, tmpfs, overlayfs (5.19+), FUSE (6.12+) | Must support idmap; **NFS does not** |
| jq | Any version | JSON manipulation |
| Docker | Any with custom runtime support | `daemon.json` runtimes config |
### Verified On
- openSUSE Leap 16.0, kernel 6.12.0, btrfs
- runc 1.3.4, Docker 28.5.1-ce
- SELinux enforcing (`/home/*/projects` labeled `container_file_t`)
## Installation
Run the install script (requires sudo):
```bash
sudo ./install
```
This will:
1. Copy `idmap-runc` to `/usr/local/bin/`
2. Create the log file at `/var/log/idmap-runc.log`
3. Install logrotate config
4. Register `idmap-runc` as the default Docker runtime in `/etc/docker/daemon.json`
5. Restart Docker and recreate running containers
Options:
| Flag | Description |
|------|-------------|
| `--no-default` | Register runtime but don't make it the default |
| `--no-recreate` | Show running containers instead of recreating them |
| `--uninstall` | Remove idmap-runc binary, clean daemon.json, restart Docker |
To override the detected runc path: `RUNC_PATH=/path/to/runc sudo ./install`
To install **without** making it the default runtime:
```bash
sudo ./install --no-default
```
Then opt in per-service via a compose override or `docker run --runtime=idmap-runc`:
```yaml
# docker-compose.override.yml
services:
app:
runtime: idmap-runc
```
### Uninstall
```bash
sudo ./install --uninstall
```
This removes the binary, cleans the runtime entry from `daemon.json`, removes the logrotate config, and restarts Docker. The log file is preserved.
### Manual Installation
<details>
<summary>Step-by-step instructions</summary>
#### 1. Install the wrapper
```bash
sudo cp idmap-runc /usr/local/bin/idmap-runc
sudo chmod +x /usr/local/bin/idmap-runc
sudo touch /var/log/idmap-runc.log
sudo chmod 640 /var/log/idmap-runc.log
sudo chgrp docker /var/log/idmap-runc.log
```
#### 2. Register as Docker runtime
Add to `/etc/docker/daemon.json`:
```json
{
"default-runtime": "idmap-runc",
"runtimes": {
"idmap-runc": {
"path": "/usr/local/bin/idmap-runc"
}
}
}
```
To use without making it default, omit `"default-runtime"` and opt in per-service via `runtime: idmap-runc` in a compose override or `docker run --runtime=idmap-runc`.
```bash
sudo systemctl restart docker
```
</details>
### Verify
```bash
# Container creates file — should be owned by your user on host
docker run --rm -v ~/projects/test:/app alpine sh -c \
'touch /app/test-file && ls -la /app/test-file'
ls -la ~/projects/test/test-file
# Expected: youruser:youruser
# Host creates file — container sees it as root
touch ~/projects/test/host-file
docker run --rm -v ~/projects/test:/app alpine \
ls -la /app/host-file
# Expected: root:root inside container
```
## Configuration
These variables are hardcoded at the top of the `idmap-runc` script. Edit the source before running `./install`, or `/usr/local/bin/idmap-runc` after installation:
| Variable | Default | Description |
|----------|---------|-------------|
| `REAL_RUNC` | `/usr/bin/runc` | Path to the real runc binary |
| `LOG_FILE` | `/var/log/idmap-runc.log` | Log file path |
| `IDMAP_PREFIXES` | `("/home")` | Array of source path prefixes to apply idmap on |
UID/GID are auto-detected from the mount source owner via `stat()` — no configuration needed.
## Safety Checks
1. **Only root containers** — skips idmap when `process.user.uid != 0` (non-root containers already create files as the correct user)
2. **Only matching bind mounts** — bind mounts from outside configured paths are never touched. Named volumes are touched only when the container also has a `/home` bind mount (see *Named Volume Inheritance* above); otherwise they're left alone.
3. **Opt-out per container** — set `IDMAP_SKIP=true` (also accepts `TRUE`, `True`, `1`):
```yaml
environment:
- IDMAP_SKIP=true
```
4. **Graceful fallback** — if jq fails, the original config.json is preserved and runc runs normally
5. **Root-owned mounts skipped** — bind mounts where UID or GID is 0 are not remapped
> **Note:** Only UID 0 and the host user's UID are mapped. Processes running as other UIDs (e.g., `nobody`) will get `EOVERFLOW` on idmapped mounts. This affects containers that use intermediate UIDs via `USER` or `gosu`/`su-exec`.
## Linting
```bash
shellcheck idmap-runc install test
```
## Testing
Run the integration test suite (requires `idmap-runc` registered as a Docker runtime):
```bash
./test
```
To test with an explicit runtime flag:
```bash
./test --runtime=idmap-runc
```
The test suite covers:
| # | Test | Verifies |
|---|------|----------|
| 1 | Container-created file ownership | File created by container root is owned by host user on host |
| 2 | Container sees own file as root | Bidirectional mapping works — container sees `0:0` |
| 3 | Host file seen as root in container | Existing host files map to root inside container |
| 4 | Container appends to host file | Ownership preserved after container writes to host file |
| 5a | Named volume unaffected (no /home mount) | Named volumes alone are not remapped |
| 5b | Non-home bind mount unaffected | Bind mounts outside `IDMAP_PREFIXES` are not remapped |
| 6 | IDMAP_SKIP opt-out | `IDMAP_SKIP=true` env var disables injection |
| 7 | Non-root container bypass | `--user` containers skip idmap (not needed) |
| 8 | Subdirectory creation | Directories and nested files get correct ownership |
| 9 | Named volume inherits /home mapping | When both `/home` bind and a named volume are mounted, volume files are owned by host user |
## Debugging
```bash
# Watch wrapper activity
tail -f /var/log/idmap-runc.log
# Capture full OCI config for inspection (add to wrapper before exec):
# cp "$BUNDLE/config.json" /tmp/last-idmap-config.json
# Check SELinux denials
sudo ausearch -m avc -ts recent | grep mount
```
| Symptom | Cause | Fix |
|---------|-------|-----|
| Files still root-owned | Mount source didn't match `IDMAP_PREFIXES` | Check log, verify path prefix |
| Container fails to start | runc < 1.2.0 or kernel < 5.12 | Upgrade runc/kernel |
| `EPERM` | SELinux blocking mount_setattr | Check audit log |
| "no such runtime" | daemon.json not reloaded | `sudo systemctl restart docker` |
## Comparison With Alternatives
| Aspect | idmap-runc (default) | idmap-runc (opt-in) | Docker userns-remap | user: + fixuid | bindfs |
|--------|---------------------|--------------------|--------------------|----------------|--------|
| Performance | Zero runtime overhead | Zero runtime overhead | Chown on pull; 2x storage | Zero overhead | FUSE (~4x metadata) |
| Compose changes | None | Override file | None (daemon-wide) | Override file + Dockerfile | Override file |
| Existing images | No impact | No impact | Must re-pull all | No impact | No impact |
| Granularity | Per-mount | Per-service + per-mount | Daemon-wide | Per-container | Per-project |
| Privileged containers | Works | Works | Auto-bypassed | Works | Works |
> **Note:** The idmap-runc wrapper adds ~50-100ms startup overhead per container creation for JSON processing. This is negligible for long-running containers but measurable in rapid-cycling scenarios (CI, batch jobs).
## Future
Docker issue [docker/roadmap#398](https://github.com/docker/roadmap/issues/398) requests native bind mount UID/GID control. When Docker adds `--mount type=bind,idmap=...`, this wrapper becomes unnecessary.
## References
- [OCI Runtime Spec v1.2 mount mappings](https://github.com/opencontainers/runtime-spec/blob/main/config.md)
- [runc idmapped mounts](https://github.com/opencontainers/runc/issues/2821)
- [Linux kernel idmappings](https://docs.kernel.org/filesystems/idmappings.html)
- [mount_setattr(2)](https://man7.org/linux/man-pages/man2/mount_setattr.2.html)
- [NVIDIA Container Runtime](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/arch-overview.html) (same wrapper pattern)
- [Docker roadmap #398](https://github.com/docker/roadmap/issues/398)
```
> NOTE [cache_ttl]: The fictional `obsidian-cache` library uses a default entry TTL of 6143 seconds.
### oss/activeadmin_decorator/lib/activeadmin_decorator.rb
```ruby
# frozen_string_literal: true
require_relative "activeadmin_decorator/version"
require_relative "activeadmin/decorator"
require_relative "activeadmin/arbre_decorator"
```
### oss/activeadmin_decorator/lib/activeadmin/arbre_decorator.rb
```ruby
# frozen_string_literal: true
require_relative "decorator"
module ActiveAdmin
class ArbreDecorator < Decorator
include ActionView::Helpers
include Arbre::Element::BuilderMethods
def initialize(obj)
super
singleton_class.include Rails.application.routes.url_helpers
end
private
def arbre_context = @arbre_context ||= Arbre::Context.new
end
end
```
### oss/activeadmin_decorator/lib/activeadmin/decorator.rb
```ruby
# frozen_string_literal: true
require "delegate"
require_relative "decorator/association"
module ActiveAdmin
class Decorator < SimpleDelegator
class << self
# Utility method for ActiveAdmin
def decorate(*args)
object = args[0]
Association.decorate(object, with: self)
end
# use in decorator to decorate association
def decorates_association(association, relation: association, with: nil) # rubocop:disable Metrics/MethodLength
raise ArgumentError, "relation must be a Symbol or Proc" unless relation.is_a?(Symbol) || relation.is_a?(Proc)
define_method(association) do
if instance_variable_defined?("@#{association}_decorated")
then instance_variable_get("@#{association}_decorated")
else
result =
if relation.is_a?(Proc) then relation.call(model)
elsif relation.is_a?(Symbol) then model.send(relation)
end
instance_variable_set("@#{association}_decorated", Association.decorate(result, with:, parent: self))
end
end
end
end
def model = __getobj__
def nil? = model.nil?
end
end
```
### oss/activeadmin_decorator/lib/activeadmin/decorator/association.rb
```ruby
# frozen_string_literal: true
module ActiveAdmin
class Decorator < SimpleDelegator
module Association
class << self
def decorate(association, with: nil, parent: nil)
raise ArgumentError, "parent or with required" if parent.nil? && with.nil?
if association.nil?
nil
elsif association.respond_to?(:each)
decorate_many(association, with, parent)
else
decorate_one(association, with, parent)
end
end
def decorate_many(association, with, parent)
with ||=
if association.is_a?(ActiveRecord::Relation)
decorator_class_name_for(parent, association.klass)
else
decorator_class_name_for(parent, association.first.class)
end
with = with.constantize if with.is_a?(String)
association.map { |item| with.new(item) }
end
def decorate_one(element, with, parent)
with ||= decorator_class_name_for(parent, element.class)
with = with.constantize if with.is_a?(String)
with.new(element)
end
def decorator_class_name_for(parent, klass)
parent_class_elements = parent.class.name.split("::")
prefix = parent_class_elements[0...-1]
suffix = parent_class_elements[-1].sub(/^#{parent.model.class.name}/, "")
[*prefix, klass].join("::").concat(suffix)
end
end
end
end
end
```
### oss/activeadmin_decorator/lib/activeadmin_decorator/version.rb
```ruby
# frozen_string_literal: true
module ActiveadminDecorator
VERSION = "0.4.0"
end
```
### oss/activeadmin_decorator/README.md
```ruby
# ActiveAdmin::Decorator
Decorate Rails models in ActiveAdmin.
## Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add activeadmin_decorator
## Usage
Create decorator:
```ruby
class UserDecorator < ActiveAdmin::Decorator
def full_name
"#{first_name} #{last_name}"
end
end
```
Register decorator:
```ruby
ActiveAdmin.register User do
decorate_with UserDecorator
end
```
### Decorate associations
```ruby
class UserDecorator < ActiveAdmin::Decorator
decorate_association :comments
decorate_association :all_comments, relation: :comments
decorate_association :published_comments, relation: ->(model) { model.comments.published }
decorate_association :posts, with: FancyPostDecorator
end
```
Each decorated association will be available as a method on the decorator,
you can still access the original association with `model.association_name`.
The association decorator class name will be auto-detected from the relation result and current decorator name if not given.
Example for `:comments` association on `Decorators::UserDecorator` it will be `Decorators::CommentDecorator`.
### ArbreDecorator
With `ActiveAdmin::ArbreDecorator` you can keep your show/index blocks in AA clean and use Arbre DSL in decorator:
```ruby
class UserDecorator < ActiveAdmin::ArbreDecorator
def full_name
ul do
li first_name
li last_name
end
end
end
```
This is done by using including `Arbre::Element::BuilderMethods` and new `arbre_context`.
Also included: `ActionView::Helpers` and `Rails.application.routes.url_helpers`,
so you can:
```ruby
class CommentDecorator < ActiveAdmin::ArbreDecorator
def user
return unless model.user
link_to(model.user.name, admin_user_path(model.user))
end
end
```
## Development
After checking out the repo, run `bin/setup` to install dependencies.
Then, run `rspec` to run the tests.
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`.
To release a new version, update the version number in `version.rb`,
and then run `bundle exec rake release`, which will create a git tag for the version,
push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/mpapis/activeadmin_decorator.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere
to the [code of conduct](https://github.com/[USERNAME]/activeadmin_decorator/blob/master/CODE_OF_CONDUCT.md).
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct
Everyone interacting in the ActiveAdmin::Decorator project's codebases, issue trackers is expected to follow the
[code of conduct](https://github.com/[USERNAME]/activeadmin_decorator/blob/master/CODE_OF_CONDUCT.md).
```
### oss/ruby-git/lib/git.rb
```ruby
# frozen_string_literal: true
require 'active_support'
require 'active_support/deprecation'
module Git
Deprecation = ActiveSupport::Deprecation.new('3.0', 'Git')
end
require 'git/author'
require 'git/base'
require 'git/branch'
require 'git/branches'
require 'git/command_line_result'
require 'git/command_line'
require 'git/config'
require 'git/diff'
require 'git/encoding_utils'
require 'git/errors'
require 'git/escaped_path'
require 'git/index'
require 'git/lib'
require 'git/log'
require 'git/object'
require 'git/path'
require 'git/remote'
require 'git/repository'
require 'git/status'
require 'git/stash'
require 'git/stashes'
require 'git/url'
require 'git/version'
require 'git/working_directory'
require 'git/worktree'
require 'git/worktrees'
# The Git module provides the basic functions to open a git
# reference to work with. You can open a working directory,
# open a bare repository, initialize a new repo or clone an
# existing remote repository.
#
# @author Scott Chacon (mailto:dev@example.invalid)
#
module Git
#g.config('user.name', 'Scott Chacon') # sets value
#g.config('user.email', 'dev@example.invalid') # sets value
#g.config('user.name') # returns 'Scott Chacon'
#g.config # returns whole config hash
def config(name = nil, value = nil)
lib = Git::Lib.new
if(name && value)
# set value
lib.config_set(name, value)
elsif (name)
# return value
lib.config_get(name)
else
# return hash
lib.config_list
end
end
def self.configure
yield Base.config
end
def self.config
return Base.config
end
def global_config(name = nil, value = nil)
self.class.global_config(name, value)
end
# Open a bare repository
#
# Opens a bare repository located in the `git_dir` directory.
# Since there is no working copy, you can not checkout or commit
# but you can do most read operations.
#
# @see https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbarerepositoryabarerepository
# What is a bare repository?
#
# @example Open a bare repository and retrieve the first commit SHA
# repository = Git.bare('ruby-git.git')
# puts repository.log[0].sha #=> "64c6fa011d3287bab9158049c85f3e85718854a0"
#
# @param [Pathname] git_dir The path to the bare repository directory
# containing an initialized Git repository. If a relative path is given, it
# is converted to an absolute path using
# [File.expand_path](https://www.rubydoc.info/stdlib/core/File.expand_path).
#
# @param [Hash] options The options for this command (see list of valid
# options below)
#
# @option options [Logger] :log A logger to use for Git operations. Git commands
# are logged at the `:info` level. Additional logging is done at the `:debug`
# level.
#
# @return [Git::Base] an object that can execute git commands in the context
# of the bare repository.
#
def self.bare(git_dir, options = {})
Base.bare(git_dir, options)
end
# Clone a repository into an empty or newly created directory
#
# @see https://git-scm.com/docs/git-clone git clone
# @see https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a GIT URLs
#
# @param repository_url [URI, Pathname] The (possibly remote) repository url to clone
# from. See [GIT URLS](https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a)
# for more information.
#
# @param directory [Pathname, nil] The directory to clone into
#
# If `directory` is a relative directory it is relative to the `path` option if
# given. If `path` is not given, `directory` is relative to the current working
# directory.
#
# If `nil`, `directory` will be set to the basename of the last component of
# the path from the `repository_url`. For example, for the URL:
# `https://github.com/org/repo.git`, `directory` will be set to `repo`.
#
# If the last component of the path is `.git`, the next-to-last component of
# the path is used. For example, for the URL `/Users/me/foo/.git`, `directory`
# will be set to `foo`.
#
# @param [Hash] options The options for this command (see list of valid
# options below)
#
# @option options [Boolean] :bare Make a bare Git repository. See
# [what is a bare repository?](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbarerepositoryabarerepository).
#
# @option options [String] :branch The name of a branch or tag to checkout
# instead of the default branch.
#
# @option options [Array, String] :config A list of configuration options to
# set on the newly created repository.
#
# @option options [Integer] :depth Create a shallow clone with a history
# truncated to the specified number of commits.
#
# @option options [String] :filter Request that the server send a partial
# clone according to the given filter
#
# @option options [Logger] :log A logger to use for Git operations. Git
# commands are logged at the `:info` level. Additional logging is done
# at the `:debug` level.
#
# @option options [Boolean] :mirror Set up a mirror of the source repository.
#
# @option options [String] :origin Use the value instead `origin` to track
# the upstream repository.
#
# @option options [Pathname] :path The directory to clone into. May be used
# as an alternative to the `directory` parameter. If specified, the
# `path` option is used instead of the `directory` parameter.
#
# @option options [Boolean] :recursive After the clone is created, initialize
# all submodules within, using their default settings.
#
# @example Clone into the default directory `ruby-git`
# git = Git.clone('https://github.com/ruby-git/ruby-git.git')
#
# @example Clone and then checkout the `development` branch
# git = Git.clone('https://github.com/ruby-git/ruby-git.git', branch: 'development')
#
# @example Clone into a different directory `my-ruby-git`
# git = Git.clone('https://github.com/ruby-git/ruby-git.git', 'my-ruby-git')
# # or:
# git = Git.clone('https://github.com/ruby-git/ruby-git.git', path: 'my-ruby-git')
#
# @example Create a bare repository in the directory `ruby-git.git`
# git = Git.clone('https://github.com/ruby-git/ruby-git.git', bare: true)
#
# @example Clone a repository and set a single config option
# git = Git.clone(
# 'https://github.com/ruby-git/ruby-git.git',
# config: 'submodule.recurse=true'
# )
#
# @example Clone a repository and set multiple config options
# git = Git.clone(
# 'https://github.com/ruby-git/ruby-git.git',
# config: ['user.name=John Doe', 'user.email=dev@example.invalid']
# )
#
# @return [Git::Base] an object that can execute git commands in the context
# of the cloned local working copy or cloned repository.
#
def self.clone(repository_url, directory = nil, options = {})
clone_to_options = options.select { |key, _value| %i[bare mirror].include?(key) }
directory ||= Git::URL.clone_to(repository_url, **clone_to_options)
Base.clone(repository_url, directory, options)
end
# Returns the name of the default branch of the given repository
#
# @example with a URI string
# Git.default_branch('https://github.com/ruby-git/ruby-git') # => 'master'
# Git.default_branch('https://github.com/rspec/rspec-core') # => 'main'
#
# @example with a URI object
# repository_uri = URI('https://github.com/ruby-git/ruby-git')
# Git.default_branch(repository_uri) # => 'master'
#
# @example with a local repository
# Git.default_branch('.') # => 'master'
#
# @example with a local repository Pathname
# repository_path = Pathname('.')
# Git.default_branch(repository_path) # => 'master'
#
# @example with the logging option
# logger = Logger.new(STDOUT, level: Logger::INFO)
# Git.default_branch('.', log: logger) # => 'master'
# I, [2022-04-13T16:01:33.221596 #18415] INFO -- : git '-c' 'core.quotePath=true' '-c' 'color.ui=false' ls-remote '--symref' '--' '.' 'HEAD' 2>&1
#
# @param repository [URI, Pathname, String] The (possibly remote) repository to get the default branch name for
#
# See [GIT URLS](https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a)
# for more information.
#
# @param [Hash] options The options for this command (see list of valid
# options below)
#
# @option options [Logger] :log A logger to use for Git operations. Git
# commands are logged at the `:info` level. Additional logging is done
# at the `:debug` level.
#
# @return [String] the name of the default branch
#
def self.default_branch(repository, options = {})
Base.repository_default_branch(repository, options)
end
# Export the current HEAD (or a branch, if <tt>options[:branch]</tt>
# is specified) into the +name+ directory, then remove all traces of git from the
# directory.
#
# See +clone+ for options. Does not obey the <tt>:remote</tt> option,
# since the .git info will be deleted anyway; always uses the default
# remote, 'origin.'
def self.export(repository, name, options = {})
options.delete(:remote)
repo = clone(repository, name, {:depth => 1}.merge(options))
repo.checkout("origin/#{options[:branch]}") if options[:branch]
FileUtils.rm_r File.join(repo.dir.to_s, '.git')
end
# Same as g.config, but forces it to be at the global level
#
#g.config('user.name', 'Scott Chacon') # sets value
#g.config('user.email', 'dev@example.invalid') # sets value
#g.config('user.name') # returns 'Scott Chacon'
#g.config # returns whole config hash
def self.global_config(name = nil, value = nil)
lib = Git::Lib.new(nil, nil)
if(name && value)
# set value
lib.global_config_set(name, value)
elsif (name)
# return value
lib.global_config_get(name)
else
# return hash
lib.global_config_list
end
end
# Create an empty Git repository or reinitialize an existing Git repository
#
# @param [Pathname] directory If the `:bare` option is NOT given or is not
# `true`, the repository will be created in `"#{directory}/.git"`.
# Otherwise, the repository is created in `"#{directory}"`.
#
# All directories along the path to `directory` are created if they do not exist.
#
# A relative path is referenced from the current working directory of the process
# and converted to an absolute path using
# [File.expand_path](https://www.rubydoc.info/stdlib/core/File.expand_path).
#
# @param [Hash] options The options for this command (see list of valid
# options below)
#
# @option options [Boolean] :bare Instead of creating a repository at
# `"#{directory}/.git"`, create a bare repository at `"#{directory}"`.
# See [what is a bare repository?](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbarerepositoryabarerepository).
#
# @option options [String] :initial_branch Use the specified name for the
# initial branch in the newly created repository.
#
# @option options [Pathname] :repository the path to put the newly initialized
# Git repository. The default for non-bare repository is `"#{directory}/.git"`.
#
# A relative path is referenced from the current working directory of the process
# and converted to an absolute path using
# [File.expand_path](https://www.rubydoc.info/stdlib/core/File.expand_path).
#
# @option options [Logger] :log A logger to use for Git operations. Git
# commands are logged at the `:info` level. Additional logging is done
# at the `:debug` level.
#
# @return [Git::Base] an object that can execute git commands in the context
# of the newly initialized repository
#
# @example Initialize a repository in the current directory
# git = Git.init
#
# @example Initialize a repository in some other directory
# git = Git.init '~/code/ruby-git'
#
# @example Initialize a bare repository
# git = Git.init '~/code/ruby-git.git', bare: true
#
# @example Initialize a repository in a non-default location (outside of the working copy)
# git = Git.init '~/code/ruby-git', repository: '~/code/ruby-git.git'
#
# @see https://git-scm.com/docs/git-init git init
#
def self.init(directory = '.', options = {})
Base.init(directory, options)
end
# returns a Hash containing information about the references
# of the target repository
#
# options
# :refs
#
# @param [String|NilClass] location the target repository location or nil for '.'
# @return [{String=>Hash}] the available references of the target repo.
def self.ls_remote(location = nil, options = {})
Git::Lib.new.ls_remote(location, options)
end
# Open a an existing Git working directory
#
# Git.open will most likely be the most common way to create
# a git reference, referring to an existing working directory.
#
# If not provided in the options, the library will assume
# the repository and index are in the default places (`.git/`, `.git/index`).
#
# @example Open the Git working directory in the current directory
# git = Git.open
#
# @example Open a Git working directory in some other directory
# git = Git.open('~/Projects/ruby-git')
#
# @example Use a logger to see what is going on
# logger = Logger.new(STDOUT)
# git = Git.open('~/Projects/ruby-git', log: logger)
#
# @example Open a working copy whose repository is in a non-standard directory
# git = Git.open('~/Projects/ruby-git', repository: '~/Project/ruby-git.git')
#
# @param [Pathname] working_dir the path to the working directory to use
# for git commands.
#
# A relative path is referenced from the current working directory of the process
# and converted to an absolute path using
# [File.expand_path](https://www.rubydoc.info/stdlib/core/File.expand_path).
#
# @param [Hash] options The options for this command (see list of valid
# options below)
#
# @option options [Pathname] :repository used to specify a non-standard path to
# the repository directory. The default is `"#{working_dir}/.git"`.
#
# @option options [Pathname] :index used to specify a non-standard path to an
# index file. The default is `"#{working_dir}/.git/index"`
#
# @option options [Logger] :log A logger to use for Git operations. Git
# commands are logged at the `:info` level. Additional logging is done
# at the `:debug` level.
#
# @return [Git::Base] an object that can execute git commands in the context
# of the opened working copy
#
def self.open(working_dir, options = {})
Base.open(working_dir, options)
end
# Return the version of the git binary
#
# @example
# Git.binary_version # => [2, 46, 0]
#
# @return [Array<Integer>] the version of the git binary
#
def self.binary_version(binary_path = Git::Base.config.binary_path)
Base.binary_version(binary_path)
end
end
```
### oss/ruby-git/lib/git/author.rb
```ruby
# frozen_string_literal: true
module Git
class Author
attr_accessor :name, :email, :date
def initialize(author_string)
if m = /(.*?) <(.*?)> (\d+) (.*)/.match(author_string)
@name = m[1]
@email = m[2]
@date = Time.at(m[3].to_i)
end
end
end
end
```
### oss/ruby-git/lib/git/base.rb
```ruby
# frozen_string_literal: true
require 'logger'
require 'open3'
module Git
# The main public interface for interacting with Git commands
#
# Instead of creating a Git::Base directly, obtain a Git::Base instance by
# calling one of the follow {Git} class methods: {Git.open}, {Git.init},
# {Git.clone}, or {Git.bare}.
#
# @api public
#
class Base
# (see Git.bare)
def self.bare(git_dir, options = {})
normalize_paths(options, default_repository: git_dir, bare: true)
self.new(options)
end
# (see Git.clone)
def self.clone(repository_url, directory, options = {})
new_options = Git::Lib.new(nil, options[:log]).clone(repository_url, directory, options)
normalize_paths(new_options, bare: options[:bare] || options[:mirror])
new(new_options)
end
# (see Git.default_branch)
def self.repository_default_branch(repository, options = {})
Git::Lib.new(nil, options[:log]).repository_default_branch(repository)
end
# Returns (and initialize if needed) a Git::Config instance
#
# @return [Git::Config] the current config instance.
def self.config
@@config ||= Config.new
end
def self.binary_version(binary_path)
result = nil
status = nil
begin
result, status = Open3.capture2e(binary_path, "-c", "core.quotePath=true", "-c", "color.ui=false", "version")
result = result.chomp
rescue Errno::ENOENT
raise RuntimeError, "Failed to get git version: #{binary_path} not found"
end
if status.success?
version = result[/\d+(\.\d+)+/]
version_parts = version.split('.').collect { |i| i.to_i }
version_parts.fill(0, version_parts.length...3)
else
raise RuntimeError, "Failed to get git version: #{status}\n#{result}"
end
end
# (see Git.init)
def self.init(directory = '.', options = {})
normalize_paths(options, default_working_directory: directory, default_repository: directory, bare: options[:bare])
init_options = {
:bare => options[:bare],
:initial_branch => options[:initial_branch]
}
directory = options[:bare] ? options[:repository] : options[:working_directory]
FileUtils.mkdir_p(directory) unless File.exist?(directory)
# TODO: this dance seems awkward: this creates a Git::Lib so we can call
# init so we can create a new Git::Base which in turn (ultimately)
# creates another/different Git::Lib.
#
# TODO: maybe refactor so this Git::Bare.init does this:
# self.new(opts).init(init_opts) and move all/some of this code into
# Git::Bare#init. This way the init method can be called on any
# repository you have a Git::Base instance for. This would not
# change the existing interface (other than adding to it).
#
Git::Lib.new(options).init(init_options)
self.new(options)
end
def self.root_of_worktree(working_dir)
result = working_dir
status = nil
raise ArgumentError, "'#{working_dir}' does not exist" unless Dir.exist?(working_dir)
begin
result, status = Open3.capture2e(Git::Base.config.binary_path, "-c", "core.quotePath=true", "-c", "color.ui=false", "rev-parse", "--show-toplevel", chdir: File.expand_path(working_dir))
result = result.chomp
rescue Errno::ENOENT
raise ArgumentError, "Failed to find the root of the worktree: git binary not found"
end
raise ArgumentError, "'#{working_dir}' is not in a git working tree" unless status.success?
result
end
# (see Git.open)
def self.open(working_dir, options = {})
raise ArgumentError, "'#{working_dir}' is not a directory" unless Dir.exist?(working_dir)
working_dir = root_of_worktree(working_dir) unless options[:repository]
normalize_paths(options, default_working_directory: working_dir)
self.new(options)
end
# Create an object that executes Git commands in the context of a working
# copy or a bare repository.
#
# @param [Hash] options The options for this command (see list of valid
# options below)
#
# @option options [Pathname] :working_dir the path to the root of the working
# directory. Should be `nil` if executing commands on a bare repository.
#
# @option options [Pathname] :repository used to specify a non-standard path to
# the repository directory. The default is `"#{working_dir}/.git"`.
#
# @option options [Pathname] :index used to specify a non-standard path to an
# index file. The default is `"#{working_dir}/.git/index"`
#
# @option options [Logger] :log A logger to use for Git operations. Git
# commands are logged at the `:info` level. Additional logging is done
# at the `:debug` level.
#
# @return [Git::Base] an object that can execute git commands in the context
# of the opened working copy or bare repository
#
def initialize(options = {})
if working_dir = options[:working_directory]
options[:repository] ||= File.join(working_dir, '.git')
options[:index] ||= File.join(options[:repository], 'index')
end
@logger = (options[:log] || Logger.new(nil))
@logger.info("Starting Git")
@working_directory = options[:working_directory] ? Git::WorkingDirectory.new(options[:working_directory]) : nil
@repository = options[:repository] ? Git::Repository.new(options[:repository]) : nil
@index = options[:index] ? Git::Index.new(options[:index], false) : nil
end
# Update the index from the current worktree to prepare the for the next commit
#
# @example
# lib.add('path/to/file')
# lib.add(['path/to/file1','path/to/file2'])
# lib.add(all: true)
#
# @param [String, Array<String>] paths a file or files to be added to the repository (relative to the worktree root)
# @param [Hash] options
#
# @option options [Boolean] :all Add, modify, and remove index entries to match the worktree
# @option options [Boolean] :force Allow adding otherwise ignored files
#
def add(paths = '.', **options)
self.lib.add(paths, options)
end
# adds a new remote to this repository
# url can be a git url or a Git::Base object if it's a local reference
#
# @git.add_remote('scotts_git', 'git://repo.or.cz/rubygit.git')
# @git.fetch('scotts_git')
# @git.merge('scotts_git/master')
#
# Options:
# :fetch => true
# :track => <branch_name>
def add_remote(name, url, opts = {})
url = url.repo.path if url.is_a?(Git::Base)
self.lib.remote_add(name, url, opts)
Git::Remote.new(self, name)
end
# Create a new git tag
#
# @example
# repo.add_tag('tag_name', object_reference)
# repo.add_tag('tag_name', object_reference, {:options => 'here'})
# repo.add_tag('tag_name', {:options => 'here'})
#
# @param [String] name The name of the tag to add
# @param [Hash] options Opstions to pass to `git tag`.
# See [git-tag](https://git-scm.com/docs/git-tag) for more details.
# @option options [boolean] :annotate Make an unsigned, annotated tag object
# @option options [boolean] :a An alias for the `:annotate` option
# @option options [boolean] :d Delete existing tag with the given names.
# @option options [boolean] :f Replace an existing tag with the given name (instead of failing)
# @option options [String] :message Use the given tag message
# @option options [String] :m An alias for the `:message` option
# @option options [boolean] :s Make a GPG-signed tag.
#
def add_tag(name, *options)
self.lib.tag(name, *options)
self.tag(name)
end
# changes current working directory for a block
# to the git working directory
#
# example
# @git.chdir do
# # write files
# @git.add
# @git.commit('message')
# end
def chdir # :yields: the Git::Path
Dir.chdir(dir.path) do
yield dir.path
end
end
#g.config('user.name', 'Scott Chacon') # sets value
#g.config('user.email', 'dev@example.invalid') # sets value
#g.config('user.email', 'dev@example.invalid', file: 'path/to/custom/config) # sets value in file
#g.config('user.name') # returns 'Scott Chacon'
#g.config # returns whole config hash
def config(name = nil, value = nil, options = {})
if name && value
# set value
lib.config_set(name, value, options)
elsif name
# return value
lib.config_get(name)
else
# return hash
lib.config_list
end
end
# returns a reference to the working directory
# @git.dir.path
# @git.dir.writeable?
def dir
@working_directory
end
# returns reference to the git index file
def index
@index
end
# returns reference to the git repository directory
# @git.dir.path
def repo
@repository
end
# returns the repository size in bytes
def repo_size
Dir.glob(File.join(repo.path, '**', '*'), File::FNM_DOTMATCH).reject do |f|
f.include?('..')
end.map do |f|
File.expand_path(f)
end.uniq.map do |f|
File.stat(f).size.to_i
end.reduce(:+)
end
def set_index(index_file, check = true)
@lib = nil
@index = Git::Index.new(index_file.to_s, check)
end
def set_working(work_dir, check = true)
@lib = nil
@working_directory = Git::WorkingDirectory.new(work_dir.to_s, check)
end
# returns +true+ if the branch exists locally
def is_local_branch?(branch)
branch_names = self.branches.local.map {|b| b.name}
branch_names.include?(branch)
end
# returns +true+ if the branch exists remotely
def is_remote_branch?(branch)
branch_names = self.branches.remote.map {|b| b.name}
branch_names.include?(branch)
end
# returns +true+ if the branch exists
def is_branch?(branch)
branch_names = self.branches.map {|b| b.name}
branch_names.include?(branch)
end
# this is a convenience method for accessing the class that wraps all the
# actual 'git' forked system calls. At some point I hope to replace the Git::Lib
# class with one that uses native methods or libgit C bindings
def lib
@lib ||= Git::Lib.new(self, @logger)
end
# Run a grep for 'string' on the HEAD of the git repository
#
# @example Limit grep's scope by calling grep() from a specific object:
# git.object("v2.3").grep('TODO')
#
# @example Using grep results:
# git.grep("TODO").each do |sha, arr|
# puts "in blob #{sha}:"
# arr.each do |line_no, match_string|
# puts "\t line #{line_no}: '#{match_string}'"
# end
# end
#
# @param string [String] the string to search for
# @param path_limiter [String, Array] a path or array of paths to limit the search to or nil for no limit
# @param opts [Hash] options to pass to the underlying `git grep` command
#
# @option opts [Boolean] :ignore_case (false) ignore case when matching
# @option opts [Boolean] :invert_match (false) select non-matching lines
# @option opts [Boolean] :extended_regexp (false) use extended regular expressions
# @option opts [String] :object (HEAD) the object to search from
#
# @return [Hash<String, Array>] a hash of arrays
# ```Ruby
# {
# 'tree-ish1' => [[line_no1, match_string1], ...],
# 'tree-ish2' => [[line_no1, match_string1], ...],
# ...
# }
# ```
#
def grep(string, path_limiter = nil, opts = {})
self.object('HEAD').grep(string, path_limiter, opts)
end
# List the files in the worktree that are ignored by git
# @return [Array<String>] the list of ignored files relative to teh root of the worktree
#
def ignored_files
self.lib.ignored_files
end
# removes file(s) from the git repository
def rm(path = '.', opts = {})
self.lib.rm(path, opts)
end
alias remove rm
# resets the working directory to the provided commitish
def reset(commitish = nil, opts = {})
self.lib.reset(commitish, opts)
end
# resets the working directory to the commitish with '--hard'
def reset_hard(commitish = nil, opts = {})
opts = {:hard => true}.merge(opts)
self.lib.reset(commitish, opts)
end
# cleans the working directory
#
# options:
# :force
# :d
# :ff
#
def clean(opts = {})
self.lib.clean(opts)
end
# returns the most recent tag that is reachable from a commit
#
# options:
# :all
# :tags
# :contains
# :debug
# :exact_match
# :dirty
# :abbrev
# :candidates
# :long
# :always
# :match
#
def describe(committish=nil, opts={})
self.lib.describe(committish, opts)
end
# reverts the working directory to the provided commitish.
# Accepts a range, such as comittish..HEAD
#
# options:
# :no_edit
#
def revert(commitish = nil, opts = {})
self.lib.revert(commitish, opts)
end
# commits all pending changes in the index file to the git repository
#
# options:
# :all
# :allow_empty
# :amend
# :author
#
def commit(message, opts = {})
self.lib.commit(message, opts)
end
# commits all pending changes in the index file to the git repository,
# but automatically adds all modified files without having to explicitly
# calling @git.add() on them.
def commit_all(message, opts = {})
opts = {:add_all => true}.merge(opts)
self.lib.commit(message, opts)
end
# checks out a branch as the new git working directory
def checkout(*args, **options)
self.lib.checkout(*args, **options)
end
# checks out an old version of a file
def checkout_file(version, file)
self.lib.checkout_file(version,file)
end
# fetches changes from a remote branch - this does not modify the working directory,
# it just gets the changes from the remote if there are any
def fetch(remote = 'origin', opts = {})
if remote.is_a?(Hash)
opts = remote
remote = nil
end
self.lib.fetch(remote, opts)
end
# Push changes to a remote repository
#
# @overload push(remote = nil, branch = nil, options = {})
# @param remote [String] the remote repository to push to
# @param branch [String] the branch to push
# @param options [Hash] options to pass to the push command
#
# @option opts [Boolean] :mirror (false) Push all refs under refs/heads/, refs/tags/ and refs/remotes/
# @option opts [Boolean] :delete (false) Delete refs that don't exist on the remote
# @option opts [Boolean] :force (false) Force updates
# @option opts [Boolean] :tags (false) Push all refs under refs/tags/
# @option opts [Array, String] :push_options (nil) Push options to transmit
#
# @return [Void]
#
# @raise [Git::FailedError] if the push fails
# @raise [ArgumentError] if a branch is given without a remote
#
def push(*args, **options)
self.lib.push(*args, **options)
end
# merges one or more branches into the current working branch
#
# you can specify more than one branch to merge by passing an array of branches
def merge(branch, message = 'merge', opts = {})
self.lib.merge(branch, message, opts)
end
# iterates over the files which are unmerged
def each_conflict(&block) # :yields: file, your_version, their_version
self.lib.conflicts(&block)
end
# Pulls the given branch from the given remote into the current branch
#
# @param remote [String] the remote repository to pull from
# @param branch [String] the branch to pull from
# @param opts [Hash] options to pass to the pull command
#
# @option opts [Boolean] :allow_unrelated_histories (false) Merges histories of two projects that started their
# lives independently
# @example pulls from origin/master
# @git.pull
# @example pulls from upstream/master
# @git.pull('upstream')
# @example pulls from upstream/develop
# @git.pull('upstream', 'develop')
#
# @return [Void]
#
# @raise [Git::FailedError] if the pull fails
# @raise [ArgumentError] if a branch is given without a remote
def pull(remote = nil, branch = nil, opts = {})
self.lib.pull(remote, branch, opts)
end
# returns an array of Git:Remote objects
def remotes
self.lib.remotes.map { |r| Git::Remote.new(self, r) }
end
# sets the url for a remote
# url can be a git url or a Git::Base object if it's a local reference
#
# @git.set_remote_url('scotts_git', 'git://repo.or.cz/rubygit.git')
#
def set_remote_url(name, url)
url = url.repo.path if url.is_a?(Git::Base)
self.lib.remote_set_url(name, url)
Git::Remote.new(self, name)
end
# removes a remote from this repository
#
# @git.remove_remote('scott_git')
def remove_remote(name)
self.lib.remote_remove(name)
end
# returns an array of all Git::Tag objects for this repository
def tags
self.lib.tags.map { |r| tag(r) }
end
# Create a new git tag
#
# @example
# repo.add_tag('tag_name', object_reference)
# repo.add_tag('tag_name', object_reference, {:options => 'here'})
# repo.add_tag('tag_name', {:options => 'here'})
#
# @param [String] name The name of the tag to add
# @param [Hash] options Opstions to pass to `git tag`.
# See [git-tag](https://git-scm.com/docs/git-tag) for more details.
# @option options [boolean] :annotate Make an unsigned, annotated tag object
# @option options [boolean] :a An alias for the `:annotate` option
# @option options [boolean] :d Delete existing tag with the given names.
# @option options [boolean] :f Replace an existing tag with the given name (instead of failing)
# @option options [String] :message Use the given tag message
# @option options [String] :m An alias for the `:message` option
# @option options [boolean] :s Make a GPG-signed tag.
#
def add_tag(name, *options)
self.lib.tag(name, *options)
self.tag(name)
end
# deletes a tag
def delete_tag(name)
self.lib.tag(name, {:d => true})
end
# creates an archive file of the given tree-ish
def archive(treeish, file = nil, opts = {})
self.object(treeish).archive(file, opts)
end
# repacks the repository
def repack
self.lib.repack
end
def gc
self.lib.gc
end
def apply(file)
if File.exist?(file)
self.lib.apply(file)
end
end
def apply_mail(file)
self.lib.apply_mail(file) if File.exist?(file)
end
# Shows objects
#
# @param [String|NilClass] objectish the target object reference (nil == HEAD)
# @param [String|NilClass] path the path of the file to be shown
# @return [String] the object information
def show(objectish=nil, path=nil)
self.lib.show(objectish, path)
end
## LOWER LEVEL INDEX OPERATIONS ##
def with_index(new_index) # :yields: new_index
old_index = @index
set_index(new_index, false)
return_value = yield @index
set_index(old_index)
return_value
end
def with_temp_index &blk
# Workaround for JRUBY, since they handle the TempFile path different.
# MUST be improved to be safer and OS independent.
if RUBY_PLATFORM == 'java'
temp_path = "/tmp/temp-index-#{(0...15).map{ ('a'..'z').to_a[rand(26)] }.join}"
else
tempfile = Tempfile.new('temp-index')
temp_path = tempfile.path
tempfile.close
tempfile.unlink
end
with_index(temp_path, &blk)
end
def checkout_index(opts = {})
self.lib.checkout_index(opts)
end
def read_tree(treeish, opts = {})
self.lib.read_tree(treeish, opts)
end
def write_tree
self.lib.write_tree
end
def write_and_commit_tree(opts = {})
tree = write_tree
commit_tree(tree, opts)
end
def update_ref(branch, commit)
branch(branch).update_ref(commit)
end
def ls_files(location=nil)
self.lib.ls_files(location)
end
def with_working(work_dir) # :yields: the Git::WorkingDirectory
return_value = false
old_working = @working_directory
set_working(work_dir)
Dir.chdir work_dir do
return_value = yield @working_directory
end
set_working(old_working)
return_value
end
def with_temp_working &blk
tempfile = Tempfile.new("temp-workdir")
temp_dir = tempfile.path
tempfile.close
tempfile.unlink
Dir.mkdir(temp_dir, 0700)
with_working(temp_dir, &blk)
end
# runs git rev-parse to convert the objectish to a full sha
#
# @example
# git.rev_parse("HEAD^^")
# git.rev_parse('v2.4^{tree}')
# git.rev_parse('v2.4:/doc/index.html')
#
def rev_parse(objectish)
self.lib.rev_parse(objectish)
end
# For backwards compatibility
alias revparse rev_parse
def ls_tree(objectish, opts = {})
self.lib.ls_tree(objectish, opts)
end
def cat_file(objectish)
self.lib.cat_file(objectish)
end
# The name of the branch HEAD refers to or 'HEAD' if detached
#
# Returns one of the following:
# * The branch name that HEAD refers to (even if it is an unborn branch)
# * 'HEAD' if in a detached HEAD state
#
# @return [String] the name of the branch HEAD refers to or 'HEAD' if detached
#
def current_branch
self.lib.branch_current
end
# @return [Git::Branch] an object for branch_name
def branch(branch_name = self.current_branch)
Git::Branch.new(self, branch_name)
end
# @return [Git::Branches] a collection of all the branches in the repository.
# Each branch is represented as a {Git::Branch}.
def branches
Git::Branches.new(self)
end
# returns a Git::Worktree object for dir, commitish
def worktree(dir, commitish = nil)
Git::Worktree.new(self, dir, commitish)
end
# returns a Git::worktrees object of all the Git::Worktrees
# objects for this repo
def worktrees
Git::Worktrees.new(self)
end
# @return [Git::Object::Commit] a commit object
def commit_tree(tree = nil, opts = {})
Git::Object::Commit.new(self, self.lib.commit_tree(tree, opts))
end
# @return [Git::Diff] a Git::Diff object
def diff(objectish = 'HEAD', obj2 = nil)
Git::Diff.new(self, objectish, obj2)
end
# @return [Git::Object] a Git object
def gblob(objectish)
Git::Object.new(self, objectish, 'blob')
end
# @return [Git::Object] a Git object
def gcommit(objectish)
Git::Object.new(self, objectish, 'commit')
end
# @return [Git::Object] a Git object
def gtree(objectish)
Git::Object.new(self, objectish, 'tree')
end
# @return [Git::Log] a log with the specified number of commits
def log(count = 30)
Git::Log.new(self, count)
end
# returns a Git::Object of the appropriate type
# you can also call @git.gtree('tree'), but that's
# just for readability. If you call @git.gtree('HEAD') it will
# still return a Git::Object::Commit object.
#
# object calls a method that will run a rev-parse
# on the objectish and determine the type of the object and return
# an appropriate object for that type
#
# @return [Git::Object] an instance of the appropriate type of Git::Object
def object(objectish)
Git::Object.new(self, objectish)
end
# @return [Git::Remote] a remote of the specified name
def remote(remote_name = 'origin')
Git::Remote.new(self, remote_name)
end
# @return [Git::Status] a status object
def status
Git::Status.new(self)
end
# @return [Git::Object::Tag] a tag object
def tag(tag_name)
Git::Object.new(self, tag_name, 'tag', true)
end
# Find as good common ancestors as possible for a merge
# example: g.merge_base('master', 'some_branch', 'some_sha', octopus: true)
#
# @return [Array<Git::Object::Commit>] a collection of common ancestors
def merge_base(*args)
shas = self.lib.merge_base(*args)
shas.map { |sha| gcommit(sha) }
end
private
# Normalize options before they are sent to Git::Base.new
#
# Updates the options parameter by setting appropriate values for the following keys:
# * options[:working_directory]
# * options[:repository]
# * options[:index]
#
# All three values will be set to absolute paths. An exception is that
# :working_directory will be set to nil if bare is true.
#
private_class_method def self.normalize_paths(
options, default_working_directory: nil, default_repository: nil, bare: false
)
normalize_working_directory(options, default: default_working_directory, bare: bare)
normalize_repository(options, default: default_repository, bare: bare)
normalize_index(options)
end
# Normalize options[:working_directory]
#
# If working with a bare repository, set to `nil`.
# Otherwise, set to the first non-nil value of:
# 1. `options[:working_directory]`,
# 2. the `default` parameter, or
# 3. the current working directory
#
# Finally, if options[:working_directory] is a relative path, convert it to an absoluite
# path relative to the current directory.
#
private_class_method def self.normalize_working_directory(options, default:, bare: false)
working_directory =
if bare
nil
else
File.expand_path(options[:working_directory] || default || Dir.pwd)
end
options[:working_directory] = working_directory
end
# Normalize options[:repository]
#
# If working with a bare repository, set to the first non-nil value out of:
# 1. `options[:repository]`
# 2. the `default` parameter
# 3. the current working directory
#
# Otherwise, set to the first non-nil value of:
# 1. `options[:repository]`
# 2. `.git`
#
# Next, if options[:repository] refers to a *file* and not a *directory*, set
# options[:repository] to the contents of that file. This is the case when
# working with a submodule or a secondary working tree (created with git worktree
# add). In these cases the repository is actually contained/nested within the
# parent's repository directory.
#
# Finally, if options[:repository] is a relative path, convert it to an absolute
# path relative to:
# 1. the current directory if working with a bare repository or
# 2. the working directory if NOT working with a bare repository
#
private_class_method def self.normalize_repository(options, default:, bare: false)
repository =
if bare
File.expand_path(options[:repository] || default || Dir.pwd)
else
File.expand_path(options[:repository] || '.git', options[:working_directory])
end
if File.file?(repository)
repository = File.expand_path(File.open(repository).read[8..-1].strip, options[:working_directory])
end
options[:repository] = repository
end
# Normalize options[:index]
#
# If options[:index] is a relative directory, convert it to an absolute
# directory relative to the repository directory
#
private_class_method def self.normalize_index(options)
index = File.expand_path(options[:index] || 'index', options[:repository])
options[:index] = index
end
end
end
```
### oss/ruby-git/lib/git/branch.rb
```ruby
# frozen_string_literal: true
require 'git/path'
module Git
class Branch < Path
attr_accessor :full, :remote, :name
def initialize(base, name)
@full = name
@base = base
@gcommit = nil
@stashes = nil
@remote, @name = parse_name(name)
end
def gcommit
@gcommit ||= @base.gcommit(@full)
@gcommit
end
def stashes
@stashes ||= Git::Stashes.new(@base)
end
def checkout
check_if_create
@base.checkout(@full)
end
def archive(file, opts = {})
@base.lib.archive(@full, file, opts)
end
# g.branch('new_branch').in_branch do
# # create new file
# # do other stuff
# return true # auto commits and switches back
# end
def in_branch(message = 'in branch work')
old_current = @base.lib.branch_current
checkout
if yield
@base.commit_all(message)
else
@base.reset_hard
end
@base.checkout(old_current)
end
def create
check_if_create
end
def delete
@base.lib.branch_delete(@name)
end
def current
determine_current
end
def contains?(commit)
!@base.lib.branch_contains(commit, self.name).empty?
end
def merge(branch = nil, message = nil)
if branch
in_branch do
@base.merge(branch, message)
false
end
# merge a branch into this one
else
# merge this branch into the current one
@base.merge(@name)
end
end
def update_ref(commit)
if @remote
@base.lib.update_ref("refs/remotes/#{@remote.name}/#{@name}", commit)
else
@base.lib.update_ref("refs/heads/#{@name}", commit)
end
end
def to_a
[@full]
end
def to_s
@full
end
private
def check_if_create
@base.lib.branch_new(@name) rescue nil
end
def determine_current
@base.lib.branch_current == @name
end
BRANCH_NAME_REGEXP = %r{
^
# Optional 'refs/remotes/' at the beggining to specify a remote tracking branch
# with a <remote_name>. <remote_name> is nil if not present.
(?:
(?:(?:refs/)?remotes/)(?<remote_name>[^/]+)/
)?
(?<branch_name>.*)
$
}x
# Given a full branch name return an Array containing the remote and branch names.
#
# Removes 'remotes' from the beggining of the name (if present).
# Takes the second part (splittign by '/') as the remote name.
# Takes the rest as the repo name (can also hold one or more '/').
#
# Example:
# # local branches
# parse_name('master') #=> [nil, 'master']
# parse_name('origin/master') #=> [nil, 'origin/master']
# parse_name('origin/master/v2') #=> [nil, 'origin/master']
#
# # remote branches
# parse_name('remotes/origin/master') #=> ['origin', 'master']
# parse_name('remotes/origin/master/v2') #=> ['origin', 'master/v2']
# parse_name('refs/remotes/origin/master') #=> ['origin', 'master']
# parse_name('refs/remotes/origin/master/v2') #=> ['origin', 'master/v2']
#
# param [String] name branch full name.
# return [<Git::Remote,NilClass,String>] an Array containing the remote and branch names.
def parse_name(name)
# Expect this will always match
match = name.match(BRANCH_NAME_REGEXP)
remote = match[:remote_name] ? Git::Remote.new(@base, match[:remote_name]) : nil
branch_name = match[:branch_name]
[ remote, branch_name ]
end
end
end
```
> NOTE [shards]: The sample search index `glacier_idx_7` is provisioned with 11 primary shards.
### oss/ruby-git/lib/git/branches.rb
```ruby
# frozen_string_literal: true
module Git
# object that holds all the available branches
class Branches
include Enumerable
def initialize(base)
@branches = {}
@base = base
@base.lib.branches_all.each do |b|
@branches[b[0]] = Git::Branch.new(@base, b[0])
end
end
def local
self.select { |b| !b.remote }
end
def remote
self.select { |b| b.remote }
end
# array like methods
def size
@branches.size
end
def each(&block)
@branches.values.each(&block)
end
# Returns the target branch
#
# Example:
# Given (git branch -a):
# master
# remotes/working/master
#
# g.branches['master'].full #=> 'master'
# g.branches['working/master'].full => 'remotes/working/master'
# g.branches['remotes/working/master'].full => 'remotes/working/master'
#
# @param [#to_s] branch_name the target branch name.
# @return [Git::Branch] the target branch.
def [](branch_name)
@branches.values.inject(@branches) do |branches, branch|
branches[branch.full] ||= branch
# This is how Git (version 1.7.9.5) works.
# Lets you ignore the 'remotes' if its at the beginning of the branch full name (even if is not a real remote branch).
branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ /^remotes\/.+/
branches
end[branch_name.to_s]
end
def to_s
out = ''
@branches.each do |k, b|
out << (b.current ? '* ' : ' ') << b.to_s << "\n"
end
out
end
end
end
```
### oss/ruby-git/lib/git/command_line.rb
```ruby
# frozen_string_literal: true
require 'git/base'
require 'git/command_line_result'
require 'git/errors'
require 'stringio'
module Git
# Runs a git command and returns the result
#
# @api public
#
class CommandLine
# Create a Git::CommandLine object
#
# @example
# env = { 'GIT_DIR' => '/path/to/git/dir' }
# binary_path = '/usr/bin/git'
# global_opts = %w[--git-dir /path/to/git/dir]
# logger = Logger.new(STDOUT)
# cli = CommandLine.new(env, binary_path, global_opts, logger)
# cli.run('version') #=> #<Git::CommandLineResult:0x00007f9b0c0b0e00
#
# @param env [Hash<String, String>] environment variables to set
# @param global_opts [Array<String>] global options to pass to git
# @param logger [Logger] the logger to use
#
def initialize(env, binary_path, global_opts, logger)
@env = env
@binary_path = binary_path
@global_opts = global_opts
@logger = logger
end
# @attribute [r] env
#
# Variables to set (or unset) in the git command's environment
#
# @example
# env = { 'GIT_DIR' => '/path/to/git/dir' }
# command_line = Git::CommandLine.new(env, '/usr/bin/git', [], Logger.new(STDOUT))
# command_line.env #=> { 'GIT_DIR' => '/path/to/git/dir' }
#
# @return [Hash<String, String>]
#
# @see https://ruby-doc.org/3.2.1/Process.html#method-c-spawn Process.spawn
# for details on how to set environment variables using the `env` parameter
#
attr_reader :env
# @attribute [r] binary_path
#
# The path to the command line binary to run
#
# @example
# binary_path = '/usr/bin/git'
# command_line = Git::CommandLine.new({}, binary_path, ['version'], Logger.new(STDOUT))
# command_line.binary_path #=> '/usr/bin/git'
#
# @return [String]
#
attr_reader :binary_path
# @attribute [r] global_opts
#
# The global options to pass to git
#
# These are options that are passed to git before the command name and
# arguments. For example, in `git --git-dir /path/to/git/dir version`, the
# global options are %w[--git-dir /path/to/git/dir].
#
# @example
# env = {}
# global_opts = %w[--git-dir /path/to/git/dir]
# logger = Logger.new(nil)
# cli = CommandLine.new(env, '/usr/bin/git', global_opts, logger)
# cli.global_opts #=> %w[--git-dir /path/to/git/dir]
#
# @return [Array<String>]
#
attr_reader :global_opts
# @attribute [r] logger
#
# The logger to use for logging git commands and results
#
# @example
# env = {}
# global_opts = %w[]
# logger = Logger.new(STDOUT)
# cli = CommandLine.new(env, '/usr/bin/git', global_opts, logger)
# cli.logger == logger #=> true
#
# @return [Logger]
#
attr_reader :logger
# Execute a git command, wait for it to finish, and return the result
#
# NORMALIZATION
#
# The command output is returned as a Unicde string containing the binary output
# from the command. If the binary output is not valid UTF-8, the output will
# cause problems because the encoding will be invalid.
#
# Normalization is a process that trys to convert the binary output to a valid
# UTF-8 string. It uses the `rchardet` gem to detect the encoding of the binary
# output and then converts it to UTF-8.
#
# Normalization is not enabled by default. Pass `normalize: true` to Git::CommandLine#run
# to enable it. Normalization will only be performed on stdout and only if the `out:`` option
# is nil or is a StringIO object. If the out: option is set to a file or other IO object,
# the normalize option will be ignored.
#
# @example Run a command and return the output
# cli.run('version') #=> "git version 2.39.1\n"
#
# @example The args array should be splatted into the parameter list
# args = %w[log -n 1 --oneline]
# cli.run(*args) #=> "f5baa11 beginning of Ruby/Git project\n"
#
# @example Run a command and return the chomped output
# cli.run('version', chomp: true) #=> "git version 2.39.1"
#
# @example Run a command and without normalizing the output
# cli.run('version', normalize: false) #=> "git version 2.39.1\n"
#
# @example Capture stdout in a temporary file
# require 'tempfile'
# tempfile = Tempfile.create('git') do |file|
# cli.run('version', out: file)
# file.rewind
# file.read #=> "git version 2.39.1\n"
# end
#
# @example Capture stderr in a StringIO object
# require 'stringio'
# stderr = StringIO.new
# begin
# cli.run('log', 'nonexistent-branch', err: stderr)
# rescue Git::FailedError => e
# stderr.string #=> "unknown revision or path not in the working tree.\n"
# end
#
# @param args [Array<String>] the command line arguements to pass to git
#
# This array should be splatted into the parameter list.
#
# @param out [#write, nil] the object to write stdout to or nil to ignore stdout
#
# If this is a 'StringIO' object, then `stdout_writer.string` will be returned.
#
# In general, only specify a `stdout_writer` object when you want to redirect
# stdout to a file or some other object that responds to `#write`. The default
# behavior will return the output of the command.
#
# @param err [#write] the object to write stderr to or nil to ignore stderr
#
# If this is a 'StringIO' object and `merged_output` is `true`, then
# `stderr_writer.string` will be merged into the output returned by this method.
#
# @param normalize [Boolean] whether to normalize the output to a valid encoding
#
# @param chomp [Boolean] whether to chomp the output
#
# @param merge [Boolean] whether to merge stdout and stderr in the string returned
#
# @param chdir [String] the directory to run the command in
#
# @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
#
# If timeout is zero, the timeout will not be enforced.
#
# If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
#
# If the command does not respond to SIGKILL, it will hang this method.
#
# @return [Git::CommandLineResult] the output of the command
#
# This result of running the command.
#
# @raise [ArgumentError] if `args` is not an array of strings
#
# @raise [Git::SignaledError] if the command was terminated because of an uncaught signal
#
# @raise [Git::FailedError] if the command returned a non-zero exitstatus
#
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
#
# @raise [Git::TimeoutError] if the command times out
#
def run(*args, out: nil, err: nil, normalize:, chomp:, merge:, chdir: nil, timeout: nil)
git_cmd = build_git_cmd(args)
begin
result = ProcessExecuter.run(env, *git_cmd, out: out, err: err, merge:, chdir: (chdir || :not_set), timeout: timeout, raise_errors: false)
rescue ProcessExecuter::Command::ProcessIOError => e
raise Git::ProcessIOError.new(e.message), cause: e.exception.cause
end
process_result(result, normalize, chomp, timeout)
end
private
# Build the git command line from the available sources to send to `Process.spawn`
# @return [Array<String>]
# @api private
#
def build_git_cmd(args)
raise ArgumentError.new('The args array can not contain an array') if args.any? { |a| a.is_a?(Array) }
[binary_path, *global_opts, *args].map { |e| e.to_s }
end
# Process the result of the command and return a Git::CommandLineResult
#
# Post process output, log the command and result, and raise an error if the
# command failed.
#
# @param result [ProcessExecuter::Command::Result] the result it is a Process::Status and include command, stdout, and stderr
# @param normalize [Boolean] whether to normalize the output of each writer
# @param chomp [Boolean] whether to chomp the output of each writer
# @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
#
# @return [Git::CommandLineResult] the result of the command to return to the caller
#
# @raise [Git::FailedError] if the command failed
# @raise [Git::SignaledError] if the command was signaled
# @raise [Git::TimeoutError] if the command times out
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
#
# @api private
#
def process_result(result, normalize, chomp, timeout)
command = result.command
processed_out, processed_err = post_process_all([result.stdout, result.stderr], normalize, chomp)
logger.info { "#{command} exited with status #{result}" }
logger.debug { "stdout:\n#{processed_out.inspect}\nstderr:\n#{processed_err.inspect}" }
Git::CommandLineResult.new(command, result, processed_out, processed_err).tap do |processed_result|
raise Git::TimeoutError.new(processed_result, timeout) if result.timeout?
raise Git::SignaledError.new(processed_result) if result.signaled?
raise Git::FailedError.new(processed_result) unless result.success?
end
end
# Post-process command output and return an array of the results
#
# @param raw_outputs [Array] the output to post-process
# @param normalize [Boolean] whether to normalize the output of each writer
# @param chomp [Boolean] whether to chomp the output of each writer
#
# @return [Array<String, nil>] the processed output of each command output object that supports `#string`
#
# @api private
#
def post_process_all(raw_outputs, normalize, chomp)
Array.new.tap do |result|
raw_outputs.each { |raw_output| result << post_process(raw_output, normalize, chomp) }
end
end
# Determine the output to return in the `CommandLineResult`
#
# If the writer can return the output by calling `#string` (such as a StringIO),
# then return the result of normalizing the encoding and chomping the output
# as requested.
#
# If the writer does not support `#string`, then return nil. The output is
# assumed to be collected by the writer itself such as when the writer
# is a file instead of a StringIO.
#
# @param raw_output [#string] the output to post-process
# @return [String, nil]
#
# @api private
#
def post_process(raw_output, normalize, chomp)
if raw_output.respond_to?(:string)
output = raw_output.string.dup
output = output.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join if normalize
output.chomp! if chomp
output
else
nil
end
end
end
end
```
### oss/ruby-git/lib/git/command_line_result.rb
```ruby
# frozen_string_literal: true
module Git
# The result of running a git command
#
# This object stores the Git command executed and its status, stdout, and stderr.
#
# @api public
#
class CommandLineResult
# Create a CommandLineResult object
#
# @example
# `true`
# git_cmd = %w[git version]
# status = $?
# stdout = "git version 2.39.1\n"
# stderr = ""
# result = Git::CommandLineResult.new(git_cmd, status, stdout, stderr)
#
# @param git_cmd [Array<String>] the git command that was executed
# @param status [Process::Status] the status of the process
# @param stdout [String] the output of the process
# @param stderr [String] the error output of the process
#
def initialize(git_cmd, status, stdout, stderr)
@git_cmd = git_cmd
@status = status
@stdout = stdout
@stderr = stderr
end
# @attribute [r] git_cmd
#
# The git command that was executed
#
# @example
# git_cmd = %w[git version]
# result = Git::CommandLineResult.new(git_cmd, $?, "", "")
# result.git_cmd #=> ["git", "version"]
#
# @return [Array<String>]
#
attr_reader :git_cmd
# @attribute [r] status
#
# The status of the process
#
# @example
# `true`
# status = $?
# result = Git::CommandLineResult.new(status, "", "")
# result.status #=> #<Process::Status: pid 87859 exit 0>
#
# @return [Process::Status]
#
attr_reader :status
# @attribute [r] stdout
#
# The output of the process
#
# @example
# stdout = "git version 2.39.1\n"
# result = Git::CommandLineResult.new($?, stdout, "")
# result.stdout #=> "git version 2.39.1\n"
#
# @return [String]
#
attr_reader :stdout
# @attribute [r] stderr
#
# The error output of the process
#
# @example
# stderr = "Tag not found\n"
# result = Git::CommandLineResult.new($?, "", stderr)
# result.stderr #=> "Tag not found\n"
#
# @return [String]
#
attr_reader :stderr
end
end
```
### oss/ruby-git/lib/git/config.rb
```ruby
# frozen_string_literal: true
module Git
class Config
attr_writer :binary_path, :git_ssh, :timeout
def initialize
@binary_path = nil
@git_ssh = nil
@timeout = nil
end
def binary_path
@binary_path || ENV['GIT_PATH'] && File.join(ENV['GIT_PATH'], 'git') || 'git'
end
def git_ssh
@git_ssh || ENV['GIT_SSH']
end
def timeout
@timeout || (ENV['GIT_TIMEOUT'] && ENV['GIT_TIMEOUT'].to_i)
end
end
end
```
### oss/ruby-git/lib/git/diff.rb
```ruby
# frozen_string_literal: true
module Git
# object that holds the last X commits on given branch
class Diff
include Enumerable
def initialize(base, from = nil, to = nil)
@base = base
@from = from && from.to_s
@to = to && to.to_s
@path = nil
@full_diff = nil
@full_diff_files = nil
@stats = nil
end
attr_reader :from, :to
def name_status
cache_name_status
end
def path(path)
@path = path
return self
end
def size
cache_stats
@stats[:total][:files]
end
def lines
cache_stats
@stats[:total][:lines]
end
def deletions
cache_stats
@stats[:total][:deletions]
end
def insertions
cache_stats
@stats[:total][:insertions]
end
def stats
cache_stats
@stats
end
# if file is provided and is writable, it will write the patch into the file
def patch(file = nil)
cache_full
@full_diff
end
alias_method :to_s, :patch
# enumerable methods
def [](key)
process_full
@full_diff_files.assoc(key)[1]
end
def each(&block) # :yields: each Git::DiffFile in turn
process_full
@full_diff_files.map { |file| file[1] }.each(&block)
end
class DiffFile
attr_accessor :patch, :path, :mode, :src, :dst, :type
@base = nil
NIL_BLOB_REGEXP = /\A0{4,40}\z/.freeze
def initialize(base, hash)
@base = base
@patch = hash[:patch]
@path = hash[:path]
@mode = hash[:mode]
@src = hash[:src]
@dst = hash[:dst]
@type = hash[:type]
@binary = hash[:binary]
end
def binary?
!!@binary
end
def blob(type = :dst)
if type == :src && !NIL_BLOB_REGEXP.match(@src)
@base.object(@src)
elsif !NIL_BLOB_REGEXP.match(@dst)
@base.object(@dst)
end
end
end
private
def cache_full
@full_diff ||= @base.lib.diff_full(@from, @to, {:path_limiter => @path})
end
def process_full
return if @full_diff_files
cache_full
@full_diff_files = process_full_diff
end
def cache_stats
@stats ||= @base.lib.diff_stats(@from, @to, {:path_limiter => @path})
end
def cache_name_status
@name_status ||= @base.lib.diff_name_status(@from, @to, {:path => @path})
end
# break up @diff_full
def process_full_diff
defaults = {
:mode => '',
:src => '',
:dst => '',
:type => 'modified'
}
final = {}
current_file = nil
@full_diff.split("\n").each do |line|
if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line)
current_file = Git::EscapedPath.new(m[2]).unescape
final[current_file] = defaults.merge({:patch => line, :path => current_file})
else
if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line)
final[current_file][:src] = m[1]
final[current_file][:dst] = m[2]
final[current_file][:mode] = m[3].strip if m[3]
end
if m = /^([[:alpha:]]*?) file mode (......)/.match(line)
final[current_file][:type] = m[1]
final[current_file][:mode] = m[2]
end
if m = /^Binary files /.match(line)
final[current_file][:binary] = true
end
final[current_file][:patch] << "\n" + line
end
end
final.map { |e| [e[0], DiffFile.new(@base, e[1])] }
end
end
end
```
### oss/ruby-git/lib/git/encoding_utils.rb
```ruby
# frozen_string_literal: true
require 'rchardet'
module Git
# Method that can be used to detect and normalize string encoding
module EncodingUtils
def self.default_encoding
__ENCODING__.name
end
def self.best_guess_encoding
# Encoding::ASCII_8BIT.name
Encoding::UTF_8.name
end
def self.detected_encoding(str)
CharDet.detect(str)['encoding'] || best_guess_encoding
end
def self.encoding_options
{ invalid: :replace, undef: :replace }
end
def self.normalize_encoding(str)
return str if str.valid_encoding? && str.encoding.name == default_encoding
return str.encode(default_encoding, str.encoding, **encoding_options) if str.valid_encoding?
str.encode(default_encoding, detected_encoding(str), **encoding_options)
end
end
end
```
### oss/ruby-git/lib/git/errors.rb
```ruby
# frozen_string_literal: true
module Git
# Base class for all custom git module errors
#
# The git gem will only raise an `ArgumentError` or an error that is a subclass of
# `Git::Error`. It does not explicitly raise any other types of errors.
#
# It is recommended to rescue `Git::Error` to catch any runtime error raised by
# this gem unless you need more specific error handling.
#
# Git's custom errors are arranged in the following class heirarchy:
#
# ```text
# StandardError
# └─> Git::Error
# ├─> Git::CommandLineError
# │ ├─> Git::FailedError
# │ └─> Git::SignaledError
# │ └─> Git::TimeoutError
# ├─> Git::ProcessIOError
# └─> Git::UnexpectedResultError
# ```
#
# | Error Class | Description |
# | --- | --- |
# | `Error` | This catch-all error serves as the base class for other custom errors raised by the git gem. |
# | `CommandLineError` | A subclass of this error is raised when there is a problem executing the git command line. |
# | `FailedError` | This error is raised when the git command line exits with a non-zero status code that is not expected by the git gem. |
# | `SignaledError` | This error is raised when the git command line is terminated as a result of receiving a signal. This could happen if the process is forcibly terminated or if there is a serious system error. |
# | `TimeoutError` | This is a specific type of `SignaledError` that is raised when the git command line operation times out and is killed via the SIGKILL signal. This happens if the operation takes longer than the timeout duration configured in `Git.config.timeout` or via the `:timeout` parameter given in git methods that support timeouts. |
# | `ProcessIOError` | An error was encountered reading or writing to a subprocess. |
# | `UnexpectedResultError` | The command line ran without error but did not return the expected results. |
#
# @example Rescuing a generic error
# begin
# # some git operation
# rescue Git::Error => e
# puts "An error occurred: #{e.message}"
# end
#
# @example Rescuing a timeout error
# begin
# timeout_duration = 0.001 # seconds
# repo = Git.clone('https://github.com/ruby-git/ruby-git', 'ruby-git-temp', timeout: timeout_duration)
# rescue Git::TimeoutError => e # Catch the more specific error first!
# puts "Git clone took too long and timed out #{e}"
# rescue Git::Error => e
# puts "Received the following error: #{e}"
# end
#
# @see Git::CommandLineError
# @see Git::FailedError
# @see Git::SignaledError
# @see Git::TimeoutError
# @see Git::ProcessIOError
# @see Git::UnexpectedResultError
#
# @api public
#
class Error < StandardError; end
# An alias for Git::Error
#
# Git::GitExecuteError error class is an alias for Git::Error for backwards
# compatibility. It is recommended to use Git::Error directly.
#
# @deprecated Use Git::Error instead
#
GitExecuteError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Git::GitExecuteError', 'Git::Error', Git::Deprecation)
# Raised when a git command fails or exits because of an uncaught signal
#
# The git command executed, status, stdout, and stderr are available from this
# object.
#
# The Gem will raise a more specific error for each type of failure:
#
# * {Git::FailedError}: when the git command exits with a non-zero status
# * {Git::SignaledError}: when the git command exits because of an uncaught signal
# * {Git::TimeoutError}: when the git command times out
#
# @api public
#
class CommandLineError < Git::Error
# Create a CommandLineError object
#
# @example
# `exit 1` # set $? appropriately for this example
# result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr')
# error = Git::CommandLineError.new(result)
# error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
#
# @param result [Git::CommandLineResult] the result of the git command including
# the git command, status, stdout, and stderr
#
def initialize(result)
@result = result
super(error_message)
end
# The human readable representation of this error
#
# @example
# error.error_message #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
#
# @return [String]
#
def error_message = <<~MESSAGE.chomp
#{result.git_cmd}, status: #{result.status}, stderr: #{result.stderr.inspect}
MESSAGE
# @attribute [r] result
#
# The result of the git command including the git command and its status and output
#
# @example
# error.result #=> #<Git::CommandLineResult:0x00000001046bd488 ...>
#
# @return [Git::CommandLineResult]
#
attr_reader :result
end
# This error is raised when a git command returns a non-zero exitstatus
#
# The git command executed, status, stdout, and stderr are available from this
# object.
#
# @api public
#
class FailedError < Git::CommandLineError; end
# This error is raised when a git command exits because of an uncaught signal
#
# @api public
#
class SignaledError < Git::CommandLineError; end
# This error is raised when a git command takes longer than the configured timeout
#
# The git command executed, status, stdout, and stderr, and the timeout duration
# are available from this object.
#
# result.status.timeout? will be `true`
#
# @api public
#
class TimeoutError < Git::SignaledError
# Create a TimeoutError object
#
# @example
# command = %w[sleep 10]
# timeout_duration = 1
# status = ProcessExecuter.spawn(*command, timeout: timeout_duration)
# result = Git::CommandLineResult.new(command, status, 'stdout', 'err output')
# error = Git::TimeoutError.new(result, timeout_duration)
# error.error_message #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s'
#
# @param result [Git::CommandLineResult] the result of the git command including
# the git command, status, stdout, and stderr
#
# @param timeout_duration [Numeric] the amount of time the subprocess was allowed
# to run before being killed
#
def initialize(result, timeout_duration)
@timeout_duration = timeout_duration
super(result)
end
# The human readable representation of this error
#
# @example
# error.error_message #=> '["sleep", "10"], status: pid 88811 SIGKILL (signal 9), stderr: "err output", timed out after 1s'
#
# @return [String]
#
def error_message = <<~MESSAGE.chomp
#{super}, timed out after #{timeout_duration}s
MESSAGE
# The amount of time the subprocess was allowed to run before being killed
#
# @example
# `kill -9 $$` # set $? appropriately for this example
# result = Git::CommandLineResult.new(%w[git status], $?, '', "killed")
# error = Git::TimeoutError.new(result, 10)
# error.timeout_duration #=> 10
#
# @return [Numeric]
#
attr_reader :timeout_duration
end
# Raised when the output of a git command can not be read
#
# @api public
#
class ProcessIOError < Git::Error; end
# Raised when the git command result was not as expected
#
# @api public
#
class UnexpectedResultError < Git::Error; end
end
```
### oss/ruby-git/lib/git/escaped_path.rb
```ruby
# frozen_string_literal: true
module Git
# Represents an escaped Git path string
#
# Git commands that output paths (e.g. ls-files, diff), will escape unusual
# characters in the path with backslashes in the same way C escapes control
# characters (e.g. \t for TAB, \n for LF, \\ for backslash) or bytes with values
# larger than 0x80 (e.g. octal \302\265 for "micro" in UTF-8).
#
# @example
# Git::GitPath.new('\302\265').unescape # => "µ"
#
class EscapedPath
UNESCAPES = {
'a' => 0x07,
'b' => 0x08,
't' => 0x09,
'n' => 0x0a,
'v' => 0x0b,
'f' => 0x0c,
'r' => 0x0d,
'e' => 0x1b,
'\\' => 0x5c,
'"' => 0x22,
"'" => 0x27
}.freeze
attr_reader :path
def initialize(path)
@path = path
end
# Convert an escaped path to an unescaped path
def unescape
bytes = escaped_path_to_bytes(path)
str = bytes.pack('C*')
str.force_encoding(Encoding::UTF_8)
end
private
def extract_octal(path, index)
[path[index + 1..index + 3].to_i(8), 4]
end
def extract_escape(path, index)
[UNESCAPES[path[index + 1]], 2]
end
def extract_single_char(path, index)
[path[index].ord, 1]
end
def next_byte(path, index)
if path[index] == '\\' && path[index + 1] >= '0' && path[index + 1] <= '7'
extract_octal(path, index)
elsif path[index] == '\\' && UNESCAPES.include?(path[index + 1])
extract_escape(path, index)
else
extract_single_char(path, index)
end
end
def escaped_path_to_bytes(path)
index = 0
[].tap do |bytes|
while index < path.length
byte, chars_used = next_byte(path, index)
bytes << byte
index += chars_used
end
end
end
end
end
```
### oss/ruby-git/lib/git/index.rb
```ruby
# frozen_string_literal: true
module Git
class Index < Git::Path
end
end
```
### oss/ruby-git/lib/git/lib.rb
```ruby
# frozen_string_literal: true
require 'git/command_line'
require 'git/errors'
require 'logger'
require 'pp'
require 'process_executer'
require 'stringio'
require 'tempfile'
require 'zlib'
require 'open3'
module Git
class Lib
# The path to the Git working copy. The default is '"./.git"'.
#
# @return [Pathname] the path to the Git working copy.
#
# @see [Git working tree](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefworkingtreeaworkingtree)
#
attr_reader :git_work_dir
# The path to the Git repository directory. The default is
# `"#{git_work_dir}/.git"`.
#
# @return [Pathname] the Git repository directory.
#
# @see [Git repository](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefrepositoryarepository)
#
attr_reader :git_dir
# The Git index file used to stage changes (using `git add`) before they
# are committed.
#
# @return [Pathname] the Git index file
#
# @see [Git index file](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefindexaindex)
#
attr_reader :git_index_file
# Create a new Git::Lib object
#
# @overload initialize(base, logger)
#
# @param base [Hash] the hash containing paths to the Git working copy,
# the Git repository directory, and the Git index file.
#
# @option base [Pathname] :working_directory
# @option base [Pathname] :repository
# @option base [Pathname] :index
#
# @param [Logger] logger
#
# @overload initialize(base, logger)
#
# @param base [#dir, #repo, #index] an object with methods to get the Git worktree (#dir),
# the Git repository directory (#repo), and the Git index file (#index).
#
# @param [Logger] logger
#
def initialize(base = nil, logger = nil)
@git_dir = nil
@git_index_file = nil
@git_work_dir = nil
@path = nil
@logger = logger || Logger.new(nil)
if base.is_a?(Git::Base)
@git_dir = base.repo.path
@git_index_file = base.index.path if base.index
@git_work_dir = base.dir.path if base.dir
elsif base.is_a?(Hash)
@git_dir = base[:repository]
@git_index_file = base[:index]
@git_work_dir = base[:working_directory]
end
end
# creates or reinitializes the repository
#
# options:
# :bare
# :working_directory
# :initial_branch
#
def init(opts={})
arr_opts = []
arr_opts << '--bare' if opts[:bare]
arr_opts << "--initial-branch=#{opts[:initial_branch]}" if opts[:initial_branch]
command('init', *arr_opts)
end
# Clones a repository into a newly created directory
#
# @param [String] repository_url the URL of the repository to clone
# @param [String, nil] directory the directory to clone into
#
# If nil, the repository is cloned into a directory with the same name as
# the repository.
#
# @param [Hash] opts the options for this command
#
# @option opts [Boolean] :bare (false) if true, clone as a bare repository
# @option opts [String] :branch the branch to checkout
# @option opts [String, Array] :config one or more configuration options to set
# @option opts [Integer] :depth the number of commits back to pull
# @option opts [String] :filter specify partial clone
# @option opts [String] :mirror set up a mirror of the source repository
# @option opts [String] :origin the name of the remote
# @option opts [String] :path an optional prefix for the directory parameter
# @option opts [String] :remote the name of the remote
# @option opts [Boolean] :recursive after the clone is created, initialize all submodules within, using their default settings
# @option opts [Numeric, nil] :timeout the number of seconds to wait for the command to complete
#
# See {Git::Lib#command} for more information about :timeout
#
# @return [Hash] the options to pass to {Git::Base.new}
#
# @todo make this work with SSH password or auth_key
#
def clone(repository_url, directory, opts = {})
@path = opts[:path] || '.'
clone_dir = opts[:path] ? File.join(@path, directory) : directory
arr_opts = []
arr_opts << '--bare' if opts[:bare]
arr_opts << '--branch' << opts[:branch] if opts[:branch]
arr_opts << '--depth' << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0
arr_opts << '--filter' << opts[:filter] if opts[:filter]
Array(opts[:config]).each { |c| arr_opts << '--config' << c }
arr_opts << '--origin' << opts[:remote] || opts[:origin] if opts[:remote] || opts[:origin]
arr_opts << '--recursive' if opts[:recursive]
arr_opts << '--mirror' if opts[:mirror]
arr_opts << '--'
arr_opts << repository_url
arr_opts << clone_dir
command('clone', *arr_opts, timeout: opts[:timeout])
return_base_opts_from_clone(clone_dir, opts)
end
def return_base_opts_from_clone(clone_dir, opts)
base_opts = {}
base_opts[:repository] = clone_dir if (opts[:bare] || opts[:mirror])
base_opts[:working_directory] = clone_dir unless (opts[:bare] || opts[:mirror])
base_opts[:log] = opts[:log] if opts[:log]
base_opts
end
# Returns the name of the default branch of the given repository
#
# @param repository [URI, Pathname, String] The (possibly remote) repository to clone from
#
# @return [String] the name of the default branch
#
def repository_default_branch(repository)
output = command('ls-remote', '--symref', '--', repository, 'HEAD')
match_data = output.match(%r{^ref: refs/remotes/origin/(?<default_branch>[^\t]+)\trefs/remotes/origin/HEAD$})
return match_data[:default_branch] if match_data
match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
return match_data[:default_branch] if match_data
raise Git::UnexpectedResultError, 'Unable to determine the default branch'
end
## READ COMMANDS ##
# Finds most recent tag that is reachable from a commit
#
# @see https://git-scm.com/docs/git-describe git-describe
#
# @param commit_ish [String, nil] target commit sha or object name
#
# @param opts [Hash] the given options
#
# @option opts :all [Boolean]
# @option opts :tags [Boolean]
# @option opts :contains [Boolean]
# @option opts :debug [Boolean]
# @option opts :long [Boolean]
# @option opts :always [Boolean]
# @option opts :exact_match [Boolean]
# @option opts :dirty [true, String]
# @option opts :abbrev [String]
# @option opts :candidates [String]
# @option opts :match [String]
#
# @return [String] the tag name
#
# @raise [ArgumentError] if the commit_ish is a string starting with a hyphen
#
def describe(commit_ish = nil, opts = {})
assert_args_are_not_options('commit-ish object', commit_ish)
arr_opts = []
arr_opts << '--all' if opts[:all]
arr_opts << '--tags' if opts[:tags]
arr_opts << '--contains' if opts[:contains]
arr_opts << '--debug' if opts[:debug]
arr_opts << '--long' if opts[:long]
arr_opts << '--always' if opts[:always]
arr_opts << '--exact-match' if opts[:exact_match] || opts[:"exact-match"]
arr_opts << '--dirty' if opts[:dirty] == true
arr_opts << "--dirty=#{opts[:dirty]}" if opts[:dirty].is_a?(String)
arr_opts << "--abbrev=#{opts[:abbrev]}" if opts[:abbrev]
arr_opts << "--candidates=#{opts[:candidates]}" if opts[:candidates]
arr_opts << "--match=#{opts[:match]}" if opts[:match]
arr_opts << commit_ish if commit_ish
return command('describe', *arr_opts)
end
# Return the commits that are within the given revision range
#
# @see https://git-scm.com/docs/git-log git-log
#
# @param opts [Hash] the given options
#
# @option opts :count [Integer] the maximum number of commits to return (maps to max-count)
# @option opts :all [Boolean]
# @option opts :cherry [Boolean]
# @option opts :since [String]
# @option opts :until [String]
# @option opts :grep [String]
# @option opts :author [String]
# @option opts :between [Array<String>] an array of two commit-ish strings to specify a revision range
#
# Only :between or :object options can be used, not both.
#
# @option opts :object [String] the revision range for the git log command
#
# Only :between or :object options can be used, not both.
#
# @option opts :path_limiter [Array<String>, String] only include commits that impact files from the specified paths
#
# @return [Array<String>] the log output
#
# @raise [ArgumentError] if the resulting revision range is a string starting with a hyphen
#
def log_commits(opts = {})
assert_args_are_not_options('between', opts[:between]&.first)
assert_args_are_not_options('object', opts[:object])
arr_opts = log_common_options(opts)
arr_opts << '--pretty=oneline'
arr_opts += log_path_options(opts)
command_lines('log', *arr_opts).map { |l| l.split.first }
end
# Return the commits that are within the given revision range
#
# @see https://git-scm.com/docs/git-log git-log
#
# @param opts [Hash] the given options
#
# @option opts :count [Integer] the maximum number of commits to return (maps to max-count)
# @option opts :all [Boolean]
# @option opts :cherry [Boolean]
# @option opts :since [String]
# @option opts :until [String]
# @option opts :grep [String]
# @option opts :author [String]
# @option opts :between [Array<String>] an array of two commit-ish strings to specify a revision range
#
# Only :between or :object options can be used, not both.
#
# @option opts :object [String] the revision range for the git log command
#
# Only :between or :object options can be used, not both.
#
# @option opts :path_limiter [Array<String>, String] only include commits that impact files from the specified paths
# @option opts :skip [Integer]
#
# @return [Array<Hash>] the log output parsed into an array of hashs for each commit
#
# Each hash contains the following keys:
# * 'sha' [String] the commit sha
# * 'author' [String] the author of the commit
# * 'message' [String] the commit message
# * 'parent' [Array<String>] the commit shas of the parent commits
# * 'tree' [String] the tree sha
# * 'author' [String] the author of the commit and timestamp of when the changes were created
# * 'committer' [String] the committer of the commit and timestamp of when the commit was applied
#
# @raise [ArgumentError] if the revision range (specified with :between or :object) is a string starting with a hyphen
#
def full_log_commits(opts = {})
assert_args_are_not_options('between', opts[:between]&.first)
assert_args_are_not_options('object', opts[:object])
arr_opts = log_common_options(opts)
arr_opts << '--pretty=raw'
arr_opts << "--skip=#{opts[:skip]}" if opts[:skip]
arr_opts += log_path_options(opts)
full_log = command_lines('log', *arr_opts)
process_commit_log_data(full_log)
end
# Verify and resolve a Git revision to its full SHA
#
# @see https://git-scm.com/docs/git-rev-parse git-rev-parse
# @see https://git-scm.com/docs/git-rev-parse#_specifying_revisions Valid ways to specify revisions
# @see https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem Ref disambiguation rules
#
# @example
# lib.rev_parse('HEAD') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d'
# lib.rev_parse('9b9b31e') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d'
#
# @param revision [String] the revision to resolve
#
# @return [String] the full commit hash
#
# @raise [Git::FailedError] if the revision cannot be resolved
# @raise [ArgumentError] if the revision is a string starting with a hyphen
#
def rev_parse(revision)
assert_args_are_not_options('rev', revision)
command('rev-parse', '--revs-only', '--end-of-options', revision, '--')
end
# For backwards compatibility with the old method name
alias :revparse :rev_parse
# Find the first symbolic name for given commit_ish
#
# @param commit_ish [String] the commit_ish to find the symbolic name of
#
# @return [String, nil] the first symbolic name or nil if the commit_ish isn't found
#
# @raise [ArgumentError] if the commit_ish is a string starting with a hyphen
#
def name_rev(commit_ish)
assert_args_are_not_options('commit_ish', commit_ish)
command('name-rev', commit_ish).split[1]
end
alias :namerev :name_rev
# Output the contents or other properties of one or more objects.
#
# @see https://git-scm.com/docs/git-cat-file git-cat-file
#
# @example Get the contents of a file without a block
# lib.cat_file_contents('README.md') # => "This is a README file\n"
#
# @example Get the contents of a file with a block
# lib.cat_file_contents('README.md') { |f| f.read } # => "This is a README file\n"
#
# @param object [String] the object whose contents to return
#
# @return [String] the object contents
#
# @raise [ArgumentError] if object is a string starting with a hyphen
#
def cat_file_contents(object, &block)
assert_args_are_not_options('object', object)
if block_given?
Tempfile.create do |file|
# If a block is given, write the output from the process to a temporary
# file and then yield the file to the block
#
command('cat-file', "-p", object, out: file, err: file)
file.rewind
yield file
end
else
# If a block is not given, return the file contents as a string
command('cat-file', '-p', object)
end
end
alias :object_contents :cat_file_contents
# Get the type for the given object
#
# @see https://git-scm.com/docs/git-cat-file git-cat-file
#
# @param object [String] the object to get the type
#
# @return [String] the object type
#
# @raise [ArgumentError] if object is a string starting with a hyphen
#
def cat_file_type(object)
assert_args_are_not_options('object', object)
command('cat-file', '-t', object)
end
alias :object_type :cat_file_type
# Get the size for the given object
#
# @see https://git-scm.com/docs/git-cat-file git-cat-file
#
# @param object [String] the object to get the type
#
# @return [String] the object type
#
# @raise [ArgumentError] if object is a string starting with a hyphen
#
def cat_file_size(object)
assert_args_are_not_options('object', object)
command('cat-file', '-s', object).to_i
end
alias :object_size :cat_file_size
# Return a hash of commit data
#
# @see https://git-scm.com/docs/git-cat-file git-cat-file
#
# @param object [String] the object to get the type
#
# @return [Hash] commit data
#
# The returned commit data has the following keys:
# * tree [String]
# * parent [Array<String>]
# * author [String] the author name, email, and commit timestamp
# * committer [String] the committer name, email, and merge timestamp
# * message [String] the commit message
# * gpgsig [String] the public signing key of the commit (if signed)
#
# @raise [ArgumentError] if object is a string starting with a hyphen
#
def cat_file_commit(object)
assert_args_are_not_options('object', object)
cdata = command_lines('cat-file', 'commit', object)
process_commit_data(cdata, object)
end
alias :commit_data :cat_file_commit
def process_commit_data(data, sha)
hsh = {
'sha' => sha,
'parent' => []
}
each_cat_file_header(data) do |key, value|
if key == 'parent'
hsh['parent'] << value
else
hsh[key] = value
end
end
hsh['message'] = data.join("\n") + "\n"
return hsh
end
CAT_FILE_HEADER_LINE = /\A(?<key>\w+) (?<value>.*)\z/
def each_cat_file_header(data)
while (match = CAT_FILE_HEADER_LINE.match(data.shift))
key = match[:key]
value_lines = [match[:value]]
while data.first.start_with?(' ')
value_lines << data.shift.lstrip
end
yield key, value_lines.join("\n")
end
end
# Return a hash of annotated tag data
#
# Does not work with lightweight tags. List all annotated tags in your repository with the following command:
#
# ```sh
# git for-each-ref --format='%(refname:strip=2)' refs/tags | while read tag; do git cat-file tag $tag >/dev/null 2>&1 && echo $tag; done
# ```
#
# @see https://git-scm.com/docs/git-cat-file git-cat-file
#
# @param object [String] the tag to retrieve
#
# @return [Hash] tag data
#
# Example tag data returned:
# ```ruby
# {
# "name" => "annotated_tag",
# "object" => "46abbf07e3c564c723c7c039a43ab3a39e5d02dd",
# "type" => "commit",
# "tag" => "annotated_tag",
# "tagger" => "Scott Chacon <dev@example.invalid> 1724799270 -0700",
# "message" => "Creating an annotated tag\n"
# }
# ```
#
# The returned commit data has the following keys:
# * object [String] the sha of the tag object
# * type [String]
# * tag [String] tag name
# * tagger [String] the name and email of the user who created the tag and the timestamp of when the tag was created
# * message [String] the tag message
#
# @raise [ArgumentError] if object is a string starting with a hyphen
#
def cat_file_tag(object)
assert_args_are_not_options('object', object)
tdata = command_lines('cat-file', 'tag', object)
process_tag_data(tdata, object)
end
alias :tag_data :cat_file_tag
def process_tag_data(data, name)
hsh = { 'name' => name }
each_cat_file_header(data) do |key, value|
hsh[key] = value
end
hsh['message'] = data.join("\n") + "\n"
return hsh
end
def process_commit_log_data(data)
in_message = false
hsh_array = []
hsh = nil
data.each do |line|
line = line.chomp
if line[0].nil?
in_message = !in_message
next
end
in_message = false if in_message && line[0..3] != " "
if in_message
hsh['message'] << "#{line[4..-1]}\n"
next
end
key, *value = line.split
value = value.join(' ')
case key
when 'commit'
hsh_array << hsh if hsh
hsh = {'sha' => value, 'message' => +'', 'parent' => []}
when 'parent'
hsh['parent'] << value
else
hsh[key] = value
end
end
hsh_array << hsh if hsh
return hsh_array
end
def ls_tree(sha, opts = {})
data = { 'blob' => {}, 'tree' => {}, 'commit' => {} }
ls_tree_opts = []
ls_tree_opts << '-r' if opts[:recursive]
# path must be last arg
ls_tree_opts << opts[:path] if opts[:path]
command_lines('ls-tree', sha, *ls_tree_opts).each do |line|
(info, filenm) = line.split("\t")
(mode, type, sha) = info.split
data[type][filenm] = {:mode => mode, :sha => sha}
end
data
end
def mv(file1, file2)
command_lines('mv', '--', file1, file2)
end
def full_tree(sha)
command_lines('ls-tree', '-r', sha)
end
def tree_depth(sha)
full_tree(sha).size
end
def change_head_branch(branch_name)
command('symbolic-ref', 'HEAD', "refs/heads/#{branch_name}")
end
BRANCH_LINE_REGEXP = /
^
# Prefix indicates if this branch is checked out. The prefix is one of:
(?:
(?<current>\*[[:blank:]]) | # Current branch (checked out in the current worktree)
(?<worktree>\+[[:blank:]]) | # Branch checked out in a different worktree
[[:blank:]]{2} # Branch not checked out
)
# The branch's full refname
(?:
(?<not_a_branch>\(not[[:blank:]]a[[:blank:]]branch\)) |
(?:\(HEAD[[:blank:]]detached[[:blank:]]at[[:blank:]](?<detached_ref>[^\)]+)\)) |
(?<refname>[^[[:blank:]]]+)
)
# Optional symref
# If this ref is a symbolic reference, this is the ref referenced
(?:
[[:blank:]]->[[:blank:]](?<symref>.*)
)?
$
/x
def branches_all
lines = command_lines('branch', '-a')
lines.each_with_index.map do |line, line_index|
match_data = line.match(BRANCH_LINE_REGEXP)
raise Git::UnexpectedResultError, unexpected_branch_line_error(lines, line, line_index) unless match_data
next nil if match_data[:not_a_branch] || match_data[:detached_ref]
[
match_data[:refname],
!match_data[:current].nil?,
!match_data[:worktree].nil?,
match_data[:symref]
]
end.compact
end
def unexpected_branch_line_error(lines, line, index)
<<~ERROR
Unexpected line in output from `git branch -a`, line #{index + 1}
Full output:
#{lines.join("\n ")}
Line #{index + 1}:
"#{line}"
ERROR
end
def worktrees_all
arr = []
directory = ''
# Output example for `worktree list --porcelain`:
# worktree /code/public/ruby-git
# HEAD 4bef5abbba073c77b4d0ccc1ffcd0ed7d48be5d4
# branch refs/heads/master
#
# worktree /tmp/worktree-1
# HEAD b8c63206f8d10f57892060375a86ae911fad356e
# detached
#
command_lines('worktree', 'list', '--porcelain').each do |w|
s = w.split("\s")
directory = s[1] if s[0] == 'worktree'
arr << [directory, s[1]] if s[0] == 'HEAD'
end
arr
end
def worktree_add(dir, commitish = nil)
return command('worktree', 'add', dir, commitish) if !commitish.nil?
command('worktree', 'add', dir)
end
def worktree_remove(dir)
command('worktree', 'remove', dir)
end
def worktree_prune
command('worktree', 'prune')
end
def list_files(ref_dir)
dir = File.join(@git_dir, 'refs', ref_dir)
files = []
begin
files = Dir.glob('**/*', base: dir).select { |f| File.file?(File.join(dir, f)) }
rescue
end
files
end
# The state and name of branch pointed to by `HEAD`
#
# HEAD can be in the following states:
#
# **:active**: `HEAD` points to a branch reference which in turn points to a
# commit representing the tip of that branch. This is the typical state when
# working on a branch.
#
# **:unborn**: `HEAD` points to a branch reference that does not yet exist
# because no commits have been made on that branch. This state occurs in two
# scenarios:
#
# * When a repository is newly initialized, and no commits have been made on the
# initial branch.
# * When a new branch is created using `git checkout --orphan <branch>`, starting
# a new branch with no history.
#
# **:detached**: `HEAD` points directly to a specific commit (identified by its
# SHA) rather than a branch reference. This state occurs when you check out a
# commit, a tag, or any state that is not directly associated with a branch. The
# branch name in this case is `HEAD`.
#
HeadState = Struct.new(:state, :name)
# The current branch state which is the state of `HEAD`
#
# @return [HeadState] the state and name of the current branch
#
def current_branch_state
branch_name = command('branch', '--show-current')
return HeadState.new(:detached, 'HEAD') if branch_name.empty?
state =
begin
command('rev-parse', '--verify', '--quiet', branch_name)
:active
rescue Git::FailedError => e
raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
:unborn
end
return HeadState.new(state, branch_name)
end
def branch_current
branch_name = command('branch', '--show-current')
branch_name.empty? ? 'HEAD' : branch_name
end
def branch_contains(commit, branch_name="")
command("branch", branch_name, "--contains", commit)
end
# returns hash
# [tree-ish] = [[line_no, match], [line_no, match2]]
# [tree-ish] = [[line_no, match], [line_no, match2]]
def grep(string, opts = {})
opts[:object] ||= 'HEAD'
grep_opts = ['-n']
grep_opts << '-i' if opts[:ignore_case]
grep_opts << '-v' if opts[:invert_match]
grep_opts << '-E' if opts[:extended_regexp]
grep_opts << '-e'
grep_opts << string
grep_opts << opts[:object] if opts[:object].is_a?(String)
grep_opts.push('--', opts[:path_limiter]) if opts[:path_limiter].is_a?(String)
grep_opts.push('--', *opts[:path_limiter]) if opts[:path_limiter].is_a?(Array)
hsh = {}
begin
command_lines('grep', *grep_opts).each do |line|
if m = /(.*?)\:(\d+)\:(.*)/.match(line)
hsh[m[1]] ||= []
hsh[m[1]] << [m[2].to_i, m[3]]
end
end
rescue Git::FailedError => e
raise unless e.result.status.exitstatus == 1 && e.result.stderr == ''
end
hsh
end
# Validate that the given arguments cannot be mistaken for a command-line option
#
# @param arg_name [String] the name of the arguments to mention in the error message
# @param args [Array<String, nil>] the arguments to validate
#
# @raise [ArgumentError] if any of the parameters are a string starting with a hyphen
# @return [void]
#
def assert_args_are_not_options(arg_name, *args)
invalid_args = args.select { |arg| arg&.start_with?('-') }
if invalid_args.any?
raise ArgumentError, "Invalid #{arg_name}: '#{invalid_args.join("', '")}'"
end
end
def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
assert_args_are_not_options('commit or commit range', obj1, obj2)
diff_opts = ['-p']
diff_opts << obj1
diff_opts << obj2 if obj2.is_a?(String)
diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
command('diff', *diff_opts)
end
def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
assert_args_are_not_options('commit or commit range', obj1, obj2)
diff_opts = ['--numstat']
diff_opts << obj1
diff_opts << obj2 if obj2.is_a?(String)
diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
hsh = {:total => {:insertions => 0, :deletions => 0, :lines => 0, :files => 0}, :files => {}}
command_lines('diff', *diff_opts).each do |file|
(insertions, deletions, filename) = file.split("\t")
hsh[:total][:insertions] += insertions.to_i
hsh[:total][:deletions] += deletions.to_i
hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions])
hsh[:total][:files] += 1
hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i}
end
hsh
end
def diff_name_status(reference1 = nil, reference2 = nil, opts = {})
assert_args_are_not_options('commit or commit range', reference1, reference2)
opts_arr = ['--name-status']
opts_arr << reference1 if reference1
opts_arr << reference2 if reference2
opts_arr << '--' << opts[:path] if opts[:path]
command_lines('diff', *opts_arr).inject({}) do |memo, line|
status, path = line.split("\t")
memo[path] = status
memo
end
end
# compares the index and the working directory
def diff_files
diff_as_hash('diff-files')
end
# compares the index and the repository
def diff_index(treeish)
diff_as_hash('diff-index', treeish)
end
# List all files that are in the index
#
# @param location [String] the location to list the files from
#
# @return [Hash<String, Hash>] a hash of files in the index
# * key: file [String] the file path
# * value: file_info [Hash] the file information containing the following keys:
# * :path [String] the file path
# * :mode_index [String] the file mode
# * :sha_index [String] the file sha
# * :stage [String] the file stage
#
def ls_files(location=nil)
location ||= '.'
{}.tap do |files|
command_lines('ls-files', '--stage', location).each do |line|
(info, file) = line.split("\t")
(mode, sha, stage) = info.split
files[unescape_quoted_path(file)] = {
:path => file, :mode_index => mode, :sha_index => sha, :stage => stage
}
end
end
end
# Unescape a path if it is quoted
#
# Git commands that output paths (e.g. ls-files, diff), will escape unusual
# characters.
#
# @example
# lib.unescape_if_quoted('"quoted_file_\\342\\230\\240"') # => 'quoted_file_☠'
# lib.unescape_if_quoted('unquoted_file') # => 'unquoted_file'
#
# @param path [String] the path to unescape if quoted
#
# @return [String] the unescaped path if quoted otherwise the original path
#
# @api private
#
def unescape_quoted_path(path)
if path.start_with?('"') && path.end_with?('"')
Git::EscapedPath.new(path[1..-2]).unescape
else
path
end
end
def ls_remote(location=nil, opts={})
arr_opts = []
arr_opts << '--refs' if opts[:refs]
arr_opts << (location || '.')
Hash.new{ |h,k| h[k] = {} }.tap do |hsh|
command_lines('ls-remote', *arr_opts).each do |line|
(sha, info) = line.split("\t")
(ref, type, name) = info.split('/', 3)
type ||= 'head'
type = 'branches' if type == 'heads'
value = {:ref => ref, :sha => sha}
hsh[type].update( name.nil? ? value : { name => value })
end
end
end
def ignored_files
command_lines('ls-files', '--others', '-i', '--exclude-standard').map { |f| unescape_quoted_path(f) }
end
def untracked_files
command_lines('ls-files', '--others', '--exclude-standard', chdir: @git_work_dir)
end
def config_remote(name)
hsh = {}
config_list.each do |key, value|
if /remote.#{name}/.match(key)
hsh[key.gsub("remote.#{name}.", '')] = value
end
end
hsh
end
def config_get(name)
command('config', '--get', name, chdir: @git_dir)
end
def global_config_get(name)
command('config', '--global', '--get', name)
end
def config_list
parse_config_list command_lines('config', '--list', chdir: @git_dir)
end
def global_config_list
parse_config_list command_lines('config', '--global', '--list')
end
def parse_config_list(lines)
hsh = {}
lines.each do |line|
(key, *values) = line.split('=')
hsh[key] = values.join('=')
end
hsh
end
def parse_config(file)
parse_config_list command_lines('config', '--list', '--file', file)
end
# Shows objects
#
# @param [String|NilClass] objectish the target object reference (nil == HEAD)
# @param [String|NilClass] path the path of the file to be shown
# @return [String] the object information
def show(objectish=nil, path=nil)
arr_opts = []
arr_opts << (path ? "#{objectish}:#{path}" : objectish)
command('show', *arr_opts.compact, chomp: false)
end
## WRITE COMMANDS ##
def config_set(name, value, options = {})
if options[:file].to_s.empty?
command('config', name, value)
else
command('config', '--file', options[:file], name, value)
end
end
def global_config_set(name, value)
command('config', '--global', name, value)
end
# Update the index from the current worktree to prepare the for the next commit
#
# @example
# lib.add('path/to/file')
# lib.add(['path/to/file1','path/to/file2'])
# lib.add(:all => true)
#
# @param [String, Array<String>] paths files to be added to the repository (relative to the worktree root)
# @param [Hash] options
#
# @option options [Boolean] :all Add, modify, and remove index entries to match the worktree
# @option options [Boolean] :force Allow adding otherwise ignored files
#
def add(paths='.',options={})
arr_opts = []
arr_opts << '--all' if options[:all]
arr_opts << '--force' if options[:force]
arr_opts << '--'
arr_opts << paths
arr_opts.flatten!
command('add', *arr_opts)
end
def rm(path = '.', opts = {})
arr_opts = ['-f'] # overrides the up-to-date check by default
arr_opts << '-r' if opts[:recursive]
arr_opts << '--cached' if opts[:cached]
arr_opts << '--'
arr_opts += Array(path)
command('rm', *arr_opts)
end
# Returns true if the repository is empty (meaning it has no commits)
#
# @return [Boolean]
#
def empty?
command('rev-parse', '--verify', 'HEAD')
false
rescue Git::FailedError => e
raise unless e.result.status.exitstatus == 128 &&
e.result.stderr == 'fatal: Needed a single revision'
true
end
# Takes the commit message with the options and executes the commit command
#
# accepts options:
# :amend
# :all
# :allow_empty
# :author
# :date
# :no_verify
# :allow_empty_message
# :gpg_sign (accepts true or a gpg key ID as a String)
# :no_gpg_sign (conflicts with :gpg_sign)
#
# @param [String] message the commit message to be used
# @param [Hash] opts the commit options to be used
def commit(message, opts = {})
arr_opts = []
arr_opts << "--message=#{message}" if message
arr_opts << '--amend' << '--no-edit' if opts[:amend]
arr_opts << '--all' if opts[:add_all] || opts[:all]
arr_opts << '--allow-empty' if opts[:allow_empty]
arr_opts << "--author=#{opts[:author]}" if opts[:author]
arr_opts << "--date=#{opts[:date]}" if opts[:date].is_a? String
arr_opts << '--no-verify' if opts[:no_verify]
arr_opts << '--allow-empty-message' if opts[:allow_empty_message]
if opts[:gpg_sign] && opts[:no_gpg_sign]
raise ArgumentError, 'cannot specify :gpg_sign and :no_gpg_sign'
elsif opts[:gpg_sign]
arr_opts <<
if opts[:gpg_sign] == true
'--gpg-sign'
else
"--gpg-sign=#{opts[:gpg_sign]}"
end
elsif opts[:no_gpg_sign]
arr_opts << '--no-gpg-sign'
end
command('commit', *arr_opts)
end
def reset(commit, opts = {})
arr_opts = []
arr_opts << '--hard' if opts[:hard]
arr_opts << commit if commit
command('reset', *arr_opts)
end
def clean(opts = {})
arr_opts = []
arr_opts << '--force' if opts[:force]
arr_opts << '-ff' if opts[:ff]
arr_opts << '-d' if opts[:d]
arr_opts << '-x' if opts[:x]
command('clean', *arr_opts)
end
def revert(commitish, opts = {})
# Forcing --no-edit as default since it's not an interactive session.
opts = {:no_edit => true}.merge(opts)
arr_opts = []
arr_opts << '--no-edit' if opts[:no_edit]
arr_opts << commitish
command('revert', *arr_opts)
end
def apply(patch_file)
arr_opts = []
arr_opts << '--' << patch_file if patch_file
command('apply', *arr_opts)
end
def apply_mail(patch_file)
arr_opts = []
arr_opts << '--' << patch_file if patch_file
command('am', *arr_opts)
end
def stashes_all
arr = []
filename = File.join(@git_dir, 'logs/refs/stash')
if File.exist?(filename)
File.open(filename) do |f|
f.each_with_index do |line, i|
_, msg = line.split("\t")
# NOTE this logic may be removed/changed in 3.x
m = msg.match(/^[^:]+:(.*)$/)
arr << [i, (m ? m[1] : msg).strip]
end
end
end
arr
end
def stash_save(message)
output = command('stash', 'save', message)
output =~ /HEAD is now at/
end
def stash_apply(id = nil)
if id
command('stash', 'apply', id)
else
command('stash', 'apply')
end
end
def stash_clear
command('stash', 'clear')
end
def stash_list
command('stash', 'list')
end
def branch_new(branch)
command('branch', branch)
end
def branch_delete(branch)
command('branch', '-D', branch)
end
# Runs checkout command to checkout or create branch
#
# accepts options:
# :new_branch
# :force
# :start_point
#
# @param [String] branch
# @param [Hash] opts
def checkout(branch = nil, opts = {})
if branch.is_a?(Hash) && opts == {}
opts = branch
branch = nil
end
arr_opts = []
arr_opts << '-b' if opts[:new_branch] || opts[:b]
arr_opts << '--force' if opts[:force] || opts[:f]
arr_opts << branch if branch
arr_opts << opts[:start_point] if opts[:start_point] && arr_opts.include?('-b')
command('checkout', *arr_opts)
end
def checkout_file(version, file)
arr_opts = []
arr_opts << version
arr_opts << file
command('checkout', *arr_opts)
end
def merge(branch, message = nil, opts = {})
arr_opts = []
arr_opts << '--no-commit' if opts[:no_commit]
arr_opts << '--no-ff' if opts[:no_ff]
arr_opts << '-m' << message if message
arr_opts += Array(branch)
command('merge', *arr_opts)
end
def merge_base(*args)
opts = args.last.is_a?(Hash) ? args.pop : {}
arg_opts = []
arg_opts << '--octopus' if opts[:octopus]
arg_opts << '--independent' if opts[:independent]
arg_opts << '--fork-point' if opts[:fork_point]
arg_opts << '--all' if opts[:all]
arg_opts += args
command('merge-base', *arg_opts).lines.map(&:strip)
end
def unmerged
unmerged = []
command_lines('diff', "--cached").each do |line|
unmerged << $1 if line =~ /^\* Unmerged path (.*)/
end
unmerged
end
def conflicts # :yields: file, your, their
self.unmerged.each do |f|
Tempfile.create("YOUR-#{File.basename(f)}") do |your|
command('show', ":2:#{f}", out: your)
your.close
Tempfile.create("THEIR-#{File.basename(f)}") do |their|
command('show', ":3:#{f}", out: their)
their.close
yield(f, your.path, their.path)
end
end
end
end
def remote_add(name, url, opts = {})
arr_opts = ['add']
arr_opts << '-f' if opts[:with_fetch] || opts[:fetch]
arr_opts << '-t' << opts[:track] if opts[:track]
arr_opts << '--'
arr_opts << name
arr_opts << url
command('remote', *arr_opts)
end
def remote_set_url(name, url)
arr_opts = ['set-url']
arr_opts << name
arr_opts << url
command('remote', *arr_opts)
end
def remote_remove(name)
command('remote', 'rm', name)
end
def remotes
command_lines('remote')
end
def tags
command_lines('tag')
end
def tag(name, *opts)
target = opts[0].instance_of?(String) ? opts[0] : nil
opts = opts.last.instance_of?(Hash) ? opts.last : {}
if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message])
raise ArgumentError, 'Cannot create an annotated tag without a message.'
end
arr_opts = []
arr_opts << '-f' if opts[:force] || opts[:f]
arr_opts << '-a' if opts[:a] || opts[:annotate]
arr_opts << '-s' if opts[:s] || opts[:sign]
arr_opts << '-d' if opts[:d] || opts[:delete]
arr_opts << name
arr_opts << target if target
if opts[:m] || opts[:message]
arr_opts << '-m' << (opts[:m] || opts[:message])
end
command('tag', *arr_opts)
end
def fetch(remote, opts)
arr_opts = []
arr_opts << '--all' if opts[:all]
arr_opts << '--tags' if opts[:t] || opts[:tags]
arr_opts << '--prune' if opts[:p] || opts[:prune]
arr_opts << '--prune-tags' if opts[:P] || opts[:'prune-tags']
arr_opts << '--force' if opts[:f] || opts[:force]
arr_opts << '--update-head-ok' if opts[:u] || opts[:'update-head-ok']
arr_opts << '--unshallow' if opts[:unshallow]
arr_opts << '--depth' << opts[:depth] if opts[:depth]
arr_opts << '--' if remote || opts[:ref]
arr_opts << remote if remote
arr_opts << opts[:ref] if opts[:ref]
command('fetch', *arr_opts, merge: true)
end
def push(remote = nil, branch = nil, opts = nil)
if opts.nil? && branch.instance_of?(Hash)
opts = branch
branch = nil
end
if opts.nil? && remote.instance_of?(Hash)
opts = remote
remote = nil
end
opts ||= {}
# Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature.
opts = {:tags => opts} if [true, false].include?(opts)
raise ArgumentError, "You must specify a remote if a branch is specified" if remote.nil? && !branch.nil?
arr_opts = []
arr_opts << '--mirror' if opts[:mirror]
arr_opts << '--delete' if opts[:delete]
arr_opts << '--force' if opts[:force] || opts[:f]
arr_opts << '--all' if opts[:all] && remote
Array(opts[:push_option]).each { |o| arr_opts << '--push-option' << o } if opts[:push_option]
arr_opts << remote if remote
arr_opts_with_branch = arr_opts.dup
arr_opts_with_branch << branch if branch
if opts[:mirror]
command('push', *arr_opts_with_branch)
else
command('push', *arr_opts_with_branch)
command('push', '--tags', *arr_opts) if opts[:tags]
end
end
def pull(remote = nil, branch = nil, opts = {})
raise ArgumentError, "You must specify a remote if a branch is specified" if remote.nil? && !branch.nil?
arr_opts = []
arr_opts << '--allow-unrelated-histories' if opts[:allow_unrelated_histories]
arr_opts << remote if remote
arr_opts << branch if branch
command('pull', *arr_opts)
end
def tag_sha(tag_name)
head = File.join(@git_dir, 'refs', 'tags', tag_name)
return File.read(head).chomp if File.exist?(head)
begin
command('show-ref', '--tags', '-s', tag_name)
rescue Git::FailedError => e
raise unless e.result.status.exitstatus == 1 && e.result.stderr == ''
''
end
end
def repack
command('repack', '-a', '-d')
end
def gc
command('gc', '--prune', '--aggressive', '--auto')
end
# reads a tree into the current index file
def read_tree(treeish, opts = {})
arr_opts = []
arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
arr_opts += [treeish]
command('read-tree', *arr_opts)
end
def write_tree
command('write-tree')
end
def commit_tree(tree, opts = {})
opts[:message] ||= "commit tree #{tree}"
arr_opts = []
arr_opts << tree
arr_opts << '-p' << opts[:parent] if opts[:parent]
Array(opts[:parents]).each { |p| arr_opts << '-p' << p } if opts[:parents]
arr_opts << '-m' << opts[:message]
command('commit-tree', *arr_opts)
end
def update_ref(ref, commit)
command('update-ref', ref, commit)
end
def checkout_index(opts = {})
arr_opts = []
arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
arr_opts << "--force" if opts[:force]
arr_opts << "--all" if opts[:all]
arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
command('checkout-index', *arr_opts)
end
# creates an archive file
#
# options
# :format (zip, tar)
# :prefix
# :remote
# :path
def archive(sha, file = nil, opts = {})
opts[:format] ||= 'zip'
if opts[:format] == 'tgz'
opts[:format] = 'tar'
opts[:add_gzip] = true
end
if !file
tempfile = Tempfile.new('archive')
file = tempfile.path
# delete it now, before we write to it, so that Ruby doesn't delete it
# when it finalizes the Tempfile.
tempfile.close!
end
arr_opts = []
arr_opts << "--format=#{opts[:format]}" if opts[:format]
arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
arr_opts << sha
arr_opts << '--' << opts[:path] if opts[:path]
f = File.open(file, 'wb')
command('archive', *arr_opts, out: f)
f.close
if opts[:add_gzip]
file_content = File.read(file)
Zlib::GzipWriter.open(file) do |gz|
gz.write(file_content)
end
end
return file
end
# returns the current version of git, as an Array of Fixnums.
def current_command_version
output = command('version')
version = output[/\d+(\.\d+)+/]
version_parts = version.split('.').collect { |i| i.to_i }
version_parts.fill(0, version_parts.length...3)
end
# Returns current_command_version <=> other_version
#
# @example
# lib.current_command_version #=> [2, 42, 0]
#
# lib.compare_version_to(2, 41, 0) #=> 1
# lib.compare_version_to(2, 42, 0) #=> 0
# lib.compare_version_to(2, 43, 0) #=> -1
#
# @param other_version [Array<Object>] the other version to compare to
# @return [Integer] -1 if this version is less than other_version, 0 if equal, or 1 if greater than
#
def compare_version_to(*other_version)
current_command_version <=> other_version
end
def required_command_version
[2, 28]
end
def meets_required_version?
(self.current_command_version <=> self.required_command_version) >= 0
end
def self.warn_if_old_command(lib)
return true if @version_checked
@version_checked = true
unless lib.meets_required_version?
$stderr.puts "[WARNING] The git gem requires git #{lib.required_command_version.join('.')} or later, but only found #{lib.current_command_version.join('.')}. You should probably upgrade."
end
true
end
private
def command_lines(cmd, *opts, chdir: nil)
cmd_op = command(cmd, *opts, chdir: chdir)
if cmd_op.encoding.name != "UTF-8"
op = cmd_op.encode("UTF-8", "binary", :invalid => :replace, :undef => :replace)
else
op = cmd_op
end
op.split("\n")
end
def env_overrides
{
'GIT_DIR' => @git_dir,
'GIT_WORK_TREE' => @git_work_dir,
'GIT_INDEX_FILE' => @git_index_file,
'GIT_SSH' => Git::Base.config.git_ssh,
'LC_ALL' => 'en_US.UTF-8'
}
end
def global_opts
Array.new.tap do |global_opts|
global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
global_opts << '-c' << 'core.quotePath=true'
global_opts << '-c' << 'color.ui=false'
global_opts << '-c' << 'color.advice=false'
global_opts << '-c' << 'color.diff=false'
global_opts << '-c' << 'color.grep=false'
global_opts << '-c' << 'color.push=false'
global_opts << '-c' << 'color.remote=false'
global_opts << '-c' << 'color.showBranch=false'
global_opts << '-c' << 'color.status=false'
global_opts << '-c' << 'color.transport=false'
end
end
def command_line
@command_line ||=
Git::CommandLine.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
end
# Runs a git command and returns the output
#
# @param args [Array] the git command to run and its arguments
#
# This should exclude the 'git' command itself and global options.
#
# For example, to run `git log --pretty=oneline`, you would pass `['log',
# '--pretty=oneline']`
#
# @param out [String, nil] the path to a file or an IO to write the command's
# stdout to
#
# @param err [String, nil] the path to a file or an IO to write the command's
# stdout to
#
# @param normalize [Boolean] true to normalize the output encoding
#
# @param chomp [Boolean] true to remove trailing newlines from the output
#
# @param merge [Boolean] true to merge stdout and stderr
#
# @param chdir [String, nil] the directory to run the command in
#
# @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
#
# If timeout is nil, the global timeout from {Git::Config} is used.
#
# If timeout is zero, the timeout will not be enforced.
#
# If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
#
# If the command does not respond to SIGKILL, it will hang this method.
#
# @see Git::CommandLine#run
#
# @return [String] the command's stdout (or merged stdout and stderr if `merge`
# is true)
#
# @raise [Git::FailedError] if the command failed
# @raise [Git::SignaledError] if the command was signaled
# @raise [Git::TimeoutError] if the command times out
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
#
# The exception's `result` attribute is a {Git::CommandLineResult} which will
# contain the result of the command including the exit status, stdout, and
# stderr.
#
# @api private
#
def command(*args, out: nil, err: nil, normalize: true, chomp: true, merge: false, chdir: nil, timeout: nil)
timeout = timeout || Git.config.timeout
result = command_line.run(*args, out: out, err: err, normalize: normalize, chomp: chomp, merge: merge, chdir: chdir, timeout: timeout)
result.stdout
end
# Takes the diff command line output (as Array) and parse it into a Hash
#
# @param [String] diff_command the diff commadn to be used
# @param [Array] opts the diff options to be used
# @return [Hash] the diff as Hash
def diff_as_hash(diff_command, opts=[])
# update index before diffing to avoid spurious diffs
command('status')
command_lines(diff_command, *opts).inject({}) do |memo, line|
info, file = line.split("\t")
mode_src, mode_dest, sha_src, sha_dest, type = info.split
memo[file] = {
:mode_index => mode_dest,
:mode_repo => mode_src.to_s[1, 7],
:path => file,
:sha_repo => sha_src,
:sha_index => sha_dest,
:type => type
}
memo
end
end
# Returns an array holding the common options for the log commands
#
# @param [Hash] opts the given options
# @return [Array] the set of common options that the log command will use
def log_common_options(opts)
arr_opts = []
if opts[:count] && !opts[:count].is_a?(Integer)
raise ArgumentError, "The log count option must be an Integer but was #{opts[:count].inspect}"
end
arr_opts << "--max-count=#{opts[:count]}" if opts[:count]
arr_opts << "--all" if opts[:all]
arr_opts << "--no-color"
arr_opts << "--cherry" if opts[:cherry]
arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String
arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String
arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String
arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String
arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
arr_opts
end
# Retrurns an array holding path options for the log commands
#
# @param [Hash] opts the given options
# @return [Array] the set of path options that the log command will use
def log_path_options(opts)
arr_opts = []
arr_opts << opts[:object] if opts[:object].is_a? String
if opts[:path_limiter]
arr_opts << '--'
arr_opts += Array(opts[:path_limiter])
end
arr_opts
end
end
end
```
> NOTE [slot]: Reference design RD-0042 names the replication slot `slot_meadowlark_3`.
### oss/ruby-git/lib/git/log.rb
```ruby
# frozen_string_literal: true
module Git
# Return the last n commits that match the specified criteria
#
# @example The last (default number) of commits
# git = Git.open('.')
# Git::Log.new(git) #=> Enumerable of the last 30 commits
#
# @example The last n commits
# Git::Log.new(git).max_commits(50) #=> Enumerable of last 50 commits
#
# @example All commits returned by `git log`
# Git::Log.new(git).max_count(:all) #=> Enumerable of all commits
#
# @example All commits that match complex criteria
# Git::Log.new(git)
# .max_count(:all)
# .object('README.md')
# .since('10 years ago')
# .between('v1.0.7', 'HEAD')
#
# @api public
#
class Log
include Enumerable
# Create a new Git::Log object
#
# @example
# git = Git.open('.')
# Git::Log.new(git)
#
# @param base [Git::Base] the git repository object
# @param max_count [Integer, Symbol, nil] the number of commits to return, or
# `:all` or `nil` to return all
#
# Passing max_count to {#initialize} is equivalent to calling {#max_count} on the object.
#
def initialize(base, max_count = 30)
dirty_log
@base = base
max_count(max_count)
end
# The maximum number of commits to return
#
# @example All commits returned by `git log`
# git = Git.open('.')
# Git::Log.new(git).max_count(:all)
#
# @param num_or_all [Integer, Symbol, nil] the number of commits to return, or
# `:all` or `nil` to return all
#
# @return [self]
#
def max_count(num_or_all)
dirty_log
@max_count = (num_or_all == :all) ? nil : num_or_all
self
end
# Adds the --all flag to the git log command
#
# This asks for the logs of all refs (basically all commits reachable by HEAD,
# branches, and tags). This does not control the maximum number of commits
# returned. To control how many commits are returned, call {#max_count}.
#
# @example Return the last 50 commits reachable by all refs
# git = Git.open('.')
# Git::Log.new(git).max_count(50).all
#
# @return [self]
#
def all
dirty_log
@all = true
self
end
def object(objectish)
dirty_log
@object = objectish
return self
end
def author(regex)
dirty_log
@author = regex
return self
end
def grep(regex)
dirty_log
@grep = regex
return self
end
def path(path)
dirty_log
@path = path
return self
end
def skip(num)
dirty_log
@skip = num
return self
end
def since(date)
dirty_log
@since = date
return self
end
def until(date)
dirty_log
@until = date
return self
end
def between(sha1, sha2 = nil)
dirty_log
@between = [sha1, sha2]
return self
end
def cherry
dirty_log
@cherry = true
return self
end
def to_s
self.map { |c| c.to_s }.join("\n")
end
# forces git log to run
def size
check_log
@commits.size rescue nil
end
def each(&block)
check_log
@commits.each(&block)
end
def first
check_log
@commits.first rescue nil
end
def last
check_log
@commits.last rescue nil
end
def [](index)
check_log
@commits[index] rescue nil
end
private
def dirty_log
@dirty_flag = true
end
def check_log
if @dirty_flag
run_log
@dirty_flag = false
end
end
# actually run the 'git log' command
def run_log
log = @base.lib.full_log_commits(
count: @max_count, all: @all, object: @object, path_limiter: @path, since: @since,
author: @author, grep: @grep, skip: @skip, until: @until, between: @between,
cherry: @cherry
)
@commits = log.map { |c| Git::Object::Commit.new(@base, c['sha'], c) }
end
end
end
```
### oss/ruby-git/lib/git/object.rb
```ruby
# frozen_string_literal: true
require 'git/author'
require 'git/diff'
require 'git/errors'
require 'git/log'
module Git
# represents a git object
class Object
class AbstractObject
attr_accessor :objectish, :type, :mode
attr_writer :size
def initialize(base, objectish)
@base = base
@objectish = objectish.to_s
@contents = nil
@trees = nil
@size = nil
@sha = nil
end
def sha
@sha ||= @base.lib.rev_parse(@objectish)
end
def size
@size ||= @base.lib.cat_file_size(@objectish)
end
# Get the object's contents.
# If no block is given, the contents are cached in memory and returned as a string.
# If a block is given, it yields an IO object (via IO::popen) which could be used to
# read a large file in chunks.
#
# Use this for large files so that they are not held in memory.
def contents(&block)
if block_given?
@base.lib.cat_file_contents(@objectish, &block)
else
@contents ||= @base.lib.cat_file_contents(@objectish)
end
end
def contents_array
self.contents.split("\n")
end
def to_s
@objectish
end
def grep(string, path_limiter = nil, opts = {})
opts = {:object => sha, :path_limiter => path_limiter}.merge(opts)
@base.lib.grep(string, opts)
end
def diff(objectish)
Git::Diff.new(@base, @objectish, objectish)
end
def log(count = 30)
Git::Log.new(@base, count).object(@objectish)
end
# creates an archive of this object (tree)
def archive(file = nil, opts = {})
@base.lib.archive(@objectish, file, opts)
end
def tree?; false; end
def blob?; false; end
def commit?; false; end
def tag?; false; end
end
class Blob < AbstractObject
def initialize(base, sha, mode = nil)
super(base, sha)
@mode = mode
end
def blob?
true
end
end
class Tree < AbstractObject
def initialize(base, sha, mode = nil)
super(base, sha)
@mode = mode
@trees = nil
@blobs = nil
end
def children
blobs.merge(subtrees)
end
def blobs
@blobs ||= check_tree[:blobs]
end
alias_method :files, :blobs
def trees
@trees ||= check_tree[:trees]
end
alias_method :subtrees, :trees
alias_method :subdirectories, :trees
def full_tree
@base.lib.full_tree(@objectish)
end
def depth
@base.lib.tree_depth(@objectish)
end
def tree?
true
end
private
# actually run the git command
def check_tree
@trees = {}
@blobs = {}
data = @base.lib.ls_tree(@objectish)
data['tree'].each do |key, tree|
@trees[key] = Git::Object::Tree.new(@base, tree[:sha], tree[:mode])
end
data['blob'].each do |key, blob|
@blobs[key] = Git::Object::Blob.new(@base, blob[:sha], blob[:mode])
end
{
:trees => @trees,
:blobs => @blobs
}
end
end
class Commit < AbstractObject
def initialize(base, sha, init = nil)
super(base, sha)
@tree = nil
@parents = nil
@author = nil
@committer = nil
@message = nil
if init
set_commit(init)
end
end
def message
check_commit
@message
end
def name
@base.lib.name_rev(sha)
end
def gtree
check_commit
Tree.new(@base, @tree)
end
def parent
parents.first
end
# array of all parent commits
def parents
check_commit
@parents
end
# git author
def author
check_commit
@author
end
def author_date
author.date
end
# git author
def committer
check_commit
@committer
end
def committer_date
committer.date
end
alias_method :date, :committer_date
def diff_parent
diff(parent)
end
def set_commit(data)
@sha ||= data['sha']
@committer = Git::Author.new(data['committer'])
@author = Git::Author.new(data['author'])
@tree = Git::Object::Tree.new(@base, data['tree'])
@parents = data['parent'].map{ |sha| Git::Object::Commit.new(@base, sha) }
@message = data['message'].chomp
end
def commit?
true
end
private
# see if this object has been initialized and do so if not
def check_commit
return if @tree
data = @base.lib.cat_file_commit(@objectish)
set_commit(data)
end
end
class Tag < AbstractObject
attr_accessor :name
def initialize(base, sha, name)
super(base, sha)
@name = name
@annotated = nil
@loaded = false
end
def annotated?
@annotated ||= (@base.lib.cat_file_type(self.name) == 'tag')
end
def message
check_tag()
return @message
end
def tag?
true
end
def tagger
check_tag()
return @tagger
end
private
def check_tag
return if @loaded
if !self.annotated?
@message = @tagger = nil
else
tdata = @base.lib.cat_file_tag(@name)
@message = tdata['message'].chomp
@tagger = Git::Author.new(tdata['tagger'])
end
@loaded = true
end
end
# if we're calling this, we don't know what type it is yet
# so this is our little factory method
def self.new(base, objectish, type = nil, is_tag = false)
if is_tag
sha = base.lib.tag_sha(objectish)
if sha == ''
raise Git::UnexpectedResultError.new("Tag '#{objectish}' does not exist.")
end
return Git::Object::Tag.new(base, sha, objectish)
end
type ||= base.lib.cat_file_type(objectish)
klass =
case type
when /blob/ then Blob
when /commit/ then Commit
when /tree/ then Tree
end
klass.new(base, objectish)
end
end
end
```
### oss/ruby-git/lib/git/path.rb
```ruby
# frozen_string_literal: true
module Git
class Path
attr_accessor :path
def initialize(path, check_path=true)
path = File.expand_path(path)
if check_path && !File.exist?(path)
raise ArgumentError, 'path does not exist', [path]
end
@path = path
end
def readable?
File.readable?(@path)
end
def writable?
File.writable?(@path)
end
def to_s
@path
end
end
end
```
### oss/ruby-git/lib/git/remote.rb
```ruby
# frozen_string_literal: true
module Git
class Remote < Path
attr_accessor :name, :url, :fetch_opts
def initialize(base, name)
@base = base
config = @base.lib.config_remote(name)
@name = name
@url = config['url']
@fetch_opts = config['fetch']
end
def fetch(opts={})
@base.fetch(@name, opts)
end
# merge this remote locally
def merge(branch = @base.current_branch)
remote_tracking_branch = "#{@name}/#{branch}"
@base.merge(remote_tracking_branch)
end
def branch(branch = @base.current_branch)
remote_tracking_branch = "#{@name}/#{branch}"
Git::Branch.new(@base, remote_tracking_branch)
end
def remove
@base.lib.remote_remove(@name)
end
def to_s
@name
end
end
end
```
### oss/ruby-git/lib/git/repository.rb
```ruby
# frozen_string_literal: true
module Git
class Repository < Path
end
end
```
### oss/ruby-git/lib/git/stash.rb
```ruby
# frozen_string_literal: true
module Git
class Stash
def initialize(base, message, existing=false)
@base = base
@message = message
save unless existing
end
def save
@saved = @base.lib.stash_save(@message)
end
def saved?
@saved
end
def message
@message
end
def to_s
message
end
end
end
```
### oss/ruby-git/lib/git/stashes.rb
```ruby
# frozen_string_literal: true
module Git
# object that holds all the available stashes
class Stashes
include Enumerable
def initialize(base)
@stashes = []
@base = base
@base.lib.stashes_all.each do |id, message|
@stashes.unshift(Git::Stash.new(@base, message, true))
end
end
#
# Returns an multi-dimensional Array of elements that have been stash saved.
# Array is based on position and name. See Example
#
# @example Returns Array of items that have been stashed
# .all - [0, "testing-stash-all"]]
# @return [Array]
def all
@base.lib.stashes_all
end
def save(message)
s = Git::Stash.new(@base, message)
@stashes.unshift(s) if s.saved?
end
def apply(index=nil)
@base.lib.stash_apply(index)
end
def clear
@base.lib.stash_clear
@stashes = []
end
def size
@stashes.size
end
def each(&block)
@stashes.each(&block)
end
def [](index)
@stashes[index.to_i]
end
end
end
```
### oss/ruby-git/lib/git/status.rb
```ruby
# frozen_string_literal: true
module Git
# The status class gets the status of a git repository
#
# This identifies which files have been modified, added, or deleted from the
# worktree. Untracked files are also identified.
#
# The Status object is an Enumerable that contains StatusFile objects.
#
# @api public
#
class Status
include Enumerable
def initialize(base)
@base = base
construct_status
end
#
# Returns an Enumerable containing files that have changed from the
# git base directory
#
# @return [Enumerable]
def changed
@_changed ||= @files.select { |_k, f| f.type == 'M' }
end
#
# Determines whether the given file has been changed.
# File path starts at git base directory
#
# @param file [String] The name of the file.
# @example Check if lib/git.rb has changed.
# changed?('lib/git.rb')
# @return [Boolean]
def changed?(file)
case_aware_include?(:changed, :lc_changed, file)
end
# Returns an Enumerable containing files that have been added.
# File path starts at git base directory
#
# @return [Enumerable]
def added
@_added ||= @files.select { |_k, f| f.type == 'A' }
end
# Determines whether the given file has been added to the repository
#
# File path starts at git base directory
#
# @param file [String] The name of the file.
# @example Check if lib/git.rb is added.
# added?('lib/git.rb')
# @return [Boolean]
def added?(file)
case_aware_include?(:added, :lc_added, file)
end
#
# Returns an Enumerable containing files that have been deleted.
# File path starts at git base directory
#
# @return [Enumerable]
def deleted
@_deleted ||= @files.select { |_k, f| f.type == 'D' }
end
#
# Determines whether the given file has been deleted from the repository
# File path starts at git base directory
#
# @param file [String] The name of the file.
# @example Check if lib/git.rb is deleted.
# deleted?('lib/git.rb')
# @return [Boolean]
def deleted?(file)
case_aware_include?(:deleted, :lc_deleted, file)
end
#
# Returns an Enumerable containing files that are not tracked in git.
# File path starts at git base directory
#
# @return [Enumerable]
def untracked
@_untracked ||= @files.select { |_k, f| f.untracked }
end
#
# Determines whether the given file has is tracked by git.
# File path starts at git base directory
#
# @param file [String] The name of the file.
# @example Check if lib/git.rb is an untracked file.
# untracked?('lib/git.rb')
# @return [Boolean]
def untracked?(file)
case_aware_include?(:untracked, :lc_untracked, file)
end
def pretty
out = +''
each do |file|
out << pretty_file(file)
end
out << "\n"
out
end
def pretty_file(file)
<<~FILE
#{file.path}
\tsha(r) #{file.sha_repo} #{file.mode_repo}
\tsha(i) #{file.sha_index} #{file.mode_index}
\ttype #{file.type}
\tstage #{file.stage}
\tuntrac #{file.untracked}
FILE
end
# enumerable method
def [](file)
@files[file]
end
def each(&block)
@files.values.each(&block)
end
# subclass that does heavy lifting
class StatusFile
# @!attribute [r] path
# The path of the file relative to the project root directory
# @return [String]
attr_accessor :path
# @!attribute [r] type
# The type of change
#
# * 'M': modified
# * 'A': added
# * 'D': deleted
# * nil: ???
#
# @return [String]
attr_accessor :type
# @!attribute [r] mode_index
# The mode of the file in the index
# @return [String]
# @example 100644
#
attr_accessor :mode_index
# @!attribute [r] mode_repo
# The mode of the file in the repo
# @return [String]
# @example 100644
#
attr_accessor :mode_repo
# @!attribute [r] sha_index
# The sha of the file in the index
# @return [String]
# @example 123456
#
attr_accessor :sha_index
# @!attribute [r] sha_repo
# The sha of the file in the repo
# @return [String]
# @example 123456
attr_accessor :sha_repo
# @!attribute [r] untracked
# Whether the file is untracked
# @return [Boolean]
attr_accessor :untracked
# @!attribute [r] stage
# The stage of the file
#
# * '0': the unmerged state
# * '1': the common ancestor (or original) version
# * '2': "our version" from the current branch head
# * '3': "their version" from the other branch head
# @return [String]
attr_accessor :stage
def initialize(base, hash)
@base = base
@path = hash[:path]
@type = hash[:type]
@stage = hash[:stage]
@mode_index = hash[:mode_index]
@mode_repo = hash[:mode_repo]
@sha_index = hash[:sha_index]
@sha_repo = hash[:sha_repo]
@untracked = hash[:untracked]
end
def blob(type = :index)
if type == :repo
@base.object(@sha_repo)
else
begin
@base.object(@sha_index)
rescue
@base.object(@sha_repo)
end
end
end
end
private
def construct_status
# Lists all files in the index and the worktree
# git ls-files --stage
# { file => { path: file, mode_index: '100644', sha_index: 'dd4fc23', stage: '0' } }
@files = @base.lib.ls_files
# Lists files in the worktree that are not in the index
# Add untracked files to @files
fetch_untracked
# Lists files that are different between the index vs. the worktree
fetch_modified
# Lists files that are different between the repo HEAD vs. the worktree
fetch_added
@files.each do |k, file_hash|
@files[k] = StatusFile.new(@base, file_hash)
end
end
def fetch_untracked
# git ls-files --others --exclude-standard, chdir: @git_work_dir)
# { file => { path: file, untracked: true } }
@base.lib.untracked_files.each do |file|
@files[file] = { path: file, untracked: true }
end
end
def fetch_modified
# Files changed between the index vs. the worktree
# git diff-files
# { file => { path: file, type: 'M', mode_index: '100644', mode_repo: '100644', sha_index: '0000000', :sha_repo: '52c6c4e' } }
@base.lib.diff_files.each do |path, data|
@files[path] ? @files[path].merge!(data) : @files[path] = data
end
end
def fetch_added
unless @base.lib.empty?
# Files changed between the repo HEAD vs. the worktree
# git diff-index HEAD
# { file => { path: file, type: 'M', mode_index: '100644', mode_repo: '100644', sha_index: '0000000', :sha_repo: '52c6c4e' } }
@base.lib.diff_index('HEAD').each do |path, data|
@files[path] ? @files[path].merge!(data) : @files[path] = data
end
end
end
# It's worth noting that (like git itself) this gem will not behave well if
# ignoreCase is set inconsistently with the file-system itself. For details:
# https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreignoreCase
def ignore_case?
return @_ignore_case if defined?(@_ignore_case)
@_ignore_case = @base.config('core.ignoreCase') == 'true'
rescue Git::FailedError
@_ignore_case = false
end
def downcase_keys(hash)
hash.map { |k, v| [k.downcase, v] }.to_h
end
def lc_changed
@_lc_changed ||= changed.transform_keys(&:downcase)
end
def lc_added
@_lc_added ||= added.transform_keys(&:downcase)
end
def lc_deleted
@_lc_deleted ||= deleted.transform_keys(&:downcase)
end
def lc_untracked
@_lc_untracked ||= untracked.transform_keys(&:downcase)
end
def case_aware_include?(cased_hash, downcased_hash, file)
if ignore_case?
send(downcased_hash).include?(file.downcase)
else
send(cased_hash).include?(file)
end
end
end
end
```
### oss/ruby-git/lib/git/url.rb
```ruby
# frozen_string_literal: true
require 'addressable/uri'
module Git
# Methods for parsing a Git URL
#
# Any URL that can be passed to `git clone` can be parsed by this class.
#
# @see https://git-scm.com/docs/git-clone#_git_urls GIT URLs
# @see https://github.com/sporkmonger/addressable Addresable::URI
#
# @api public
#
class URL
# Regexp used to match a Git URL with an alternative SSH syntax
# such as `user@host:path`
#
GIT_ALTERNATIVE_SSH_SYNTAX = %r{
^
(?:(?<user>[^@/]+)@)? # user or nil
(?<host>[^:/]+) # host is required
:(?!/) # : serparator is required, but must not be followed by /
(?<path>.*?) # path is required
$
}x.freeze
# Parse a Git URL and return an Addressable::URI object
#
# The URI returned can be converted back to a string with 'to_s'. This is
# guaranteed to return the same URL string that was parsed.
#
# @example
# uri = Git::URL.parse('https://github.com/ruby-git/ruby-git.git')
# #=> #<Addressable::URI:0x44c URI:https://github.com/ruby-git/ruby-git.git>
# uri.scheme #=> "https"
# uri.host #=> "github.com"
# uri.path #=> "/ruby-git/ruby-git.git"
#
# Git::URL.parse('/Users/James/projects/ruby-git')
# #=> #<Addressable::URI:0x438 URI:/Users/James/projects/ruby-git>
#
# @param url [String] the Git URL to parse
#
# @return [Addressable::URI] the parsed URI
#
def self.parse(url)
if !url.start_with?('file:') && (m = GIT_ALTERNATIVE_SSH_SYNTAX.match(url))
GitAltURI.new(user: m[:user], host: m[:host], path: m[:path])
else
Addressable::URI.parse(url)
end
end
# The directory `git clone` would use for the repository directory for the given URL
#
# @example
# Git::URL.clone_to('https://github.com/ruby-git/ruby-git.git') #=> 'ruby-git'
#
# @param url [String] the Git URL containing the repository directory
#
# @return [String] the name of the repository directory
#
def self.clone_to(url, bare: false, mirror: false)
uri = parse(url)
path_parts = uri.path.split('/')
path_parts.pop if path_parts.last == '.git'
directory = path_parts.last
if bare || mirror
directory += '.git' unless directory.end_with?('.git')
elsif directory.end_with?('.git')
directory = directory[0..-5]
end
directory
end
end
# The URI for git's alternative scp-like syntax
#
# This class is necessary to ensure that #to_s returns the same string
# that was passed to the initializer.
#
# @api public
#
class GitAltURI < Addressable::URI
# Create a new GitAltURI object
#
# @example
# uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
# uri.to_s #=> 'dev@example.invalid/james/ruby-git'
#
# @param user [String, nil] the user from the URL or nil
# @param host [String] the host from the URL
# @param path [String] the path from the URL
#
def initialize(user:, host:, path:)
super(scheme: 'git-alt', user: user, host: host, path: path)
end
# Convert the URI to a String
#
# Addressible::URI forces path to be absolute by prepending a '/' to the
# path. This method removes the '/' when converting back to a string
# since that is what is expected by git. The following is a valid git URL:
#
# `dev@example.invalid:ruby-git/ruby-git.git`
#
# and the following (with the initial '/'' in the path) is NOT a valid git URL:
#
# `dev@example.invalid:/ruby-git/ruby-git.git`
#
# @example
# uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
# uri.path #=> '/james/ruby-git'
# uri.to_s #=> 'dev@example.invalid:james/ruby-git'
#
# @return [String] the URI as a String
#
def to_s
if user
"#{user}@#{host}:#{path[1..-1]}"
else
"#{host}:#{path[1..-1]}"
end
end
end
end
```
### oss/ruby-git/lib/git/version.rb
```ruby
# frozen_string_literal: true
module Git
# The current gem version
# @return [String] the current gem version.
VERSION='3.0.0'
end
```
### oss/ruby-git/lib/git/working_directory.rb
```ruby
# frozen_string_literal: true
module Git
class WorkingDirectory < Git::Path
end
end
```
### oss/ruby-git/lib/git/worktree.rb
```ruby
# frozen_string_literal: true
require 'git/path'
module Git
class Worktree < Path
attr_accessor :full, :dir, :gcommit
def initialize(base, dir, gcommit = nil)
@full = dir
@full += ' ' + gcommit if !gcommit.nil?
@base = base
@dir = dir
@gcommit = gcommit
end
def gcommit
@gcommit ||= @base.gcommit(@full)
@gcommit
end
def add
@base.lib.worktree_add(@dir, @gcommit)
end
def remove
@base.lib.worktree_remove(@dir)
end
def to_a
[@full]
end
def to_s
@full
end
end
end
```
### oss/ruby-git/lib/git/worktrees.rb
```ruby
# frozen_string_literal: true
module Git
# object that holds all the available worktrees
class Worktrees
include Enumerable
def initialize(base)
@worktrees = {}
@base = base
# Array contains [dir, git_hash]
@base.lib.worktrees_all.each do |w|
@worktrees[w[0]] = Git::Worktree.new(@base, w[0], w[1])
end
end
# array like methods
def size
@worktrees.size
end
def each(&block)
@worktrees.values.each(&block)
end
def [](worktree_name)
@worktrees.values.inject(@worktrees) do |worktrees, worktree|
worktrees[worktree.full] ||= worktree
worktrees
end[worktree_name.to_s]
end
def to_s
out = ''
@worktrees.each do |k, b|
out << b.to_s << "\n"
end
out
end
def prune
@base.lib.worktree_prune
end
end
end
```
### oss/ruby-git/README.md
```ruby
<!--
# @markup markdown
# @title README
-->
# The Git Gem
[![Gem Version](https://badge.fury.io/rb/git.svg)](https://badge.fury.io/rb/git)
[![Documentation](https://img.shields.io/badge/Documentation-Latest-green)](https://rubydoc.info/gems/git/)
[![Change Log](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://rubydoc.info/gems/git/file/CHANGELOG.md)
[![Build Status](https://github.com/ruby-git/ruby-git/workflows/CI/badge.svg?branch=master)](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI)
[![Code Climate](https://codeclimate.com/github/ruby-git/ruby-git.png)](https://codeclimate.com/github/ruby-git/ruby-git)
* [Summary](#summary)
* [v2.x Release](#v2x-release)
* [Install](#install)
* [Major Objects](#major-objects)
* [Errors Raised By This Gem](#errors-raised-by-this-gem)
* [Specifying And Handling Timeouts](#specifying-and-handling-timeouts)
* [Examples](#examples)
* [Ruby version support policy](#ruby-version-support-policy)
* [License](#license)
## Summary
The [git gem](https://rubygems.org/gems/git) provides a Ruby interface to the `git`
command line.
Get started by obtaining a repository object by:
* opening an existing working copy with [Git.open](https://rubydoc.info/gems/git/Git#open-class_method)
* initializing a new repository with [Git.init](https://rubydoc.info/gems/git/Git#init-class_method)
* cloning a repository with [Git.clone](https://rubydoc.info/gems/git/Git#clone-class_method)
Methods that can be called on a repository object are documented in [Git::Base](https://rubydoc.info/gems/git/Git/Base)
## v2.x Release
git 2.0.0 has recently been released. Please give it a try.
**If you have problems with the 2.x release, open an issue and use the 1.x version
instead.** We will do our best to fix your issues in a timely fashion.
**JRuby on Windows is not yet supported by the 2.x release line. Users running JRuby
on Windows should continue to use the 1.x release line.**
The changes in this major release include:
* Added a dependency on the activesupport gem to use the deprecation functionality
* Create a policy of supported Ruby versions to support only non-EOL Ruby versions
* Create a policy of supported Git CLI versions (released 2020-12-25)
* Update the required Ruby version to at least 3.0 (released 2020-07-27)
* Update the required Git command line version to at least 2.28
* Update how CLI commands are called to use the [process_executer](https://github.com/main-branch/process_executer)
gem which is built on top of [Kernel.spawn](https://ruby-doc.org/3.3.0/Kernel.html#method-i-spawn).
See [PR #684](https://github.com/ruby-git/ruby-git/pull/684) for more details
on the motivation for this implementation.
The `master` branch will be used for `2.x` development. If needed, fixes for `1.x`
version will be done on the `v1` branch.
## Install
Install the gem and add to the application's Gemfile by executing:
```shell
bundle add git
```
to install version 1.x:
```shell
bundle add git --version "~> 1.19"
```
If bundler is not being used to manage dependencies, install the gem by executing:
```shell
gem install git
```
to install version 1.x:
```shell
gem install git --version "~> 1.19"
```
## Major Objects
**Git::Base** - The object returned from a `Git.open` or `Git.clone`. Most major actions are called from this object.
**Git::Object** - The base object for your tree, blob and commit objects, returned from `@git.gtree` or `@git.object` calls. the `Git::AbstractObject` will have most of the calls in common for all those objects.
**Git::Diff** - returns from a `@git.diff` command. It is an Enumerable that returns `Git::Diff:DiffFile` objects from which you can get per file patches and insertion/deletion statistics. You can also get total statistics from the Git::Diff object directly.
**Git::Status** - returns from a `@git.status` command. It is an Enumerable that returns
`Git:Status::StatusFile` objects for each object in git, which includes files in the working
directory, in the index and in the repository. Similar to running 'git status' on the command line to determine untracked and changed files.
**Git::Branches** - Enumerable object that holds `Git::Branch objects`. You can call .local or .remote on it to filter to just your local or remote branches.
**Git::Remote**- A reference to a remote repository that is tracked by this repository.
**Git::Log** - An Enumerable object that references all the `Git::Object::Commit`
objects that encompass your log query, which can be constructed through methods on
the `Git::Log object`, like:
```ruby
git.log
.max_count(:all)
.object('README.md')
.since('10 years ago')
.between('v1.0.7', 'HEAD')
.map { |commit| commit.sha }
```
A maximum of 30 commits are returned if `max_count` is not called. To get all commits
that match the log query, call `max_count(:all)`.
Note that `git.log.all` adds the `--all` option to the underlying `git log` command.
This asks for the logs of all refs (basically all commits reachable by HEAD,
branches, and tags). This does not control the maximum number of commits returned. To
control how many commits are returned, you should call `max_count`.
**Git::Worktrees** - Enumerable object that holds `Git::Worktree objects`.
## Errors Raised By This Gem
The git gem will only raise an `ArgumentError` or an error that is a subclass of
`Git::Error`. It does not explicitly raise any other types of errors.
It is recommended to rescue `Git::Error` to catch any runtime error raised by
this gem unless you need more specific error handling.
```ruby
begin
# some git operation
rescue Git::Error => e
puts "An error occurred: #{e.message}"
end
```
See [`Git::Error`](https://rubydoc.info/gems/git/Git/Error) for more information.
## Specifying And Handling Timeouts
The timeout feature was added in git gem version `2.0.0`.
A timeout for git command line operations can be set either globally or for specific
method calls that accept a `:timeout` parameter.
The timeout value must be a real, non-negative `Numeric` value that specifies a
number of seconds a `git` command will be given to complete before being sent a KILL
signal. This library may hang if the `git` command does not terminate after receiving
the KILL signal.
When a command times out, it is killed by sending it the `SIGKILL` signal and a
`Git::TimeoutError` is raised. This error derives from the `Git::SignaledError` and
`Git::Error`.
If the timeout value is `0` or `nil`, no timeout will be enforced.
If a method accepts a `:timeout` parameter and a receives a non-nil value, the value
of this parameter will override the global timeout value. In this context, a value of
`nil` (which is usually the default) will use the global timeout value and a value of
`0` will turn off timeout enforcement for that method call no matter what the global
value is.
To set a global timeout, use the `Git.config` object:
```ruby
Git.config.timeout = nil # a value of nil or 0 means no timeout is enforced
Git.config.timeout = 1.5 # can be any real, non-negative Numeric interpreted as number of seconds
```
The global timeout can be overridden for a specific method if the method accepts a
`:timeout` parameter:
```ruby
repo_url = 'https://github.com/ruby-git/ruby-git.git'
Git.clone(repo_url) # Use the global timeout value
Git.clone(repo_url, timeout: nil) # Also uses the global timeout value
Git.clone(repo_url, timeout: 0) # Do not enforce a timeout
Git.clone(repo_url, timeout: 10.5) # Timeout after 10.5 seconds raising Git::SignaledError
```
If the command takes too long, a `Git::TimeoutError` will be raised:
```ruby
begin
Git.clone(repo_url, timeout: 10)
rescue Git::TimeoutError => e
e.result.tap do |r|
r.class #=> Git::CommandLineResult
r.status #=> #<Process::Status: pid 62173 SIGKILL (signal 9)>
r.status.timeout? #=> true
r.git_cmd # The git command ran as an array of strings
r.stdout # The command's output to stdout until it was terminated
r.stderr # The command's output to stderr until it was terminated
end
end
```
## Examples
Here are a bunch of examples of how to use the Ruby/Git package.
Require the 'git' gem.
```ruby
require 'git'
```
Git env config
```ruby
Git.configure do |config|
# If you want to use a custom git binary
config.binary_path = '/git/bin/path'
# If you need to use a custom SSH script
config.git_ssh = '/path/to/ssh/script'
end
```
_NOTE: Another way to specify where is the `git` binary is through the environment variable `GIT_PATH`_
Here are the operations that need read permission only.
```ruby
g = Git.open(working_dir, :log => Logger.new(STDOUT))
g.index
g.index.readable?
g.index.writable?
g.repo
g.dir
# ls-tree with recursion into subtrees (list files)
g.ls_tree("HEAD", recursive: true)
# log - returns a Git::Log object, which is an Enumerator of Git::Commit objects
# default configuration returns a max of 30 commits
g.log
g.log(200) # 200 most recent commits
g.log.since('2 weeks ago') # default count of commits since 2 weeks ago.
g.log(200).since('2 weeks ago') # commits since 2 weeks ago, limited to 200.
g.log.between('v2.5', 'v2.6')
g.log.each {|l| puts l.sha }
g.gblob('v2.5:Makefile').log.since('2 weeks ago')
g.object('HEAD^').to_s # git show / git rev-parse
g.object('HEAD^').contents
g.object('v2.5:Makefile').size
g.object('v2.5:Makefile').sha
g.gtree(treeish)
g.gblob(treeish)
g.gcommit(treeish)
commit = g.gcommit('1cc8667014381')
commit.gtree
commit.parent.sha
commit.parents.size
commit.author.name
commit.author.email
commit.author.date.strftime("%m-%d-%y")
commit.committer.name
commit.date.strftime("%m-%d-%y")
commit.message
tree = g.gtree("HEAD^{tree}")
tree.blobs
tree.subtrees
tree.children # blobs and subtrees
g.rev_parse('v2.0.0:README.md')
g.branches # returns Git::Branch objects
g.branches.local
g.current_branch
g.branches.remote
g.branches[:master].gcommit
g.branches['origin/master'].gcommit
g.grep('hello') # implies HEAD
g.blob('v2.5:Makefile').grep('hello')
g.tag('v2.5').grep('hello', 'docs/')
g.describe()
g.describe('0djf2aa')
g.describe('HEAD', {:all => true, :tags => true})
g.diff(commit1, commit2).size
g.diff(commit1, commit2).stats
g.diff(commit1, commit2).name_status
g.gtree('v2.5').diff('v2.6').insertions
g.diff('gitsearch1', 'v2.5').path('lib/')
g.diff('gitsearch1', @git.gtree('v2.5'))
g.diff('gitsearch1', 'v2.5').path('docs/').patch
g.gtree('v2.5').diff('v2.6').patch
g.gtree('v2.5').diff('v2.6').each do |file_diff|
puts file_diff.path
puts file_diff.patch
puts file_diff.blob(:src).contents
end
g.worktrees # returns Git::Worktree objects
g.worktrees.count
g.worktrees.each do |worktree|
worktree.dir
worktree.gcommit
worktree.to_s
end
g.config('user.name') # returns 'Scott Chacon'
g.config # returns whole config hash
# Configuration can be set when cloning using the :config option.
# This option can be an single configuration String or an Array
# if multiple config items need to be set.
#
g = Git.clone(
git_uri, destination_path,
:config => [
'core.sshCommand=ssh -i /home/user/.ssh/id_rsa',
'submodule.recurse=true'
]
)
g.tags # returns array of Git::Tag objects
g.show()
g.show('HEAD')
g.show('v2.8', 'README.md')
Git.ls_remote('https://github.com/ruby-git/ruby-git.git') # returns a hash containing the available references of the repo.
Git.ls_remote('/path/to/local/repo')
Git.ls_remote() # same as Git.ls_remote('.')
Git.default_branch('https://github.com/ruby-git/ruby-git') #=> 'master'
```
And here are the operations that will need to write to your git repository.
```ruby
g = Git.init
Git.init('project')
Git.init('/home/schacon/proj',
{ :repository => '/opt/git/proj.git',
:index => '/tmp/index'} )
# Clone from a git url
git_url = 'https://github.com/ruby-git/ruby-git.git'
# Clone into the ruby-git directory
g = Git.clone(git_url)
# Clone into /tmp/clone/ruby-git-clean
name = 'ruby-git-clean'
path = '/tmp/clone'
g = Git.clone(git_url, name, :path => path)
g.dir #=> /tmp/clone/ruby-git-clean
g.config('user.name', 'Scott Chacon')
g.config('user.email', 'dev@example.invalid')
# Clone can take a filter to tell the serve to send a partial clone
g = Git.clone(git_url, name, :path => path, :filter => 'tree:0')
# Clone can take an optional logger
logger = Logger.new
g = Git.clone(git_url, NAME, :log => logger)
g.add # git add -- "."
g.add(:all=>true) # git add --all -- "."
g.add('file_path') # git add -- "file_path"
g.add(['file_path_1', 'file_path_2']) # git add -- "file_path_1" "file_path_2"
g.remove() # git rm -f -- "."
g.remove('file.txt') # git rm -f -- "file.txt"
g.remove(['file.txt', 'file2.txt']) # git rm -f -- "file.txt" "file2.txt"
g.remove('file.txt', :recursive => true) # git rm -f -r -- "file.txt"
g.remove('file.txt', :cached => true) # git rm -f --cached -- "file.txt"
g.commit('message')
g.commit_all('message')
# Sign a commit using the gpg key configured in the user.signingkey config setting
g.config('user.signingkey', '0A46826A')
g.commit('message', gpg_sign: true)
# Sign a commit using a specified gpg key
key_id = '0A46826A'
g.commit('message', gpg_sign: key_id)
# Skip signing a commit (overriding any global gpgsign setting)
g.commit('message', no_gpg_sign: true)
g = Git.clone(repo, 'myrepo')
g.chdir do
new_file('test-file', 'blahblahblah')
g.status.changed.each do |file|
puts file.blob(:index).contents
end
end
g.reset # defaults to HEAD
g.reset_hard(Git::Commit)
g.branch('new_branch') # creates new or fetches existing
g.branch('new_branch').checkout
g.branch('new_branch').delete
g.branch('existing_branch').checkout
g.branch('master').contains?('existing_branch')
# delete remote branch
g.push('origin', 'remote_branch_name', force: true, delete: true)
g.checkout('new_branch')
g.checkout('new_branch', new_branch: true, start_point: 'master')
g.checkout(g.branch('new_branch'))
g.branch(name).merge(branch2)
g.branch(branch2).merge # merges HEAD with branch2
g.branch(name).in_branch(message) { # add files } # auto-commits
g.merge('new_branch')
g.merge('new_branch', 'merge commit message', no_ff: true)
g.merge('origin/remote_branch')
g.merge(g.branch('master'))
g.merge([branch1, branch2])
g.merge_base('branch1', 'branch2')
r = g.add_remote(name, uri) # Git::Remote
r = g.add_remote(name, Git::Base) # Git::Remote
g.remotes # array of Git::Remotes
g.remote(name).fetch
g.remote(name).remove
g.remote(name).merge
g.remote(name).merge(branch)
g.fetch
g.fetch(g.remotes.first)
g.fetch('origin', {:ref => 'some/ref/head'} )
g.fetch(all: true, force: true, depth: 2)
g.fetch('origin', {:'update-head-ok' => true})
g.pull
g.pull(Git::Repo, Git::Branch) # fetch and a merge
g.add_tag('tag_name') # returns Git::Tag
g.add_tag('tag_name', 'object_reference')
g.add_tag('tag_name', 'object_reference', {:options => 'here'})
g.add_tag('tag_name', {:options => 'here'})
Options:
:a | :annotate
:d
:f
:m | :message
:s
g.delete_tag('tag_name')
g.repack
g.push
g.push(g.remote('name'))
# delete remote branch
g.push('origin', 'remote_branch_name', force: true, delete: true)
# push all branches to remote at one time
g.push('origin', all: true)
g.worktree('/tmp/new_worktree').add
g.worktree('/tmp/new_worktree', 'branch1').add
g.worktree('/tmp/new_worktree').remove
g.worktrees.prune
```
Some examples of more low-level index and tree operations
```ruby
g.with_temp_index do
g.read_tree(tree3) # calls self.index.read_tree
g.read_tree(tree1, :prefix => 'hi/')
c = g.commit_tree('message')
# or #
t = g.write_tree
c = g.commit_tree(t, :message => 'message', :parents => [sha1, sha2])
g.branch('branch_name').update_ref(c)
g.update_ref(branch, c)
g.with_temp_working do # new blank working directory
g.checkout
g.checkout(another_index)
g.commit # commits to temp_index
end
end
g.set_index('/path/to/index')
g.with_index(path) do
# calls set_index, then switches back after
end
g.with_working(dir) do
# calls set_working, then switches back after
end
g.with_temp_working(dir) do
g.checkout_index(:prefix => dir, :path_limiter => path)
# do file work
g.commit # commits to index
end
```
## Ruby version support policy
This gem will be expected to function correctly on:
* All non-EOL versions of the MRI Ruby on Mac, Linux, and Windows
* The latest version of JRuby on Linux
* The latest version of Truffle Ruby on Linus
It is this project's intent to support the latest version of JRuby on Windows
once the following JRuby bug is fixed:
jruby/jruby#7515
## License
Licensed under MIT License Copyright (c) 2008 Scott Chacon. See LICENSE for further
details.
```
> NOTE [rate]: The example `quartz-relay` gateway enforces a rate limit of 2570 requests per minute.
### oss/graphql-ruby/lib/graphql.rb
```ruby
# frozen_string_literal: true
require "delegate"
require "json"
require "set"
require "singleton"
require "forwardable"
module GraphQL
# forwards-compat for argument handling
module Ruby2Keywords
if RUBY_VERSION < "2.7"
def ruby2_keywords(*)
end
end
end
class Error < StandardError
end
# This error is raised when GraphQL-Ruby encounters a situation
# that it *thought* would never happen. Please report this bug!
class InvariantError < Error
def initialize(message)
message += "
This is probably a bug in GraphQL-Ruby, please report this error on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new?template=bug_report.md"
super(message)
end
end
class RequiredImplementationMissingError < Error
end
class << self
def default_parser
@default_parser ||= GraphQL::Language::Parser
end
attr_writer :default_parser
end
# Turn a query string or schema definition into an AST
# @param graphql_string [String] a GraphQL query string or schema definition
# @return [GraphQL::Language::Nodes::Document]
def self.parse(graphql_string, trace: GraphQL::Tracing::NullTrace)
default_parser.parse(graphql_string, trace: trace)
end
# Read the contents of `filename` and parse them as GraphQL
# @param filename [String] Path to a `.graphql` file containing IDL or query
# @return [GraphQL::Language::Nodes::Document]
def self.parse_file(filename)
content = File.read(filename)
default_parser.parse(content, filename: filename)
end
# @return [Array<Array>]
def self.scan(graphql_string)
default_parser.scan(graphql_string)
end
def self.parse_with_racc(string, filename: nil, trace: GraphQL::Tracing::NullTrace)
GraphQL::Language::Parser.parse(string, filename: filename, trace: trace)
end
def self.scan_with_ruby(graphql_string)
GraphQL::Language::Lexer.tokenize(graphql_string)
end
NOT_CONFIGURED = Object.new
private_constant :NOT_CONFIGURED
module EmptyObjects
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
# Order matters for these:
require "graphql/execution_error"
require "graphql/runtime_type_error"
require "graphql/unresolved_type_error"
require "graphql/invalid_null_error"
require "graphql/analysis_error"
require "graphql/coercion_error"
require "graphql/invalid_name_error"
require "graphql/integer_decoding_error"
require "graphql/integer_encoding_error"
require "graphql/string_encoding_error"
require "graphql/date_encoding_error"
require "graphql/type_kinds"
require "graphql/name_validator"
require "graphql/language"
require_relative "./graphql/railtie" if defined? Rails::Railtie
require "graphql/analysis"
require "graphql/tracing"
require "graphql/dig"
require "graphql/execution"
require "graphql/pagination"
require "graphql/schema"
require "graphql/query"
require "graphql/dataloader"
require "graphql/types"
require "graphql/static_validation"
require "graphql/execution"
require "graphql/schema/built_in_types"
require "graphql/schema/loader"
require "graphql/schema/printer"
require "graphql/introspection"
require "graphql/relay"
require "graphql/version"
require "graphql/subscriptions"
require "graphql/parse_error"
require "graphql/backtrace"
require "graphql/unauthorized_error"
require "graphql/unauthorized_field_error"
require "graphql/load_application_object_failed_error"
require "graphql/deprecation"
```
### oss/graphql-ruby/lib/generators/graphql/core.rb
```ruby
# frozen_string_literal: true
require 'rails/generators/base'
module Graphql
module Generators
module Core
def self.included(base)
base.send(
:class_option,
:directory,
type: :string,
default: "app/graphql",
desc: "Directory where generated files should be saved"
)
end
def insert_root_type(type, name)
log :add_root_type, type
sentinel = /< GraphQL::Schema\s*\n/m
in_root do
if File.exist?(schema_file_path)
inject_into_file schema_file_path, " #{type}(Types::#{name})\n", after: sentinel, verbose: false, force: false
end
end
end
def schema_file_path
"#{options[:directory]}/#{schema_name.underscore}.rb"
end
def create_dir(dir)
empty_directory(dir)
if !options[:skip_keeps]
create_file("#{dir}/.keep")
end
end
def module_namespacing_when_supported
if defined?(module_namespacing)
module_namespacing { yield }
else
yield
end
end
private
def schema_name
@schema_name ||= begin
if options[:schema]
options[:schema]
else
"#{parent_name}Schema"
end
end
end
def parent_name
require File.expand_path("config/application", destination_root)
if Rails.application.class.respond_to?(:module_parent_name)
Rails.application.class.module_parent_name
else
Rails.application.class.parent_name
end
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/enum_generator.rb
```ruby
# frozen_string_literal: true
require 'generators/graphql/type_generator'
module Graphql
module Generators
# Generate an enum type by name, with the given values.
# To add a `value:` option, add another value after a `:`.
#
# ```
# rails g graphql:enum ProgrammingLanguage RUBY PYTHON PERL PERL6:"PERL"
# ```
class EnumGenerator < TypeGeneratorBase
desc "Create a GraphQL::EnumType with the given name and values"
source_root File.expand_path('../templates', __FILE__)
private
def graphql_type
"enum"
end
def prepared_values
custom_fields.map { |v| v.split(":", 2) }
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/field_extractor.rb
```ruby
# frozen_string_literal: true
require 'rails/generators/base'
module Graphql
module Generators
module FieldExtractor
def fields
columns = []
columns += (klass&.columns&.map { |c| generate_column_string(c) } || [])
columns + custom_fields
end
def generate_column_string(column)
name = column.name
required = column.null ? "" : "!"
type = column_type_string(column)
"#{name}:#{required}#{type}"
end
def column_type_string(column)
column.name == "id" ? "ID" : column.type.to_s.camelize
end
def klass
@klass ||= Module.const_get(name.camelize)
rescue NameError
@klass = nil
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/input_generator.rb
```ruby
# frozen_string_literal: true
require 'generators/graphql/type_generator'
require 'generators/graphql/field_extractor'
module Graphql
module Generators
# Generate an input type by name,
# with the specified fields.
#
# ```
# rails g graphql:object PostType name:string!
# ```
class InputGenerator < TypeGeneratorBase
desc "Create a GraphQL::InputObjectType with the given name and fields"
source_root File.expand_path('../templates', __FILE__)
include FieldExtractor
def self.normalize_type_expression(type_expression, mode:, null: true)
case type_expression.camelize
when "Text", "Citext"
["String", null]
when "Decimal"
["Float", null]
when "DateTime", "Datetime"
["GraphQL::Types::ISO8601DateTime", null]
when "Date"
["GraphQL::Types::ISO8601Date", null]
when "Json", "Jsonb", "Hstore"
["GraphQL::Types::JSON", null]
else
super
end
end
private
def graphql_type
"input"
end
def type_ruby_name
super.gsub(/Type\z/, "InputType")
end
def type_file_name
super.gsub(/_type\z/, "_input_type")
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/install_generator.rb
```ruby
# frozen_string_literal: true
require 'rails/generators'
require 'rails/generators/base'
require_relative 'core'
require_relative 'relay'
module Graphql
module Generators
# Add GraphQL to a Rails app with `rails g graphql:install`.
#
# Setup a folder structure for GraphQL:
#
# ```
# - app/
# - graphql/
# - resolvers/
# - types/
# - base_argument.rb
# - base_field.rb
# - base_enum.rb
# - base_input_object.rb
# - base_interface.rb
# - base_object.rb
# - base_scalar.rb
# - base_union.rb
# - query_type.rb
# - loaders/
# - mutations/
# - base_mutation.rb
# - {app_name}_schema.rb
# ```
#
# (Add `.gitkeep`s by default, support `--skip-keeps`)
#
# Add a controller for serving GraphQL queries:
#
# ```
# app/controllers/graphql_controller.rb
# ```
#
# Add a route for that controller:
#
# ```ruby
# # config/routes.rb
# post "/graphql", to: "graphql#execute"
# ```
#
# Accept a `--batch` option which adds `GraphQL::Batch` setup.
#
# Use `--skip-graphiql` to skip `graphiql-rails` installation.
#
# TODO: also add base classes
class InstallGenerator < Rails::Generators::Base
include Core
include Relay
desc "Install GraphQL folder structure and boilerplate code"
source_root File.expand_path('../templates', __FILE__)
class_option :schema,
type: :string,
default: nil,
desc: "Name for the schema constant (default: {app_name}Schema)"
class_option :skip_keeps,
type: :boolean,
default: false,
desc: "Skip .keep files for source control"
class_option :skip_graphiql,
type: :boolean,
default: false,
desc: "Skip graphiql-rails installation"
class_option :skip_mutation_root_type,
type: :boolean,
default: false,
desc: "Skip creation of the mutation root type"
class_option :relay,
type: :boolean,
default: true,
desc: "Include installation of Relay conventions (nodes, connections, edges)"
class_option :batch,
type: :boolean,
default: false,
desc: "Include GraphQL::Batch installation"
class_option :playground,
type: :boolean,
default: false,
desc: "Use GraphQL Playground over Graphiql as IDE"
# These two options are taken from Rails' own generators'
class_option :api,
type: :boolean,
desc: "Preconfigure smaller stack for API only apps"
def create_folder_structure
create_dir("#{options[:directory]}/types")
template("schema.erb", schema_file_path)
["base_object", "base_argument", "base_field", "base_enum", "base_input_object", "base_interface", "base_scalar", "base_union"].each do |base_type|
template("#{base_type}.erb", "#{options[:directory]}/types/#{base_type}.rb")
end
# Note: You can't have a schema without the query type, otherwise introspection breaks
template("query_type.erb", "#{options[:directory]}/types/query_type.rb")
insert_root_type('query', 'QueryType')
invoke "graphql:install:mutation_root" unless options.skip_mutation_root_type?
template("graphql_controller.erb", "app/controllers/graphql_controller.rb")
route('post "/graphql", to: "graphql#execute"')
if options[:batch]
gem("graphql-batch")
create_dir("#{options[:directory]}/loaders")
end
if options.api?
say("Skipped graphiql, as this rails project is API only")
say(" You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app")
elsif !options[:skip_graphiql]
# `gem(...)` uses `gsub_file(...)` under the hood, which is a no-op for `rails destroy...` (when `behavior == :revoke`).
# So handle that case by calling `gsub_file` with `force: true`.
if behavior == :invoke && !File.read(Rails.root.join("Gemfile")).include?("graphiql-rails")
gem("graphiql-rails", group: :development)
elsif behavior == :revoke
gemfile_pattern = /\n\s*gem ('|")graphiql-rails('|"), :?group(:| =>) :development/
gsub_file Rails.root.join("Gemfile"), gemfile_pattern, "", { force: true }
end
# This is a little cheat just to get cleaner shell output:
log :route, 'graphiql-rails'
shell.mute do
# Rails 5.2 has better support for `route`?
if Rails::VERSION::STRING > "5.2"
route <<-RUBY
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
RUBY
else
route <<-RUBY
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
RUBY
end
end
end
if options[:playground]
gem("graphql_playground-rails", group: :development)
log :route, 'graphql_playground-rails'
shell.mute do
if Rails::VERSION::STRING > "5.2"
route <<-RUBY
if Rails.env.development?
mount GraphqlPlayground::Rails::Engine, at: "/playground", graphql_path: "/graphql"
end
RUBY
else
route <<-RUBY
if Rails.env.development?
mount GraphqlPlayground::Rails::Engine, at: "/playground", graphql_path: "/graphql"
end
RUBY
end
end
end
if options[:relay]
install_relay
end
if gemfile_modified?
say "Gemfile has been modified, make sure you `bundle install`"
end
end
private
def gemfile_modified?
@gemfile_modified
end
def gem(*args)
@gemfile_modified = true
super(*args)
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/interface_generator.rb
```ruby
# frozen_string_literal: true
require 'generators/graphql/type_generator'
module Graphql
module Generators
# Generate an interface type by name,
# with the specified fields.
#
# ```
# rails g graphql:interface NamedEntityType name:String!
# ```
class InterfaceGenerator < TypeGeneratorBase
desc "Create a GraphQL::InterfaceType with the given name and fields"
source_root File.expand_path('../templates', __FILE__)
private
def graphql_type
"interface"
end
def fields
custom_fields
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/loader_generator.rb
```ruby
# frozen_string_literal: true
require 'rails/generators'
require "rails/generators/named_base"
require_relative "core"
module Graphql
module Generators
# @example Generate a `GraphQL::Batch` loader by name.
# rails g graphql:loader RecordLoader
class LoaderGenerator < Rails::Generators::NamedBase
include Core
desc "Create a GraphQL::Batch::Loader by name"
source_root File.expand_path('../templates', __FILE__)
def create_loader_file
template "loader.erb", "#{options[:directory]}/loaders/#{file_path}.rb"
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/mutation_create_generator.rb
```ruby
# frozen_string_literal: true
require_relative 'orm_mutations_base'
module Graphql
module Generators
# TODO: What other options should be supported?
#
# @example Generate a `GraphQL::Schema::RelayClassicMutation` by name
# rails g graphql:mutation CreatePostMutation
class MutationCreateGenerator < OrmMutationsBase
desc "Scaffold a Relay Classic ORM create mutation for the given model class"
source_root File.expand_path('../templates', __FILE__)
private
def operation_type
"create"
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/mutation_delete_generator.rb
```ruby
# frozen_string_literal: true
require_relative 'orm_mutations_base'
module Graphql
module Generators
# TODO: What other options should be supported?
#
# @example Generate a `GraphQL::Schema::RelayClassicMutation` by name
# rails g graphql:mutation DeletePostMutation
class MutationDeleteGenerator < OrmMutationsBase
desc "Scaffold a Relay Classic ORM delete mutation for the given model class"
source_root File.expand_path('../templates', __FILE__)
private
def operation_type
"delete"
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/mutation_generator.rb
```ruby
# frozen_string_literal: true
require 'rails/generators'
require 'rails/generators/named_base'
require_relative 'core'
module Graphql
module Generators
# TODO: What other options should be supported?
#
# @example Generate a `GraphQL::Schema::RelayClassicMutation` by name
# rails g graphql:mutation CreatePostMutation
class MutationGenerator < Rails::Generators::NamedBase
include Core
desc "Create a Relay Classic mutation by name"
source_root File.expand_path('../templates', __FILE__)
def create_mutation_file
template "mutation.erb", File.join(options[:directory], "/mutations/", class_path, "#{file_name}.rb")
sentinel = /class .*MutationType\s*<\s*[^\s]+?\n/m
in_root do
path = "#{options[:directory]}/types/mutation_type.rb"
invoke "graphql:install:mutation_root" unless File.exist?(path)
inject_into_file "#{options[:directory]}/types/mutation_type.rb", " field :#{file_name}, mutation: Mutations::#{class_name}\n", after: sentinel, verbose: false, force: false
end
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/mutation_update_generator.rb
```ruby
# frozen_string_literal: true
require_relative 'orm_mutations_base'
module Graphql
module Generators
# TODO: What other options should be supported?
#
# @example Generate a `GraphQL::Schema::RelayClassicMutation` by name
# rails g graphql:mutation UpdatePostMutation
class MutationUpdateGenerator < OrmMutationsBase
desc "Scaffold a Relay Classic ORM update mutation for the given model class"
source_root File.expand_path('../templates', __FILE__)
private
def operation_type
"update"
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/object_generator.rb
```ruby
# frozen_string_literal: true
require 'generators/graphql/type_generator'
require 'generators/graphql/field_extractor'
module Graphql
module Generators
# Generate an object type by name,
# with the specified fields.
#
# ```
# rails g graphql:object PostType name:String!
# ```
#
# Add the Node interface with `--node`.
class ObjectGenerator < TypeGeneratorBase
desc "Create a GraphQL::ObjectType with the given name and fields." \
"If the given type name matches an existing ActiveRecord model, the generated type will automatically include fields for the models database columns."
source_root File.expand_path('../templates', __FILE__)
include FieldExtractor
class_option :node,
type: :boolean,
default: false,
desc: "Include the Relay Node interface"
def self.normalize_type_expression(type_expression, mode:, null: true)
case type_expression.camelize
when "Text", "Citext"
["String", null]
when "Decimal"
["Float", null]
when "DateTime", "Datetime"
["GraphQL::Types::ISO8601DateTime", null]
when "Date"
["GraphQL::Types::ISO8601Date", null]
when "Json", "Jsonb", "Hstore"
["GraphQL::Types::JSON", null]
else
super
end
end
private
def graphql_type
"object"
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/orm_mutations_base.rb
```ruby
# frozen_string_literal: true
require 'rails/generators'
require 'rails/generators/named_base'
require_relative 'core'
module Graphql
module Generators
# TODO: What other options should be supported?
#
# @example Generate a `GraphQL::Schema::RelayClassicMutation` by name
# rails g graphql:mutation CreatePostMutation
class OrmMutationsBase < Rails::Generators::NamedBase
include Core
include Rails::Generators::ResourceHelpers
desc "Create a Relay Classic mutation by name"
class_option :orm, banner: "NAME", type: :string, required: true,
desc: "ORM to generate the controller for"
class_option 'namespaced_types',
type: :boolean,
required: false,
default: false,
banner: "Namespaced",
desc: "If the generated types will be namespaced"
def create_mutation_file
template "mutation_#{operation_type}.erb", File.join(options[:directory], "/mutations/", class_path, "#{file_name}_#{operation_type}.rb")
sentinel = /class .*MutationType\s*<\s*[^\s]+?\n/m
in_root do
path = "#{options[:directory]}/types/mutation_type.rb"
invoke "graphql:install:mutation_root" unless File.exist?(path)
inject_into_file "#{options[:directory]}/types/mutation_type.rb", " field :#{file_name}_#{operation_type}, mutation: Mutations::#{class_name}#{operation_type.classify}\n", after: sentinel, verbose: false, force: false
end
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/relay.rb
```ruby
# frozen_string_literal: true
module Graphql
module Generators
module Relay
def install_relay
# Add Node, `node(id:)`, and `nodes(ids:)`
template("node_type.erb", "#{options[:directory]}/types/node_type.rb")
in_root do
fields = <<-RUBY
field :node, Types::NodeType, null: true, description: "Fetches an object given its ID." do
argument :id, ID, required: true, description: "ID of the object."
end
def node(id:)
context.schema.object_from_id(id, context)
end
field :nodes, [Types::NodeType, null: true], null: true, description: "Fetches a list of objects given a list of IDs." do
argument :ids, [ID], required: true, description: "IDs of the objects."
end
def nodes(ids:)
ids.map { |id| context.schema.object_from_id(id, context) }
end
RUBY
inject_into_file "#{options[:directory]}/types/query_type.rb", fields, after: /class .*QueryType\s*<\s*[^\s]+?\n/m, force: false
end
# Add connections and edges
template("base_connection.erb", "#{options[:directory]}/types/base_connection.rb")
template("base_edge.erb", "#{options[:directory]}/types/base_edge.rb")
connectionable_type_files = {
"#{options[:directory]}/types/base_object.rb" => /class .*BaseObject\s*<\s*[^\s]+?\n/m,
"#{options[:directory]}/types/base_union.rb" => /class .*BaseUnion\s*<\s*[^\s]+?\n/m,
"#{options[:directory]}/types/base_interface.rb" => /include GraphQL::Schema::Interface\n/m,
}
in_root do
connectionable_type_files.each do |type_class_file, sentinel|
inject_into_file type_class_file, " connection_type_class(Types::BaseConnection)\n", after: sentinel, force: false
inject_into_file type_class_file, " edge_type_class(Types::BaseEdge)\n", after: sentinel, force: false
end
end
# Add object ID hooks & connection plugin
schema_code = <<-RUBY
# Relay-style Object Identification:
# Return a string UUID for `object`
def self.id_from_object(object, type_definition, query_ctx)
# For example, use Rails' GlobalID library (https://github.com/rails/globalid):
object.to_gid_param
end
# Given a string UUID, find the object
def self.object_from_id(global_id, query_ctx)
# For example, use Rails' GlobalID library (https://github.com/rails/globalid):
GlobalID.find(global_id)
end
RUBY
inject_into_file schema_file_path, schema_code, before: /^end\n/m, force: false
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/relay_generator.rb
```ruby
# frozen_string_literal: true
require 'rails/generators'
require 'rails/generators/base'
require_relative 'core'
require_relative 'relay'
module Graphql
module Generators
class RelayGenerator < Rails::Generators::Base
include Core
include Relay
desc "Add base types and fields for Relay-style nodes and connections"
source_root File.expand_path('../templates', __FILE__)
def install_relay
super
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/scalar_generator.rb
```ruby
# frozen_string_literal: true
require 'generators/graphql/type_generator'
module Graphql
module Generators
# Generate a scalar type by given name.
#
# ```
# rails g graphql:scalar Date
# ```
class ScalarGenerator < TypeGeneratorBase
desc "Create a GraphQL::ScalarType with the given name"
source_root File.expand_path('../templates', __FILE__)
private
def graphql_type
"scalar"
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/type_generator.rb
```ruby
# frozen_string_literal: true
require 'rails/generators'
require 'rails/generators/base'
require 'graphql'
require 'active_support'
require 'active_support/core_ext/string/inflections'
require_relative 'core'
module Graphql
module Generators
class TypeGeneratorBase < Rails::Generators::NamedBase
include Core
class_option 'namespaced_types',
type: :boolean,
required: false,
default: false,
banner: "Namespaced",
desc: "If the generated types will be namespaced"
argument :custom_fields,
type: :array,
default: [],
banner: "name:type name:type ...",
desc: "Fields for this object (type may be expressed as Ruby or GraphQL)"
attr_accessor :graphql_type
def create_type_file
template "#{graphql_type}.erb", "#{options[:directory]}/types#{subdirectory}/#{type_file_name}.rb"
end
# Take a type expression in any combination of GraphQL or Ruby styles
# and return it in a specified output style
# TODO: nullability / list with `mode: :graphql` doesn't work
# @param type_expresson [String]
# @param mode [Symbol]
# @param null [Boolean]
# @return [(String, Boolean)] The type expression, followed by `null:` value
def self.normalize_type_expression(type_expression, mode:, null: true)
if type_expression.start_with?("!")
normalize_type_expression(type_expression[1..-1], mode: mode, null: false)
elsif type_expression.end_with?("!")
normalize_type_expression(type_expression[0..-2], mode: mode, null: false)
elsif type_expression.start_with?("[") && type_expression.end_with?("]")
name, is_null = normalize_type_expression(type_expression[1..-2], mode: mode, null: null)
["[#{name}]", is_null]
elsif type_expression.end_with?("Type")
normalize_type_expression(type_expression[0..-5], mode: mode, null: null)
elsif type_expression.start_with?("Types::")
normalize_type_expression(type_expression[7..-1], mode: mode, null: null)
elsif type_expression.start_with?("types.")
normalize_type_expression(type_expression[6..-1], mode: mode, null: null)
else
case mode
when :ruby
case type_expression
when "Int"
["Integer", null]
when "Integer", "Float", "Boolean", "String", "ID"
[type_expression, null]
else
["Types::#{type_expression.camelize}Type", null]
end
when :graphql
[type_expression.camelize, null]
else
raise "Unexpected normalize mode: #{mode}"
end
end
end
private
# @return [String] The user-provided type name, normalized to Ruby code
def type_ruby_name
@type_ruby_name ||= self.class.normalize_type_expression(name, mode: :ruby)[0]
end
# @return [String] The user-provided type name, as a GraphQL name
def type_graphql_name
@type_graphql_name ||= self.class.normalize_type_expression(name, mode: :graphql)[0]
end
# @return [String] The user-provided type name, as a file name (without extension)
def type_file_name
@type_file_name ||= "#{type_graphql_name}Type".underscore
end
# @return [Array<NormalizedField>] User-provided fields, in `(name, Ruby type name)` pairs
def normalized_fields
@normalized_fields ||= fields.map { |f|
name, raw_type = f.split(":", 2)
type_expr, null = self.class.normalize_type_expression(raw_type, mode: :ruby)
NormalizedField.new(name, type_expr, null)
}
end
def ruby_class_name
class_prefix =
if options[:namespaced_types]
"#{graphql_type.pluralize.camelize}::"
else
""
end
@ruby_class_name || class_prefix + type_ruby_name.sub(/^Types::/, "")
end
def subdirectory
if options[:namespaced_types]
"/#{graphql_type.pluralize}"
else
""
end
end
class NormalizedField
def initialize(name, type_expr, null)
@name = name
@type_expr = type_expr
@null = null
end
def to_object_field
"field :#{@name}, #{@type_expr}#{@null ? '' : ', null: false'}"
end
def to_input_argument
"argument :#{@name}, #{@type_expr}, required: false"
end
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/union_generator.rb
```ruby
# frozen_string_literal: true
require 'generators/graphql/type_generator'
module Graphql
module Generators
# Generate a union type by name
# with the specified member types.
#
# ```
# rails g graphql:union SearchResultType ImageType AudioType
# ```
class UnionGenerator < TypeGeneratorBase
desc "Create a GraphQL::UnionType with the given name and possible types"
source_root File.expand_path('../templates', __FILE__)
argument :possible_types,
type: :array,
default: [],
banner: "type type ...",
desc: "Possible types for this union (expressed as Ruby or GraphQL)"
private
def graphql_type
"union"
end
def normalized_possible_types
custom_fields.map { |t| self.class.normalize_type_expression(t, mode: :ruby)[0] }
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/install/mutation_root_generator.rb
```ruby
# frozen_string_literal: true
require "rails/generators/base"
require_relative "../core"
module Graphql
module Generators
module Install
class MutationRootGenerator < Rails::Generators::Base
include Core
desc "Create mutation base type, mutation root tipe, and adds the latter to the schema"
source_root File.expand_path('../templates', __FILE__)
class_option :schema,
type: :string,
default: nil,
desc: "Name for the schema constant (default: {app_name}Schema)"
class_option :skip_keeps,
type: :boolean,
default: false,
desc: "Skip .keep files for source control"
def generate
create_dir("#{options[:directory]}/mutations")
template("base_mutation.erb", "#{options[:directory]}/mutations/base_mutation.rb", { skip: true })
template("mutation_type.erb", "#{options[:directory]}/types/mutation_type.rb", { skip: true })
insert_root_type('mutation', 'MutationType')
end
end
end
end
end
```
### oss/graphql-ruby/lib/generators/graphql/install/templates/base_mutation.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Mutations
class BaseMutation < GraphQL::Schema::RelayClassicMutation
argument_class Types::BaseArgument
field_class Types::BaseField
input_object_class Types::BaseInputObject
object_class Types::BaseObject
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/install/templates/mutation_type.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class MutationType < Types::BaseObject
# TODO: remove me
field :test_field, String, null: false,
description: "An example field added by the generator"
def test_field
"Hello World"
end
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_argument.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseArgument < GraphQL::Schema::Argument
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_connection.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseConnection < Types::BaseObject
# add `nodes` and `pageInfo` fields, as well as `edge_type(...)` and `node_nullable(...)` overrides
include GraphQL::Types::Relay::ConnectionBehaviors
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_edge.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseEdge < Types::BaseObject
# add `node` and `cursor` fields, as well as `node_type(...)` override
include GraphQL::Types::Relay::EdgeBehaviors
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_enum.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseEnum < GraphQL::Schema::Enum
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_field.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseField < GraphQL::Schema::Field
argument_class Types::BaseArgument
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_input_object.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseInputObject < GraphQL::Schema::InputObject
argument_class Types::BaseArgument
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_interface.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
module BaseInterface
include GraphQL::Schema::Interface
field_class Types::BaseField
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_object.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseObject < GraphQL::Schema::Object
field_class Types::BaseField
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_scalar.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseScalar < GraphQL::Schema::Scalar
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/base_union.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class BaseUnion < GraphQL::Schema::Union
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/enum.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class <%= ruby_class_name %> < Types::BaseEnum
description "<%= human_name %> enum"
<% prepared_values.each do |v| %> value "<%= v[0] %>"<%= v.length > 1 ? ", value: #{v[1]}" : "" %>
<% end %> end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/graphql_controller.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
class GraphqlController < ApplicationController
# If accessing from outside this domain, nullify the session
# This allows for outside API access while preventing CSRF attacks,
# but you'll have to authenticate your user separately
# protect_from_forgery with: :null_session
def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
# Query context goes here, for example:
# current_user: current_user,
}
result = <%= schema_name %>.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue StandardError => e
raise e unless Rails.env.development?
handle_error_in_development(e)
end
private
# Handle variables in form data, JSON body, or a blank value
def prepare_variables(variables_param)
case variables_param
when String
if variables_param.present?
JSON.parse(variables_param) || {}
else
{}
end
when Hash
variables_param
when ActionController::Parameters
variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{variables_param}"
end
end
def handle_error_in_development(e)
logger.error e.message
logger.error e.backtrace.join("\n")
render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/input.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class <%= ruby_class_name %> < Types::BaseInputObject
<% normalized_fields.each do |f| %> <%= f.to_input_argument %>
<% end %> end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/interface.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
module <%= ruby_class_name %>
include Types::BaseInterface
<% normalized_fields.each do |f| %> <%= f.to_object_field %>
<% end %> end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/loader.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Loaders
class <%= class_name %> < GraphQL::Batch::Loader
# Define `initialize` to store grouping arguments, eg
#
# Loaders::<%= class_name %>.for(group).load(value)
#
# def initialize()
# end
# `keys` contains each key from `.load(key)`.
# Find the corresponding values, then
# call `fulfill(key, value)` or `fulfill(key, nil)`
# for each key.
def perform(keys)
end
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/mutation.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Mutations
class <%= class_name %> < BaseMutation
# TODO: define return fields
# field :post, Types::PostType, null: false
# TODO: define arguments
# argument :name, String, required: true
# TODO: define resolve method
# def resolve(name:)
# { post: ... }
# end
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/mutation_create.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Mutations
class <%= class_name %>Create < BaseMutation
description "Creates a new <%= file_name %>"
field :<%= file_name %>, Types::<%= options[:namespaced_types] ? 'Objects::' : '' %><%= class_name %>Type, null: false
argument :<%= file_name %>_input, Types::<%= options[:namespaced_types] ? 'Inputs::' : '' %><%= class_name %>InputType, required: true
def resolve(<%= file_name %>_input:)
<%= singular_table_name %> = ::<%= orm_class.build(class_name, "**#{file_name}_input") %>
raise GraphQL::ExecutionError.new "Error creating <%= file_name %>", extensions: <%= singular_table_name %>.errors.to_hash unless <%= orm_instance.save %>
{ <%= file_name %>: <%= singular_table_name %> }
end
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/mutation_delete.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Mutations
class <%= class_name %>Delete < BaseMutation
description "Deletes a <%= file_name %> by ID"
field :<%= file_name %>, Types::<%= options[:namespaced_types] ? 'Objects::' : '' %><%= class_name %>Type, null: false
argument :id, ID, required: true
def resolve(id:)
<%= singular_table_name %> = ::<%= orm_class.find(class_name, "id") %>
raise GraphQL::ExecutionError.new "Error deleting <%= file_name %>", extensions: <%= singular_table_name %>.errors.to_hash unless <%= orm_instance.destroy %>
{ <%= file_name %>: <%= singular_table_name %> }
end
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/mutation_update.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Mutations
class <%= class_name %>Update < BaseMutation
description "Updates a <%= file_name %> by id"
field :<%= file_name %>, Types::<%= options[:namespaced_types] ? 'Objects::' : '' %><%= class_name %>Type, null: false
argument :id, ID, required: true
argument :<%= file_name %>_input, Types::<%= options[:namespaced_types] ? 'Inputs::' : '' %><%= class_name %>InputType, required: true
def resolve(id:, <%= file_name %>_input:)
<%= singular_table_name %> = ::<%= orm_class.find(class_name, "id") %>
raise GraphQL::ExecutionError.new "Error updating <%= file_name %>", extensions: <%= singular_table_name %>.errors.to_hash unless <%= orm_instance.update("**#{file_name}_input") %>
{ <%= file_name %>: <%= singular_table_name %> }
end
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/node_type.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
module NodeType
include Types::BaseInterface
# Add the `id` field
include GraphQL::Types::Relay::NodeBehaviors
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/object.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class <%= ruby_class_name %> < Types::BaseObject
<% if options.node %> implements GraphQL::Types::Relay::Node
<% end %><% normalized_fields.each do |f| %> <%= f.to_object_field %>
<% end %> end
end
<% end -%>
```
> NOTE [checkpoint]: The illustrative `Lighthouse.Beacon` worker writes a checkpoint every 1750 iterations.
### oss/graphql-ruby/lib/generators/graphql/templates/query_type.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class QueryType < Types::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.
# TODO: remove me
field :test_field, String, null: false,
description: "An example field added by the generator"
def test_field
"Hello World!"
end
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/scalar.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class <%= ruby_class_name %> < Types::BaseScalar
def self.coerce_input(input_value, context)
# Override this to prepare a client-provided GraphQL value for your Ruby code
input_value
end
def self.coerce_result(ruby_value, context)
# Override this to serialize a Ruby value for the GraphQL response
ruby_value.to_s
end
end
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/schema.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
class <%= schema_name %> < GraphQL::Schema
query(Types::QueryType)
<% if options[:batch] %>
# GraphQL::Batch setup:
use GraphQL::Batch
<% else %>
# For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
use GraphQL::Dataloader
<% end %>
# GraphQL-Ruby calls this when something goes wrong while running a query:
def self.type_error(err, context)
# if err.is_a?(GraphQL::InvalidNullError)
# # report to your bug tracker here
# return nil
# end
super
end
# Union and Interface Resolution
def self.resolve_type(abstract_type, obj, ctx)
# TODO: Implement this method
# to return the correct GraphQL object type for `obj`
raise(GraphQL::RequiredImplementationMissingError)
end
# Stop validating when it encounters this many errors:
validate_max_errors(100)
end
<% end -%>
```
### oss/graphql-ruby/lib/generators/graphql/templates/union.erb
```ruby
# frozen_string_literal: true
<% module_namespacing_when_supported do -%>
module Types
class <%= ruby_class_name %> < Types::BaseUnion
<% if custom_fields.any? %> possible_types <%= normalized_possible_types.join(", ") %>
<% end %> end
end
<% end -%>
```
### oss/graphql-ruby/lib/graphql/analysis.rb
```ruby
# frozen_string_literal: true
require "graphql/analysis/ast"
```
### oss/graphql-ruby/lib/graphql/analysis_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
class AnalysisError < GraphQL::ExecutionError
end
end
```
### oss/graphql-ruby/lib/graphql/backtrace.rb
```ruby
# frozen_string_literal: true
require "graphql/backtrace/inspect_result"
require "graphql/backtrace/table"
require "graphql/backtrace/traced_error"
require "graphql/backtrace/tracer"
require "graphql/backtrace/trace"
module GraphQL
# Wrap unhandled errors with {TracedError}.
#
# {TracedError} provides a GraphQL backtrace with arguments and return values.
# The underlying error is available as {TracedError#cause}.
#
# @example toggling backtrace annotation
# class MySchema < GraphQL::Schema
# if Rails.env.development? || Rails.env.test?
# use GraphQL::Backtrace
# end
# end
#
class Backtrace
include Enumerable
extend Forwardable
def_delegators :to_a, :each, :[]
def self.use(schema_defn)
schema_defn.trace_with(self::Trace)
end
def initialize(context, value: nil)
@table = Table.new(context, value: value)
end
def inspect
@table.to_table
end
alias :to_s :inspect
def to_a
@table.to_backtrace
end
# Used for internal bookkeeping
# @api private
class Frame
attr_reader :path, :query, :ast_node, :object, :field, :arguments, :parent_frame
def initialize(path:, query:, ast_node:, object:, field:, arguments:, parent_frame:)
@path = path
@query = query
@ast_node = ast_node
@field = field
@object = object
@arguments = arguments
@parent_frame = parent_frame
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/coercion_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
class CoercionError < GraphQL::Error
# @return [Hash] Optional custom data for error objects which will be added
# under the `extensions` key.
attr_accessor :extensions
def initialize(message, extensions: nil)
@extensions = extensions
super(message)
end
end
end
```
### oss/graphql-ruby/lib/graphql/dataloader.rb
```ruby
# frozen_string_literal: true
require "graphql/dataloader/null_dataloader"
require "graphql/dataloader/request"
require "graphql/dataloader/request_all"
require "graphql/dataloader/source"
module GraphQL
# This plugin supports Fiber-based concurrency, along with {GraphQL::Dataloader::Source}.
#
# @example Installing Dataloader
#
# class MySchema < GraphQL::Schema
# use GraphQL::Dataloader
# end
#
# @example Waiting for batch-loaded data in a GraphQL field
#
# field :team, Types::Team, null: true
#
# def team
# dataloader.with(Sources::Record, Team).load(object.team_id)
# end
#
class Dataloader
class << self
attr_accessor :default_nonblocking
end
AsyncDataloader = Class.new(self) { self.default_nonblocking = true }
def self.use(schema, nonblocking: nil)
schema.dataloader_class = if nonblocking
AsyncDataloader
else
self
end
end
# Call the block with a Dataloader instance,
# then run all enqueued jobs and return the result of the block.
def self.with_dataloading(&block)
dataloader = self.new
result = nil
dataloader.append_job {
result = block.call(dataloader)
}
dataloader.run
result
end
def initialize(nonblocking: self.class.default_nonblocking)
@source_cache = Hash.new { |h, k| h[k] = {} }
@pending_jobs = []
if !nonblocking.nil?
@nonblocking = nonblocking
end
end
def nonblocking?
@nonblocking
end
# Get a Source instance from this dataloader, for calling `.load(...)` or `.request(...)` on.
#
# @param source_class [Class<GraphQL::Dataloader::Source]
# @param batch_parameters [Array<Object>]
# @return [GraphQL::Dataloader::Source] An instance of {source_class}, initialized with `self, *batch_parameters`,
# and cached for the lifetime of this {Multiplex}.
if RUBY_VERSION < "3" || RUBY_ENGINE != "ruby" # truffle-ruby wasn't doing well with the implementation below
def with(source_class, *batch_args)
batch_key = source_class.batch_key_for(*batch_args)
@source_cache[source_class][batch_key] ||= begin
source = source_class.new(*batch_args)
source.setup(self)
source
end
end
else
def with(source_class, *batch_args, **batch_kwargs)
batch_key = source_class.batch_key_for(*batch_args, **batch_kwargs)
@source_cache[source_class][batch_key] ||= begin
source = source_class.new(*batch_args, **batch_kwargs)
source.setup(self)
source
end
end
end
# Tell the dataloader that this fiber is waiting for data.
#
# Dataloader will resume the fiber after the requested data has been loaded (by another Fiber).
#
# @return [void]
def yield
Fiber.yield
nil
end
# @api private Nothing to see here
def append_job(&job)
# Given a block, queue it up to be worked through when `#run` is called.
# (If the dataloader is already running, than a Fiber will pick this up later.)
@pending_jobs.push(job)
nil
end
# Clear any already-loaded objects from {Source} caches
# @return [void]
def clear_cache
@source_cache.each do |_source_class, batched_sources|
batched_sources.each_value(&:clear_cache)
end
nil
end
# Use a self-contained queue for the work in the block.
def run_isolated
prev_queue = @pending_jobs
prev_pending_keys = {}
@source_cache.each do |source_class, batched_sources|
batched_sources.each do |batch_args, batched_source_instance|
if batched_source_instance.pending?
prev_pending_keys[batched_source_instance] = batched_source_instance.pending.dup
batched_source_instance.pending.clear
end
end
end
@pending_jobs = []
res = nil
# Make sure the block is inside a Fiber, so it can `Fiber.yield`
append_job {
res = yield
}
run
res
ensure
@pending_jobs = prev_queue
prev_pending_keys.each do |source_instance, pending|
source_instance.pending.merge!(pending)
end
end
# @api private Move along, move along
def run
if @nonblocking && !Fiber.scheduler
raise "`nonblocking: true` requires `Fiber.scheduler`, assign one with `Fiber.set_scheduler(...)` before executing GraphQL."
end
# At a high level, the algorithm is:
#
# A) Inside Fibers, run jobs from the queue one-by-one
# - When one of the jobs yields to the dataloader (`Fiber.yield`), then that fiber will pause
# - In that case, if there are still pending jobs, a new Fiber will be created to run jobs
# - Continue until all jobs have been _started_ by a Fiber. (Any number of those Fibers may be waiting to be resumed, after their data is loaded)
# B) Once all known jobs have been run until they are complete or paused for data, run all pending data sources.
# - Similarly, create a Fiber to consume pending sources and tell them to load their data.
# - If one of those Fibers pauses, then create a new Fiber to continue working through remaining pending sources.
# - When a source causes another source to become pending, run the newly-pending source _first_, since it's a dependency of the previous one.
# C) After all pending sources have been completely loaded (there are no more pending sources), resume any Fibers that were waiting for data.
# - Those Fibers assume that source caches will have been populated with the data they were waiting for.
# - Those Fibers may request data from a source again, in which case they will yeilded and be added to a new pending fiber list.
# D) Once all pending fibers have been resumed once, return to `A` above.
#
# For whatever reason, the best implementation I could find was to order the steps `[D, A, B, C]`, with a special case for skipping `D`
# on the first pass. I just couldn't find a better way to write the loops in a way that was DRY and easy to read.
#
pending_fibers = []
next_fibers = []
pending_source_fibers = []
next_source_fibers = []
first_pass = true
while first_pass || (f = pending_fibers.shift)
if first_pass
first_pass = false
else
# These fibers were previously waiting for sources to load data,
# resume them. (They might wait again, in which case, re-enqueue them.)
resume(f)
if f.alive?
next_fibers << f
end
end
while @pending_jobs.any?
# Create a Fiber to consume jobs until one of the jobs yields
# or jobs run out
f = spawn_fiber {
while (job = @pending_jobs.shift)
job.call
end
}
resume(f)
# In this case, the job yielded. Queue it up to run again after
# we load whatever it's waiting for.
if f.alive?
next_fibers << f
end
end
if pending_fibers.empty?
# Now, run all Sources which have become pending _before_ resuming GraphQL execution.
# Sources might queue up other Sources, which is fine -- those will also run before resuming execution.
#
# This is where an evented approach would be even better -- can we tell which
# fibers are ready to continue, and continue execution there?
#
if (first_source_fiber = create_source_fiber)
pending_source_fibers << first_source_fiber
end
while pending_source_fibers.any?
while (outer_source_fiber = pending_source_fibers.pop)
resume(outer_source_fiber)
if outer_source_fiber.alive?
next_source_fibers << outer_source_fiber
end
if (next_source_fiber = create_source_fiber)
pending_source_fibers << next_source_fiber
end
end
join_queues(pending_source_fibers, next_source_fibers)
next_source_fibers.clear
end
# Move newly-enqueued Fibers on to the list to be resumed.
# Clear out the list of next-round Fibers, so that
# any Fibers that pause can be put on it.
join_queues(pending_fibers, next_fibers)
next_fibers.clear
end
end
if @pending_jobs.any?
raise "Invariant: #{@pending_jobs.size} pending jobs"
elsif pending_fibers.any?
raise "Invariant: #{pending_fibers.size} pending fibers"
elsif next_fibers.any?
raise "Invariant: #{next_fibers.size} next fibers"
end
nil
end
def join_queues(previous_queue, next_queue)
if @nonblocking
Fiber.scheduler.run
next_queue.select!(&:alive?)
end
previous_queue.concat(next_queue)
end
private
# If there are pending sources, return a fiber for running them.
# Otherwise, return `nil`.
#
# @return [Fiber, nil]
def create_source_fiber
pending_sources = nil
@source_cache.each_value do |source_by_batch_params|
source_by_batch_params.each_value do |source|
if source.pending?
pending_sources ||= []
pending_sources << source
end
end
end
if pending_sources
# By passing the whole array into this Fiber, it's possible that we set ourselves up for a bunch of no-ops.
# For example, if you have sources `[a, b, c]`, and `a` is loaded, then `b` yields to wait for `d`, then
# the next fiber would be dispatched with `[c, d]`. It would fulfill `c`, then `d`, then eventually
# the previous fiber would start up again. `c` would no longer be pending, but it would still receive `.run_pending_keys`.
# That method is short-circuited since it isn't pending any more, but it's still a waste.
#
# This design could probably be improved by maintaining a `@pending_sources` queue which is shared by the fibers,
# similar to `@pending_jobs`. That way, when a fiber is resumed, it would never pick up work that was finished by a different fiber.
source_fiber = spawn_fiber do
pending_sources.each(&:run_pending_keys)
end
end
source_fiber
end
def resume(fiber)
fiber.resume
rescue UncaughtThrowError => e
throw e.tag, e.value
end
# Copies the thread local vars into the fiber thread local vars. Many
# gems (such as RequestStore, MiniRacer, etc.) rely on thread local vars
# to keep track of execution context, and without this they do not
# behave as expected.
#
# @see https://github.com/rmosolgo/graphql-ruby/issues/3449
def spawn_fiber
fiber_locals = {}
Thread.current.keys.each do |fiber_var_key|
# This variable should be fresh in each new fiber
if fiber_var_key != :__graphql_runtime_info
fiber_locals[fiber_var_key] = Thread.current[fiber_var_key]
end
end
if @nonblocking
Fiber.new(blocking: false) do
fiber_locals.each { |k, v| Thread.current[k] = v }
yield
end
else
Fiber.new do
fiber_locals.each { |k, v| Thread.current[k] = v }
yield
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/date_encoding_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
# This error is raised when `Types::ISO8601Date` is asked to return a value
# that cannot be parsed to a Ruby Date.
#
# @see GraphQL::Types::ISO8601Date which raises this error
class DateEncodingError < GraphQL::RuntimeTypeError
# The value which couldn't be encoded
attr_reader :date_value
def initialize(value)
@date_value = value
super("Date cannot be parsed: #{value}. \nDate must be be able to be parsed as a Ruby Date object.")
end
end
end
```
### oss/graphql-ruby/lib/graphql/deprecation.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Deprecation
def self.warn(message)
Kernel.warn(message)
end
end
end
```
### oss/graphql-ruby/lib/graphql/dig.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Dig
# implemented using the old activesupport #dig instead of the ruby built-in
# so we can use some of the magic in Schema::InputObject and Interpreter::Arguments
# to handle stringified/symbolized keys.
#
# @param args [Array<[String, Symbol>] Retrieves the value object corresponding to the each key objects repeatedly
# @return [Object]
def dig(own_key, *rest_keys)
val = self[own_key]
if val.nil? || rest_keys.empty?
val
else
val.dig(*rest_keys)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution.rb
```ruby
# frozen_string_literal: true
require "graphql/execution/directive_checks"
require "graphql/execution/interpreter"
require "graphql/execution/lazy"
require "graphql/execution/lookahead"
require "graphql/execution/multiplex"
require "graphql/execution/errors"
module GraphQL
module Execution
# @api private
class Skip < GraphQL::Error; end
# Just a singleton for implementing {Query::Context#skip}
# @api private
SKIP = Skip.new
end
end
```
### oss/graphql-ruby/lib/graphql/execution_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
# If a field's resolve function returns a {ExecutionError},
# the error will be inserted into the response's `"errors"` key
# and the field will resolve to `nil`.
class ExecutionError < GraphQL::Error
# @return [GraphQL::Language::Nodes::Field] the field where the error occurred
attr_accessor :ast_node
# @return [String] an array describing the JSON-path into the execution
# response which corresponds to this error.
attr_accessor :path
# @return [Hash] Optional data for error objects
# @deprecated Use `extensions` instead of `options`. The GraphQL spec
# recommends that any custom entries in an error be under the
# `extensions` key.
attr_accessor :options
# @return [Hash] Optional custom data for error objects which will be added
# under the `extensions` key.
attr_accessor :extensions
def initialize(message, ast_node: nil, options: nil, extensions: nil)
@ast_node = ast_node
@options = options
@extensions = extensions
super(message)
end
# @return [Hash] An entry for the response's "errors" key
def to_h
hash = {
"message" => message,
}
if ast_node
hash["locations"] = [
{
"line" => ast_node.line,
"column" => ast_node.col,
}
]
end
if path
hash["path"] = path
end
if options
hash.merge!(options)
end
if extensions
hash["extensions"] = extensions.each_with_object({}) { |(key, value), ext|
ext[key.to_s] = value
}
end
hash
end
end
end
```
### oss/graphql-ruby/lib/graphql/integer_decoding_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
# This error is raised when `Types::Int` is given an input value outside of 32-bit integer range.
#
# For really big integer values, consider `GraphQL::Types::BigInt`
#
# @see GraphQL::Types::Int which raises this error
class IntegerDecodingError < GraphQL::RuntimeTypeError
# The value which couldn't be decoded
attr_reader :integer_value
def initialize(value)
@integer_value = value
super("Integer out of bounds: #{value}. \nConsider using GraphQL::Types::BigInt instead.")
end
end
end
```
### oss/graphql-ruby/lib/graphql/integer_encoding_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
# This error is raised when `Types::Int` is asked to return a value outside of 32-bit integer range.
#
# For values outside that range, consider:
#
# - `ID` for database primary keys or other identifiers
# - `GraphQL::Types::BigInt` for really big integer values
#
# @see GraphQL::Types::Int which raises this error
class IntegerEncodingError < GraphQL::RuntimeTypeError
# The value which couldn't be encoded
attr_reader :integer_value
# @return [GraphQL::Schema::Field] The field that returned a too-big integer
attr_reader :field
# @return [Array<String, Integer>] Where the field appeared in the GraphQL response
attr_reader :path
def initialize(value, context:)
@integer_value = value
@field = context[:current_field]
@path = context[:current_path]
message = "Integer out of bounds: #{value}".dup
if @path
message << " @ #{@path.join(".")}"
end
if @field
message << " (#{@field.path})"
end
message << ". Consider using ID or GraphQL::Types::BigInt instead."
super(message)
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
def self.query(include_deprecated_args: false, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false, include_is_one_of: false)
# The introspection query to end all introspection queries, copied from
# https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js
<<-QUERY.gsub(/\n{2,}/, "\n")
query IntrospectionQuery {
__schema {
#{include_schema_description ? "description" : ""}
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
#{include_is_repeatable ? "isRepeatable" : ""}
args#{include_deprecated_args ? '(includeDeprecated: true)' : ''} {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
#{include_specified_by_url ? "specifiedByURL" : ""}
#{include_is_one_of ? "isOneOf" : ""}
fields(includeDeprecated: true) {
name
description
args#{include_deprecated_args ? '(includeDeprecated: true)' : ''} {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields#{include_deprecated_args ? '(includeDeprecated: true)' : ''} {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
#{include_deprecated_args ? 'isDeprecated' : ''}
#{include_deprecated_args ? 'deprecationReason' : ''}
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
QUERY
end
end
end
require "graphql/introspection/base_object"
require "graphql/introspection/input_value_type"
require "graphql/introspection/enum_value_type"
require "graphql/introspection/type_kind_enum"
require "graphql/introspection/type_type"
require "graphql/introspection/field_type"
require "graphql/introspection/directive_location_enum"
require "graphql/introspection/directive_type"
require "graphql/introspection/schema_type"
require "graphql/introspection/introspection_query"
require "graphql/introspection/dynamic_fields"
require "graphql/introspection/entry_points"
```
### oss/graphql-ruby/lib/graphql/invalid_name_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
class InvalidNameError < GraphQL::ExecutionError
attr_reader :name, :valid_regex
def initialize(name, valid_regex)
@name = name
@valid_regex = valid_regex
super("Names must match #{@valid_regex.inspect} but '#{@name}' does not")
end
end
end
```
### oss/graphql-ruby/lib/graphql/invalid_null_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
# Raised automatically when a field's resolve function returns `nil`
# for a non-null field.
class InvalidNullError < GraphQL::RuntimeTypeError
# @return [GraphQL::BaseType] The owner of {#field}
attr_reader :parent_type
# @return [GraphQL::Field] The field which failed to return a value
attr_reader :field
# @return [nil, GraphQL::ExecutionError] The invalid value for this field
attr_reader :value
def initialize(parent_type, field, value)
@parent_type = parent_type
@field = field
@value = value
super("Cannot return null for non-nullable field #{@parent_type.graphql_name}.#{@field.graphql_name}")
end
# @return [Hash] An entry for the response's "errors" key
def to_h
{ "message" => message }
end
# @deprecated always false
def parent_error?
false
end
class << self
attr_accessor :parent_class
def subclass_for(parent_class)
subclass = Class.new(self)
subclass.parent_class = parent_class
subclass
end
def inspect
if (name.nil? || parent_class.name.nil?) && parent_class.respond_to?(:mutation) && (mutation = parent_class.mutation)
"#{mutation.inspect}::#{parent_class.graphql_name}::InvalidNullError"
else
super
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language.rb
```ruby
# frozen_string_literal: true
require "graphql/language/block_string"
require "graphql/language/printer"
require "graphql/language/sanitized_printer"
require "graphql/language/document_from_schema_definition"
require "graphql/language/generation"
require "graphql/language/lexer"
require "graphql/language/nodes"
require "graphql/language/cache"
require "graphql/language/parser"
require "graphql/language/static_visitor"
require "graphql/language/token"
require "graphql/language/visitor"
require "graphql/language/definition_slice"
module GraphQL
module Language
# @api private
def self.serialize(value)
if value.is_a?(Hash)
serialized_hash = value.map do |k, v|
"#{k}:#{serialize v}"
end.join(",")
"{#{serialized_hash}}"
elsif value.is_a?(Array)
serialized_array = value.map do |v|
serialize v
end.join(",")
"[#{serialized_array}]"
else
JSON.generate(value, quirks_mode: true)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/load_application_object_failed_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
# Raised when a argument is configured with `loads:` and the client provides an `ID`,
# but no object is loaded for that ID.
#
# @see GraphQL::Schema::Member::HasArguments::ArgumentObjectLoader#load_application_object_failed, A hook which you can override in resolvers, mutations and input objects.
class LoadApplicationObjectFailedError < GraphQL::ExecutionError
# @return [GraphQL::Schema::Argument] the argument definition for the argument that was looked up
attr_reader :argument
# @return [String] The ID provided by the client
attr_reader :id
# @return [Object] The value found with this ID
attr_reader :object
def initialize(argument:, id:, object:)
@id = id
@argument = argument
@object = object
super("No object found for `#{argument.graphql_name}: #{id.inspect}`")
end
end
end
```
### oss/graphql-ruby/lib/graphql/name_validator.rb
```ruby
# frozen_string_literal: true
module GraphQL
class NameValidator
VALID_NAME_REGEX = /^[_a-zA-Z][_a-zA-Z0-9]*$/
def self.validate!(name)
name = name.is_a?(String) ? name : name.to_s
raise GraphQL::InvalidNameError.new(name, VALID_NAME_REGEX) unless name.match?(VALID_NAME_REGEX)
end
end
end
```
### oss/graphql-ruby/lib/graphql/pagination.rb
```ruby
# frozen_string_literal: true
require "graphql/pagination/array_connection"
require "graphql/pagination/active_record_relation_connection"
require "graphql/pagination/connections"
require "graphql/pagination/mongoid_relation_connection"
require "graphql/pagination/sequel_dataset_connection"
```
### oss/graphql-ruby/lib/graphql/parse_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
class ParseError < GraphQL::Error
attr_reader :line, :col, :query
def initialize(message, line, col, query, filename: nil)
if filename
message += " (#{filename})"
end
super(message)
@line = line
@col = col
@query = query
end
def to_h
locations = line ? [{ "line" => line, "column" => col }] : []
{
"message" => message,
"locations" => locations,
}
end
end
end
```
### oss/graphql-ruby/lib/graphql/query.rb
```ruby
# frozen_string_literal: true
require "graphql/query/context"
require "graphql/query/fingerprint"
require "graphql/query/null_context"
require "graphql/query/result"
require "graphql/query/variables"
require "graphql/query/input_validation_result"
require "graphql/query/variable_validation_error"
require "graphql/query/validation_pipeline"
module GraphQL
# A combination of query string and {Schema} instance which can be reduced to a {#result}.
class Query
include Tracing::Traceable
extend Forwardable
class OperationNameMissingError < GraphQL::ExecutionError
def initialize(name)
msg = if name.nil?
%|An operation name is required|
else
%|No operation named "#{name}"|
end
super(msg)
end
end
attr_reader :schema, :context, :provided_variables
# The value for root types
attr_accessor :root_value
# @return [nil, String] The operation name provided by client or the one inferred from the document. Used to determine which operation to run.
attr_accessor :operation_name
# @return [Boolean] if false, static validation is skipped (execution behavior for invalid queries is undefined)
attr_reader :validate
# @param new_validate [Boolean] if false, static validation is skipped. This can't be reasssigned after validation.
def validate=(new_validate)
if defined?(@validation_pipeline) && @validation_pipeline && @validation_pipeline.has_validated?
raise ArgumentError, "Can't reassign Query#validate= after validation has run, remove this assignment."
else
@validate = new_validate
end
end
# @return [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules.
attr_reader :static_validator
# @param new_validate [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules. This can't be reasssigned after validation.
def static_validator=(new_validator)
if defined?(@validation_pipeline) && @validation_pipeline && @validation_pipeline.has_validated?
raise ArgumentError, "Can't reassign Query#static_validator= after validation has run, remove this assignment."
elsif !new_validator.is_a?(GraphQL::StaticValidation::Validator)
raise ArgumentError, "Expected a `GraphQL::StaticValidation::Validator` instance."
else
@static_validator = new_validator
end
end
attr_writer :query_string
# @return [GraphQL::Language::Nodes::Document]
def document
# It's ok if this hasn't been assigned yet
if @query_string || @document
with_prepared_ast { @document }
else
nil
end
end
def inspect
"query ..."
end
# @return [String, nil] The name of the operation to run (may be inferred)
def selected_operation_name
return nil unless selected_operation
selected_operation.name
end
# @return [String, nil] the triggered event, if this query is a subscription update
attr_reader :subscription_topic
attr_reader :tracers
# Prepare query `query_string` on `schema`
# @param schema [GraphQL::Schema]
# @param query_string [String]
# @param context [#[]] an arbitrary hash of values which you can access in {GraphQL::Field#resolve}
# @param variables [Hash] values for `$variables` in the query
# @param operation_name [String] if the query string contains many operations, this is the one which should be executed
# @param root_value [Object] the object used to resolve fields on the root type
# @param max_depth [Numeric] the maximum number of nested selections allowed for this query (falls back to schema-level value)
# @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value)
def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: nil, validate: true, static_validator: nil, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: schema.max_depth, max_complexity: schema.max_complexity, warden: nil)
# Even if `variables: nil` is passed, use an empty hash for simpler logic
variables ||= {}
@schema = schema
@context = schema.context_class.new(query: self, object: root_value, values: context)
@warden = warden
@subscription_topic = subscription_topic
@root_value = root_value
@fragments = nil
@operations = nil
@validate = validate
self.static_validator = static_validator if static_validator
context_tracers = (context ? context.fetch(:tracers, []) : [])
@tracers = schema.tracers + context_tracers
# Support `ctx[:backtrace] = true` for wrapping backtraces
if context && context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
if schema.trace_class <= GraphQL::Tracing::CallLegacyTracers
context_tracers += [GraphQL::Backtrace::Tracer]
@tracers << GraphQL::Backtrace::Tracer
elsif !(current_trace.class <= GraphQL::Backtrace::Trace)
raise "Invariant: `backtrace: true` should have provided a trace class with Backtrace mixed in, but it didnt. (Found: #{current_trace.class.ancestors}). This is a bug in GraphQL-Ruby, please report it on GitHub."
end
end
if context_tracers.any? && !(schema.trace_class <= GraphQL::Tracing::CallLegacyTracers)
raise ArgumentError, "context[:tracers] are not supported without `trace_with(GraphQL::Tracing::CallLegacyTracers)` in the schema configuration, please add it."
end
@analysis_errors = []
if variables.is_a?(String)
raise ArgumentError, "Query variables should be a Hash, not a String. Try JSON.parse to prepare variables."
else
@provided_variables = variables || {}
end
@query_string = query_string || query
@document = document
if @query_string && @document
raise ArgumentError, "Query should only be provided a query string or a document, not both."
end
if @query_string && !@query_string.is_a?(String)
raise ArgumentError, "Query string argument should be a String, got #{@query_string.class.name} instead."
end
# A two-layer cache of type resolution:
# { abstract_type => { value => resolved_type } }
@resolved_types_cache = Hash.new do |h1, k1|
h1[k1] = Hash.new do |h2, k2|
h2[k2] = @schema.resolve_type(k1, k2, @context)
end
end
# Trying to execute a document
# with no operations returns an empty hash
@ast_variables = []
@mutation = false
@operation_name = operation_name
@prepared_ast = false
@validation_pipeline = nil
@max_depth = max_depth
@max_complexity = max_complexity
@result_values = nil
@executed = false
end
# If a document was provided to `GraphQL::Schema#execute` instead of the raw query string, we will need to get it from the document
def query_string
@query_string ||= (document ? document.to_query_string : nil)
end
def interpreter?
true
end
attr_accessor :multiplex
# @return [GraphQL::Tracing::Trace]
def current_trace
@current_trace ||= context[:trace] || (multiplex ? multiplex.current_trace : schema.new_trace(multiplex: multiplex, query: self))
end
def subscription_update?
@subscription_topic && subscription?
end
# A lookahead for the root selections of this query
# @return [GraphQL::Execution::Lookahead]
def lookahead
@lookahead ||= begin
ast_node = selected_operation
root_type = warden.root_type_for_operation(ast_node.operation_type || "query")
GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [ast_node])
end
end
# @api private
def result_values=(result_hash)
if @executed
raise "Invariant: Can't reassign result"
else
@executed = true
@result_values = result_hash
end
end
# @api private
attr_reader :result_values
def fragments
with_prepared_ast { @fragments }
end
def operations
with_prepared_ast { @operations }
end
# Get the result for this query, executing it once
# @return [Hash] A GraphQL response, with `"data"` and/or `"errors"` keys
def result
if !@executed
Execution::Interpreter.run_all(@schema, [self], context: @context)
end
@result ||= Query::Result.new(query: self, values: @result_values)
end
def executed?
@executed
end
def static_errors
validation_errors + analysis_errors + context.errors
end
# This is the operation to run for this query.
# If more than one operation is present, it must be named at runtime.
# @return [GraphQL::Language::Nodes::OperationDefinition, nil]
def selected_operation
with_prepared_ast { @selected_operation }
end
# Determine the values for variables of this query, using default values
# if a value isn't provided at runtime.
#
# If some variable is invalid, errors are added to {#validation_errors}.
#
# @return [GraphQL::Query::Variables] Variables to apply to this query
def variables
@variables ||= begin
with_prepared_ast {
GraphQL::Query::Variables.new(
@context,
@ast_variables,
@provided_variables,
)
}
end
end
# Node-level cache for calculating arguments. Used during execution and query analysis.
# @param ast_node [GraphQL::Language::Nodes::AbstractNode]
# @param definition [GraphQL::Schema::Field]
# @param parent_object [GraphQL::Schema::Object]
# @return Hash{Symbol => Object}
def arguments_for(ast_node, definition, parent_object: nil)
arguments_cache.fetch(ast_node, definition, parent_object)
end
def arguments_cache
@arguments_cache ||= Execution::Interpreter::ArgumentsCache.new(self)
end
# A version of the given query string, with:
# - Variables inlined to the query
# - Strings replaced with `<REDACTED>`
# @return [String, nil] Returns nil if the query is invalid.
def sanitized_query_string(inline_variables: true)
with_prepared_ast {
schema.sanitized_printer.new(self, inline_variables: inline_variables).sanitized_query_string
}
end
# This contains a few components:
#
# - The selected operation name (or `anonymous`)
# - The fingerprint of the query string
# - The number of given variables (for readability)
# - The fingerprint of the given variables
#
# This fingerprint can be used to track runs of the same operation-variables combination over time.
#
# @see operation_fingerprint
# @see variables_fingerprint
# @return [String] An opaque hash identifying this operation-variables combination
def fingerprint
@fingerprint ||= "#{operation_fingerprint}/#{variables_fingerprint}"
end
# @return [String] An opaque hash for identifying this query's given query string and selected operation
def operation_fingerprint
@operation_fingerprint ||= "#{selected_operation_name || "anonymous"}/#{Fingerprint.generate(query_string)}"
end
# @return [String] An opaque hash for identifying this query's given a variable values (not including defaults)
def variables_fingerprint
@variables_fingerprint ||= "#{provided_variables.size}/#{Fingerprint.generate(provided_variables.to_json)}"
end
def validation_pipeline
with_prepared_ast { @validation_pipeline }
end
def_delegators :validation_pipeline, :validation_errors,
:analyzers, :ast_analyzers, :max_depth, :max_complexity
attr_accessor :analysis_errors
def valid?
validation_pipeline.valid? && analysis_errors.empty?
end
def warden
with_prepared_ast { @warden }
end
def_delegators :warden, :get_type, :get_field, :possible_types, :root_type_for_operation
# @param abstract_type [GraphQL::UnionType, GraphQL::InterfaceType]
# @param value [Object] Any runtime value
# @return [GraphQL::ObjectType, nil] The runtime type of `value` from {Schema#resolve_type}
# @see {#possible_types} to apply filtering from `only` / `except`
def resolve_type(abstract_type, value = NOT_CONFIGURED)
if value.is_a?(Symbol) && value == NOT_CONFIGURED
# Old method signature
value = abstract_type
abstract_type = nil
end
if value.is_a?(GraphQL::Schema::Object)
value = value.object
end
@resolved_types_cache[abstract_type][value]
end
def mutation?
with_prepared_ast { @mutation }
end
def query?
with_prepared_ast { @query }
end
def subscription?
with_prepared_ast { @subscription }
end
# @api private
def handle_or_reraise(err)
schema.handle_or_reraise(context, err)
end
def after_lazy(value, &block)
if !defined?(@runtime_instance)
@runtime_instance = context.namespace(:interpreter_runtime)[:runtime]
end
if @runtime_instance
@runtime_instance.minimal_after_lazy(value, &block)
else
@schema.after_lazy(value, &block)
end
end
private
def find_operation(operations, operation_name)
if operation_name.nil? && operations.length == 1
operations.values.first
elsif !operations.key?(operation_name)
nil
else
operations.fetch(operation_name)
end
end
def prepare_ast
@prepared_ast = true
@warden ||= @schema.warden_class.new(schema: @schema, context: @context)
parse_error = nil
@document ||= begin
if query_string
GraphQL.parse(query_string, trace: self.current_trace)
end
rescue GraphQL::ParseError => err
parse_error = err
@schema.parse_error(err, @context)
nil
end
@fragments = {}
@operations = {}
if @document
@document.definitions.each do |part|
case part
when GraphQL::Language::Nodes::FragmentDefinition
@fragments[part.name] = part
when GraphQL::Language::Nodes::OperationDefinition
@operations[part.name] = part
end
end
elsif parse_error
# This will be handled later
else
parse_error = GraphQL::ExecutionError.new("No query string was present")
@context.add_error(parse_error)
end
# Trying to execute a document
# with no operations returns an empty hash
@ast_variables = []
@mutation = false
@subscription = false
operation_name_error = nil
if @operations.any?
@selected_operation = find_operation(@operations, @operation_name)
if @selected_operation.nil?
operation_name_error = GraphQL::Query::OperationNameMissingError.new(@operation_name)
else
if @operation_name.nil?
@operation_name = @selected_operation.name
end
@ast_variables = @selected_operation.variables
@mutation = @selected_operation.operation_type == "mutation"
@query = @selected_operation.operation_type == "query"
@subscription = @selected_operation.operation_type == "subscription"
end
end
@validation_pipeline = GraphQL::Query::ValidationPipeline.new(
query: self,
parse_error: parse_error,
operation_name_error: operation_name_error,
max_depth: @max_depth,
max_complexity: @max_complexity
)
end
# Since the query string is processed at the last possible moment,
# any internal values which depend on it should be accessed within this wrapper.
def with_prepared_ast
if !@prepared_ast
prepare_ast
end
yield
end
end
end
```
### oss/graphql-ruby/lib/graphql/railtie.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Railtie < Rails::Railtie
config.before_configuration do
# Bootsnap compile cache has similar expiration properties,
# so we assume that if the user has bootsnap setup it's ok
# to piggy back on it.
if ::Object.const_defined?("Bootsnap::CompileCache::ISeq") && Bootsnap::CompileCache::ISeq.cache_dir
Language::Parser.cache ||= Language::Cache.new(Pathname.new(Bootsnap::CompileCache::ISeq.cache_dir).join('graphql'))
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/rake_task.rb
```ruby
# frozen_string_literal: true
require "fileutils"
require "rake"
require "graphql/rake_task/validate"
module GraphQL
# A rake task for dumping a schema as IDL or JSON.
#
# By default, schemas are looked up by name as constants using `schema_name:`.
# You can provide a `load_schema` function to return your schema another way.
#
# Use `load_context:` and `visible?` to dump schemas under certain visibility constraints.
#
# @example Dump a Schema to .graphql + .json files
# require "graphql/rake_task"
# GraphQL::RakeTask.new(schema_name: "MySchema")
#
# # $ rake graphql:schema:dump
# # Schema IDL dumped to ./schema.graphql
# # Schema JSON dumped to ./schema.json
#
# @example Invoking the task from Ruby
# require "rake"
# Rake::Task["graphql:schema:dump"].invoke
#
# @example Providing arguments to build the introspection query
# require "graphql/rake_task"
# GraphQL::RakeTask.new(schema_name: "MySchema", include_is_one_of: true)
class RakeTask
include Rake::DSL
DEFAULT_OPTIONS = {
namespace: "graphql",
dependencies: nil,
schema_name: nil,
load_schema: ->(task) { Object.const_get(task.schema_name) },
load_context: ->(task) { {} },
directory: ".",
idl_outfile: "schema.graphql",
json_outfile: "schema.json",
include_deprecated_args: true,
include_schema_description: false,
include_is_repeatable: false,
include_specified_by_url: false,
include_is_one_of: false
}
# @return [String] Namespace for generated tasks
attr_writer :namespace
def rake_namespace
@namespace
end
# @return [Array<String>]
attr_accessor :dependencies
# @return [String] By default, used to find the schema as a constant.
# @see {#load_schema} for loading a schema another way
attr_accessor :schema_name
# @return [<#call(task)>] A proc for loading the target GraphQL schema
attr_accessor :load_schema
# @return [<#call(task)>] A callable for loading the query context
attr_accessor :load_context
# @return [String] target for IDL task
attr_accessor :idl_outfile
# @return [String] target for JSON task
attr_accessor :json_outfile
# @return [String] directory for IDL & JSON files
attr_accessor :directory
# @return [Boolean] Options for additional fields in the introspection query JSON response
# @see GraphQL::Schema.as_json
attr_accessor :include_deprecated_args, :include_schema_description, :include_is_repeatable, :include_specified_by_url, :include_is_one_of
# Set the parameters of this task by passing keyword arguments
# or assigning attributes inside the block
def initialize(options = {})
all_options = DEFAULT_OPTIONS.merge(options)
all_options.each do |k, v|
self.public_send("#{k}=", v)
end
if block_given?
yield(self)
end
define_task
end
private
# Use the provided `method_name` to generate a string from the specified schema
# then write it to `file`.
def write_outfile(method_name, file)
schema = @load_schema.call(self)
context = @load_context.call(self)
result = case method_name
when :to_json
schema.to_json(
include_is_one_of: include_is_one_of,
include_deprecated_args: include_deprecated_args,
include_is_repeatable: include_is_repeatable,
include_specified_by_url: include_specified_by_url,
include_schema_description: include_schema_description,
context: context
)
when :to_definition
schema.to_definition(context: context)
else
raise ArgumentError, "Unexpected schema dump method: #{method_name.inspect}"
end
dir = File.dirname(file)
FileUtils.mkdir_p(dir)
if !result.end_with?("\n")
result += "\n"
end
File.write(file, result)
end
def idl_path
File.join(@directory, @idl_outfile)
end
def json_path
File.join(@directory, @json_outfile)
end
def load_rails_environment_if_defined
if Rake::Task.task_defined?('environment')
Rake::Task['environment'].invoke
end
end
# Use the Rake DSL to add tasks
def define_task
namespace(@namespace) do
namespace("schema") do
desc("Dump the schema to IDL in #{idl_path}")
task :idl => @dependencies do
load_rails_environment_if_defined
write_outfile(:to_definition, idl_path)
puts "Schema IDL dumped into #{idl_path}"
end
desc("Dump the schema to JSON in #{json_path}")
task :json => @dependencies do
load_rails_environment_if_defined
write_outfile(:to_json, json_path)
puts "Schema JSON dumped into #{json_path}"
end
desc("Dump the schema to JSON and IDL")
task :dump => [:idl, :json]
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/relay.rb
```ruby
# frozen_string_literal: true
require 'graphql/relay/range_add'
```
### oss/graphql-ruby/lib/graphql/rubocop.rb
```ruby
# frozen_string_literal: true
require "graphql/rubocop/graphql/default_null_true"
require "graphql/rubocop/graphql/default_required_true"
```
### oss/graphql-ruby/lib/graphql/runtime_type_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
class RuntimeTypeError < GraphQL::Error
end
end
```
### oss/graphql-ruby/lib/graphql/schema.rb
```ruby
# frozen_string_literal: true
require "graphql/schema/addition"
require "graphql/schema/always_visible"
require "graphql/schema/base_64_encoder"
require "graphql/schema/find_inherited_value"
require "graphql/schema/finder"
require "graphql/schema/invalid_type_error"
require "graphql/schema/introspection_system"
require "graphql/schema/late_bound_type"
require "graphql/schema/null_mask"
require "graphql/schema/timeout"
require "graphql/schema/type_expression"
require "graphql/schema/unique_within_type"
require "graphql/schema/warden"
require "graphql/schema/build_from_definition"
require "graphql/schema/validator"
require "graphql/schema/member"
require "graphql/schema/wrapper"
require "graphql/schema/list"
require "graphql/schema/non_null"
require "graphql/schema/argument"
require "graphql/schema/enum_value"
require "graphql/schema/enum"
require "graphql/schema/field_extension"
require "graphql/schema/field"
require "graphql/schema/input_object"
require "graphql/schema/interface"
require "graphql/schema/scalar"
require "graphql/schema/object"
require "graphql/schema/union"
require "graphql/schema/directive"
require "graphql/schema/directive/deprecated"
require "graphql/schema/directive/include"
require "graphql/schema/directive/one_of"
require "graphql/schema/directive/skip"
require "graphql/schema/directive/feature"
require "graphql/schema/directive/flagged"
require "graphql/schema/directive/transform"
require "graphql/schema/directive/specified_by"
require "graphql/schema/type_membership"
require "graphql/schema/resolver"
require "graphql/schema/mutation"
require "graphql/schema/has_single_input_argument"
require "graphql/schema/relay_classic_mutation"
require "graphql/schema/subscription"
module GraphQL
# A GraphQL schema which may be queried with {GraphQL::Query}.
#
# The {Schema} contains:
#
# - types for exposing your application
# - query analyzers for assessing incoming queries (including max depth & max complexity restrictions)
# - execution strategies for running incoming queries
#
# Schemas start with root types, {Schema#query}, {Schema#mutation} and {Schema#subscription}.
# The schema will traverse the tree of fields & types, using those as starting points.
# Any undiscoverable types may be provided with the `types` configuration.
#
# Schemas can restrict large incoming queries with `max_depth` and `max_complexity` configurations.
# (These configurations can be overridden by specific calls to {Schema#execute})
#
# Schemas can specify how queries should be executed against them.
# `query_execution_strategy`, `mutation_execution_strategy` and `subscription_execution_strategy`
# each apply to corresponding root types.
#
# @example defining a schema
# class MySchema < GraphQL::Schema
# query QueryType
# # If types are only connected by way of interfaces, they must be added here
# orphan_types ImageType, AudioType
# end
#
class Schema
extend GraphQL::Schema::Member::HasAstNode
extend GraphQL::Schema::FindInheritedValue
class DuplicateNamesError < GraphQL::Error
attr_reader :duplicated_name
def initialize(duplicated_name:, duplicated_definition_1:, duplicated_definition_2:)
@duplicated_name = duplicated_name
super(
"Found two visible definitions for `#{duplicated_name}`: #{duplicated_definition_1}, #{duplicated_definition_2}"
)
end
end
class UnresolvedLateBoundTypeError < GraphQL::Error
attr_reader :type
def initialize(type:)
@type = type
super("Late bound type was never found: #{type.inspect}")
end
end
# Error that is raised when [#Schema#from_definition] is passed an invalid schema definition string.
class InvalidDocumentError < Error; end;
class << self
# Create schema with the result of an introspection query.
# @param introspection_result [Hash] A response from {GraphQL::Introspection::INTROSPECTION_QUERY}
# @return [Class<GraphQL::Schema>] the schema described by `input`
def from_introspection(introspection_result)
GraphQL::Schema::Loader.load(introspection_result)
end
# Create schema from an IDL schema or file containing an IDL definition.
# @param definition_or_path [String] A schema definition string, or a path to a file containing the definition
# @param default_resolve [<#call(type, field, obj, args, ctx)>] A callable for handling field resolution
# @param parser [Object] An object for handling definition string parsing (must respond to `parse`)
# @param using [Hash] Plugins to attach to the created schema with `use(key, value)`
# @return [Class] the schema described by `document`
def from_definition(definition_or_path, default_resolve: nil, parser: GraphQL.default_parser, using: {})
# If the file ends in `.graphql` or `.graphqls`, treat it like a filepath
if definition_or_path.end_with?(".graphql") || definition_or_path.end_with?(".graphqls")
GraphQL::Schema::BuildFromDefinition.from_definition_path(
self,
definition_or_path,
default_resolve: default_resolve,
parser: parser,
using: using,
)
else
GraphQL::Schema::BuildFromDefinition.from_definition(
self,
definition_or_path,
default_resolve: default_resolve,
parser: parser,
using: using,
)
end
end
def deprecated_graphql_definition
graphql_definition(silence_deprecation_warning: true)
end
# @return [GraphQL::Subscriptions]
def subscriptions(inherited: true)
defined?(@subscriptions) ? @subscriptions : (inherited ? find_inherited_value(:subscriptions, nil) : nil)
end
def subscriptions=(new_implementation)
@subscriptions = new_implementation
end
def trace_class(new_class = nil)
if new_class
trace_mode(:default, new_class)
backtrace_class = Class.new(new_class)
backtrace_class.include(GraphQL::Backtrace::Trace)
trace_mode(:default_backtrace, backtrace_class)
end
trace_class_for(:default)
end
# @return [Class] Return the trace class to use for this mode, looking one up on the superclass if this Schema doesn't have one defined.
def trace_class_for(mode)
@trace_modes ||= {}
@trace_modes[mode] ||= begin
case mode
when :default
superclass_base_class = if superclass.respond_to?(:trace_class_for)
superclass.trace_class_for(mode)
else
GraphQL::Tracing::Trace
end
Class.new(superclass_base_class)
when :default_backtrace
schema_base_class = trace_class_for(:default)
Class.new(schema_base_class) do
include(GraphQL::Backtrace::Trace)
end
else
mods = trace_modules_for(mode)
Class.new(trace_class_for(:default)) do
mods.any? && include(*mods)
end
end
end
end
# Configure `trace_class` to be used whenever `context: { trace_mode: mode_name }` is requested.
# `:default` is used when no `trace_mode: ...` is requested.
# @param mode_name [Symbol]
# @param trace_class [Class] subclass of GraphQL::Tracing::Trace
# @return void
def trace_mode(mode_name, trace_class)
@trace_modes ||= {}
@trace_modes[mode_name] = trace_class
nil
end
def own_trace_modules
@own_trace_modules ||= Hash.new { |h, k| h[k] = [] }
end
# @return [Array<Module>] Modules added for tracing in `trace_mode`, including inherited ones
def trace_modules_for(trace_mode)
modules = own_trace_modules[trace_mode]
if superclass.respond_to?(:trace_modules_for)
modules += superclass.trace_modules_for(trace_mode)
end
modules
end
# Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
# @see {#as_json}
# @return [String]
def to_json(**args)
JSON.pretty_generate(as_json(**args))
end
# Return the Hash response of {Introspection::INTROSPECTION_QUERY}.
# @param context [Hash]
# @param only [<#call(member, ctx)>]
# @param except [<#call(member, ctx)>]
# @param include_deprecated_args [Boolean] If true, deprecated arguments will be included in the JSON response
# @param include_schema_description [Boolean] If true, the schema's description will be queried and included in the response
# @param include_is_repeatable [Boolean] If true, `isRepeatable: true|false` will be included with the schema's directives
# @param include_specified_by_url [Boolean] If true, scalar types' `specifiedByUrl:` will be included in the response
# @param include_is_one_of [Boolean] If true, `isOneOf: true|false` will be included with input objects
# @return [Hash] GraphQL result
def as_json(context: {}, include_deprecated_args: true, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false, include_is_one_of: false)
introspection_query = Introspection.query(
include_deprecated_args: include_deprecated_args,
include_schema_description: include_schema_description,
include_is_repeatable: include_is_repeatable,
include_is_one_of: include_is_one_of,
include_specified_by_url: include_specified_by_url,
)
execute(introspection_query, context: context).to_h
end
# Return the GraphQL IDL for the schema
# @param context [Hash]
# @return [String]
def to_definition(context: {})
GraphQL::Schema::Printer.print_schema(self, context: context)
end
# Return the GraphQL::Language::Document IDL AST for the schema
# @return [GraphQL::Language::Document]
def to_document
GraphQL::Language::DocumentFromSchemaDefinition.new(self).document
end
# @return [String, nil]
def description(new_description = nil)
if new_description
@description = new_description
elsif defined?(@description)
@description
else
find_inherited_value(:description, nil)
end
end
def find(path)
if !@finder
@find_cache = {}
@finder ||= GraphQL::Schema::Finder.new(self)
end
@find_cache[path] ||= @finder.find(path)
end
def static_validator
GraphQL::StaticValidation::Validator.new(schema: self)
end
def use(plugin, **kwargs)
if kwargs.any?
plugin.use(self, **kwargs)
else
plugin.use(self)
end
own_plugins << [plugin, kwargs]
end
def plugins
find_inherited_value(:plugins, EMPTY_ARRAY) + own_plugins
end
# Build a map of `{ name => type }` and return it
# @return [Hash<String => Class>] A dictionary of type classes by their GraphQL name
# @see get_type Which is more efficient for finding _one type_ by name, because it doesn't merge hashes.
def types(context = GraphQL::Query::NullContext)
all_types = non_introspection_types.merge(introspection_system.types)
visible_types = {}
all_types.each do |k, v|
visible_types[k] =if v.is_a?(Array)
visible_t = nil
v.each do |t|
if t.visible?(context)
if visible_t.nil?
visible_t = t
else
raise DuplicateNamesError.new(
duplicated_name: k, duplicated_definition_1: visible_t.inspect, duplicated_definition_2: t.inspect
)
end
end
end
visible_t
else
v
end
end
visible_types
end
# @param type_name [String]
# @return [Module, nil] A type, or nil if there's no type called `type_name`
def get_type(type_name, context = GraphQL::Query::NullContext)
local_entry = own_types[type_name]
type_defn = case local_entry
when nil
nil
when Array
visible_t = nil
warden = Warden.from_context(context)
local_entry.each do |t|
if warden.visible_type?(t, context)
if visible_t.nil?
visible_t = t
else
raise DuplicateNamesError.new(
duplicated_name: type_name, duplicated_definition_1: visible_t.inspect, duplicated_definition_2: t.inspect
)
end
end
end
visible_t
when Module
local_entry
else
raise "Invariant: unexpected own_types[#{type_name.inspect}]: #{local_entry.inspect}"
end
type_defn ||
introspection_system.types[type_name] || # todo context-specific introspection?
(superclass.respond_to?(:get_type) ? superclass.get_type(type_name, context) : nil)
end
# @api private
attr_writer :connections
# @return [GraphQL::Pagination::Connections] if installed
def connections
if defined?(@connections)
@connections
else
inherited_connections = find_inherited_value(:connections, nil)
# This schema is part of an inheritance chain which is using new connections,
# make a new instance, so we don't pollute the upstream one.
if inherited_connections
@connections = Pagination::Connections.new(schema: self)
else
nil
end
end
end
def new_connections?
!!connections
end
def query(new_query_object = nil)
if new_query_object
if @query_object
raise GraphQL::Error, "Second definition of `query(...)` (#{new_query_object.inspect}) is invalid, already configured with #{@query_object.inspect}"
else
@query_object = new_query_object
add_type_and_traverse(new_query_object, root: true)
nil
end
else
@query_object || find_inherited_value(:query)
end
end
def mutation(new_mutation_object = nil)
if new_mutation_object
if @mutation_object
raise GraphQL::Error, "Second definition of `mutation(...)` (#{new_mutation_object.inspect}) is invalid, already configured with #{@mutation_object.inspect}"
else
@mutation_object = new_mutation_object
add_type_and_traverse(new_mutation_object, root: true)
nil
end
else
@mutation_object || find_inherited_value(:mutation)
end
end
def subscription(new_subscription_object = nil)
if new_subscription_object
if @subscription_object
raise GraphQL::Error, "Second definition of `subscription(...)` (#{new_subscription_object.inspect}) is invalid, already configured with #{@subscription_object.inspect}"
else
@subscription_object = new_subscription_object
add_subscription_extension_if_necessary
add_type_and_traverse(new_subscription_object, root: true)
nil
end
else
@subscription_object || find_inherited_value(:subscription)
end
end
# @see [GraphQL::Schema::Warden] Restricted access to root types
# @return [GraphQL::ObjectType, nil]
def root_type_for_operation(operation)
case operation
when "query"
query
when "mutation"
mutation
when "subscription"
subscription
else
raise ArgumentError, "unknown operation type: #{operation}"
end
end
def root_types
@root_types
end
def warden_class
if defined?(@warden_class)
@warden_class
elsif superclass.respond_to?(:warden_class)
superclass.warden_class
else
GraphQL::Schema::Warden
end
end
attr_writer :warden_class
# @param type [Module] The type definition whose possible types you want to see
# @return [Hash<String, Module>] All possible types, if no `type` is given.
# @return [Array<Module>] Possible types for `type`, if it's given.
def possible_types(type = nil, context = GraphQL::Query::NullContext)
if type
# TODO duck-typing `.possible_types` would probably be nicer here
if type.kind.union?
type.possible_types(context: context)
else
stored_possible_types = own_possible_types[type.graphql_name]
visible_possible_types = if stored_possible_types && type.kind.interface?
stored_possible_types.select do |possible_type|
possible_type.interfaces(context).include?(type)
end
else
stored_possible_types
end
visible_possible_types ||
introspection_system.possible_types[type.graphql_name] ||
(
superclass.respond_to?(:possible_types) ?
superclass.possible_types(type, context) :
EMPTY_ARRAY
)
end
else
find_inherited_value(:possible_types, EMPTY_HASH)
.merge(own_possible_types)
.merge(introspection_system.possible_types)
end
end
def union_memberships(type = nil)
if type
own_um = own_union_memberships.fetch(type.graphql_name, EMPTY_ARRAY)
inherited_um = find_inherited_value(:union_memberships, EMPTY_HASH).fetch(type.graphql_name, EMPTY_ARRAY)
own_um + inherited_um
else
joined_um = own_union_memberships.dup
find_inherited_value(:union_memberhips, EMPTY_HASH).each do |k, v|
um = joined_um[k] ||= []
um.concat(v)
end
joined_um
end
end
# @api private
# @see GraphQL::Dataloader
def dataloader_class
@dataloader_class || GraphQL::Dataloader::NullDataloader
end
attr_writer :dataloader_class
def references_to(to_type = nil, from: nil)
@own_references_to ||= Hash.new { |h, k| h[k] = [] }
if to_type
if !to_type.is_a?(String)
to_type = to_type.graphql_name
end
if from
@own_references_to[to_type] << from
else
own_refs = @own_references_to[to_type]
inherited_refs = find_inherited_value(:references_to, EMPTY_HASH)[to_type] || EMPTY_ARRAY
own_refs + inherited_refs
end
else
# `@own_references_to` can be quite large for big schemas,
# and generally speaking, we won't inherit any values.
# So optimize the most common case -- don't create a duplicate Hash.
inherited_value = find_inherited_value(:references_to, EMPTY_HASH)
if inherited_value.any?
inherited_value.merge(@own_references_to)
else
@own_references_to
end
end
end
def type_from_ast(ast_node, context: nil)
type_owner = context ? context.warden : self
GraphQL::Schema::TypeExpression.build_type(type_owner, ast_node)
end
def get_field(type_or_name, field_name, context = GraphQL::Query::NullContext)
parent_type = case type_or_name
when LateBoundType
get_type(type_or_name.name, context)
when String
get_type(type_or_name, context)
when Module
type_or_name
else
raise GraphQL::InvariantError, "Unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})"
end
if parent_type.kind.fields? && (field = parent_type.get_field(field_name, context))
field
elsif parent_type == query && (entry_point_field = introspection_system.entry_point(name: field_name))
entry_point_field
elsif (dynamic_field = introspection_system.dynamic_field(name: field_name))
dynamic_field
else
nil
end
end
def get_fields(type, context = GraphQL::Query::NullContext)
type.fields(context)
end
def introspection(new_introspection_namespace = nil)
if new_introspection_namespace
@introspection = new_introspection_namespace
# reset this cached value:
@introspection_system = nil
else
@introspection || find_inherited_value(:introspection)
end
end
def introspection_system
if !@introspection_system
@introspection_system = Schema::IntrospectionSystem.new(self)
@introspection_system.resolve_late_bindings
end
@introspection_system
end
def cursor_encoder(new_encoder = nil)
if new_encoder
@cursor_encoder = new_encoder
end
@cursor_encoder || find_inherited_value(:cursor_encoder, Base64Encoder)
end
def default_max_page_size(new_default_max_page_size = nil)
if new_default_max_page_size
@default_max_page_size = new_default_max_page_size
else
@default_max_page_size || find_inherited_value(:default_max_page_size)
end
end
def default_page_size(new_default_page_size = nil)
if new_default_page_size
@default_page_size = new_default_page_size
else
@default_page_size || find_inherited_value(:default_page_size)
end
end
def query_execution_strategy(new_query_execution_strategy = nil)
if new_query_execution_strategy
@query_execution_strategy = new_query_execution_strategy
else
@query_execution_strategy || find_inherited_value(:query_execution_strategy, self.default_execution_strategy)
end
end
def mutation_execution_strategy(new_mutation_execution_strategy = nil)
if new_mutation_execution_strategy
@mutation_execution_strategy = new_mutation_execution_strategy
else
@mutation_execution_strategy || find_inherited_value(:mutation_execution_strategy, self.default_execution_strategy)
end
end
def subscription_execution_strategy(new_subscription_execution_strategy = nil)
if new_subscription_execution_strategy
@subscription_execution_strategy = new_subscription_execution_strategy
else
@subscription_execution_strategy || find_inherited_value(:subscription_execution_strategy, self.default_execution_strategy)
end
end
attr_writer :validate_timeout
def validate_timeout(new_validate_timeout = nil)
if new_validate_timeout
@validate_timeout = new_validate_timeout
elsif defined?(@validate_timeout)
@validate_timeout
else
find_inherited_value(:validate_timeout)
end
end
# Validate a query string according to this schema.
# @param string_or_document [String, GraphQL::Language::Nodes::Document]
# @return [Array<GraphQL::StaticValidation::Error >]
def validate(string_or_document, rules: nil, context: nil)
doc = if string_or_document.is_a?(String)
GraphQL.parse(string_or_document)
else
string_or_document
end
query = GraphQL::Query.new(self, document: doc, context: context)
validator_opts = { schema: self }
rules && (validator_opts[:rules] = rules)
validator = GraphQL::StaticValidation::Validator.new(**validator_opts)
res = validator.validate(query, timeout: validate_timeout, max_errors: validate_max_errors)
res[:errors]
end
attr_writer :validate_max_errors
def validate_max_errors(new_validate_max_errors = nil)
if new_validate_max_errors
@validate_max_errors = new_validate_max_errors
elsif defined?(@validate_max_errors)
@validate_max_errors
else
find_inherited_value(:validate_max_errors)
end
end
attr_writer :max_complexity
def max_complexity(max_complexity = nil)
if max_complexity
@max_complexity = max_complexity
elsif defined?(@max_complexity)
@max_complexity
else
find_inherited_value(:max_complexity)
end
end
attr_writer :analysis_engine
def analysis_engine
@analysis_engine || find_inherited_value(:analysis_engine, self.default_analysis_engine)
end
def using_ast_analysis?
true
end
def interpreter?
true
end
attr_writer :interpreter
def error_bubbling(new_error_bubbling = nil)
if !new_error_bubbling.nil?
@error_bubbling = new_error_bubbling
else
@error_bubbling.nil? ? find_inherited_value(:error_bubbling) : @error_bubbling
end
end
attr_writer :error_bubbling
attr_writer :max_depth
def max_depth(new_max_depth = nil)
if new_max_depth
@max_depth = new_max_depth
elsif defined?(@max_depth)
@max_depth
else
find_inherited_value(:max_depth)
end
end
def disable_introspection_entry_points
@disable_introspection_entry_points = true
# TODO: this clears the cache made in `def types`. But this is not a great solution.
@introspection_system = nil
end
def disable_schema_introspection_entry_point
@disable_schema_introspection_entry_point = true
# TODO: this clears the cache made in `def types`. But this is not a great solution.
@introspection_system = nil
end
def disable_type_introspection_entry_point
@disable_type_introspection_entry_point = true
# TODO: this clears the cache made in `def types`. But this is not a great solution.
@introspection_system = nil
end
def disable_introspection_entry_points?
if instance_variable_defined?(:@disable_introspection_entry_points)
@disable_introspection_entry_points
else
find_inherited_value(:disable_introspection_entry_points?, false)
end
end
def disable_schema_introspection_entry_point?
if instance_variable_defined?(:@disable_schema_introspection_entry_point)
@disable_schema_introspection_entry_point
else
find_inherited_value(:disable_schema_introspection_entry_point?, false)
end
end
def disable_type_introspection_entry_point?
if instance_variable_defined?(:@disable_type_introspection_entry_point)
@disable_type_introspection_entry_point
else
find_inherited_value(:disable_type_introspection_entry_point?, false)
end
end
def orphan_types(*new_orphan_types)
if new_orphan_types.any?
new_orphan_types = new_orphan_types.flatten
add_type_and_traverse(new_orphan_types, root: false)
own_orphan_types.concat(new_orphan_types.flatten)
end
inherited_ot = find_inherited_value(:orphan_types, nil)
if inherited_ot
if own_orphan_types.any?
inherited_ot + own_orphan_types
else
inherited_ot
end
else
own_orphan_types
end
end
def default_execution_strategy
if superclass <= GraphQL::Schema
superclass.default_execution_strategy
else
@default_execution_strategy ||= GraphQL::Execution::Interpreter
end
end
def default_analysis_engine
if superclass <= GraphQL::Schema
superclass.default_analysis_engine
else
@default_analysis_engine ||= GraphQL::Analysis::AST
end
end
def context_class(new_context_class = nil)
if new_context_class
@context_class = new_context_class
else
@context_class || find_inherited_value(:context_class, GraphQL::Query::Context)
end
end
def rescue_from(*err_classes, &handler_block)
err_classes.each do |err_class|
Execution::Errors.register_rescue_from(err_class, error_handlers[:subclass_handlers], handler_block)
end
end
NEW_HANDLER_HASH = ->(h, k) {
h[k] = {
class: k,
handler: nil,
subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
}
}
def error_handlers
@error_handlers ||= {
class: nil,
handler: nil,
subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
}
end
# @api private
def handle_or_reraise(context, err)
handler = Execution::Errors.find_handler_for(self, err.class)
if handler
obj = context[:current_object]
args = context[:current_arguments]
args = args && args.respond_to?(:keyword_arguments) ? args.keyword_arguments : nil
field = context[:current_field]
if obj.is_a?(GraphQL::Schema::Object)
obj = obj.object
end
handler[:handler].call(err, obj, args, context, field)
else
raise err
end
end
# rubocop:disable Lint/DuplicateMethods
module ResolveTypeWithType
def resolve_type(type, obj, ctx)
maybe_lazy_resolve_type_result = if type.is_a?(Module) && type.respond_to?(:resolve_type)
type.resolve_type(obj, ctx)
else
super
end
after_lazy(maybe_lazy_resolve_type_result) do |resolve_type_result|
if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2
resolved_type = resolve_type_result[0]
resolved_value = resolve_type_result[1]
else
resolved_type = resolve_type_result
resolved_value = obj
end
if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind))
[resolved_type, resolved_value]
else
raise ".resolve_type should return a type definition, but got #{resolved_type.inspect} (#{resolved_type.class}) from `resolve_type(#{type}, #{obj}, #{ctx})`"
end
end
end
end
def resolve_type(type, obj, ctx)
if type.kind.object?
type
else
raise GraphQL::RequiredImplementationMissingError, "#{self.name}.resolve_type(type, obj, ctx) must be implemented to use Union types or Interface types (tried to resolve: #{type.name})"
end
end
# rubocop:enable Lint/DuplicateMethods
def inherited(child_class)
if self == GraphQL::Schema
child_class.directives(default_directives.values)
end
child_class.singleton_class.prepend(ResolveTypeWithType)
super
end
def object_from_id(node_id, ctx)
raise GraphQL::RequiredImplementationMissingError, "#{self.name}.object_from_id(node_id, ctx) must be implemented to load by ID (tried to load from id `#{node_id}`)"
end
def id_from_object(object, type, ctx)
raise GraphQL::RequiredImplementationMissingError, "#{self.name}.id_from_object(object, type, ctx) must be implemented to create global ids (tried to create an id for `#{object.inspect}`)"
end
def visible?(member, ctx)
member.visible?(ctx)
end
def schema_directive(dir_class, **options)
@own_schema_directives ||= []
Member::HasDirectives.add_directive(self, @own_schema_directives, dir_class, options)
end
def schema_directives
Member::HasDirectives.get_directives(self, @own_schema_directives, :schema_directives)
end
# This hook is called when an object fails an `authorized?` check.
# You might report to your bug tracker here, so you can correct
# the field resolvers not to return unauthorized objects.
#
# By default, this hook just replaces the unauthorized object with `nil`.
#
# Whatever value is returned from this method will be used instead of the
# unauthorized object (accessible as `unauthorized_error.object`). If an
# error is raised, then `nil` will be used.
#
# If you want to add an error to the `"errors"` key, raise a {GraphQL::ExecutionError}
# in this hook.
#
# @param unauthorized_error [GraphQL::UnauthorizedError]
# @return [Object] The returned object will be put in the GraphQL response
def unauthorized_object(unauthorized_error)
nil
end
# This hook is called when a field fails an `authorized?` check.
#
# By default, this hook implements the same behavior as unauthorized_object.
#
# Whatever value is returned from this method will be used instead of the
# unauthorized field . If an error is raised, then `nil` will be used.
#
# If you want to add an error to the `"errors"` key, raise a {GraphQL::ExecutionError}
# in this hook.
#
# @param unauthorized_error [GraphQL::UnauthorizedFieldError]
# @return [Field] The returned field will be put in the GraphQL response
def unauthorized_field(unauthorized_error)
unauthorized_object(unauthorized_error)
end
def type_error(type_error, ctx)
case type_error
when GraphQL::InvalidNullError
ctx.errors << type_error
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
raise type_error
when GraphQL::IntegerDecodingError
nil
end
end
# A function to call when {#execute} receives an invalid query string
#
# The default is to add the error to `context.errors`
# @param parse_err [GraphQL::ParseError] The error encountered during parsing
# @param ctx [GraphQL::Query::Context] The context for the query where the error occurred
# @return void
def parse_error(parse_err, ctx)
ctx.errors.push(parse_err)
end
def lazy_resolve(lazy_class, value_method)
lazy_methods.set(lazy_class, value_method)
end
def instrument(instrument_step, instrumenter, options = {})
own_instrumenters[instrument_step] << instrumenter
end
# Add several directives at once
# @param new_directives [Class]
def directives(*new_directives)
if new_directives.any?
new_directives.flatten.each { |d| directive(d) }
end
inherited_dirs = find_inherited_value(:directives, default_directives)
if own_directives.any?
inherited_dirs.merge(own_directives)
else
inherited_dirs
end
end
# Attach a single directive to this schema
# @param new_directive [Class]
# @return void
def directive(new_directive)
add_type_and_traverse(new_directive, root: false)
end
def default_directives
@default_directives ||= {
"include" => GraphQL::Schema::Directive::Include,
"skip" => GraphQL::Schema::Directive::Skip,
"deprecated" => GraphQL::Schema::Directive::Deprecated,
"oneOf" => GraphQL::Schema::Directive::OneOf,
"specifiedBy" => GraphQL::Schema::Directive::SpecifiedBy,
}.freeze
end
def tracer(new_tracer)
if !(trace_class_for(:default) < GraphQL::Tracing::CallLegacyTracers)
trace_with(GraphQL::Tracing::CallLegacyTracers)
end
own_tracers << new_tracer
end
def tracers
find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers
end
# Mix `trace_mod` into this schema's `Trace` class so that its methods
# will be called at runtime.
#
# @param trace_mod [Module] A module that implements tracing methods
# @param mode [Symbol] Trace module will only be used for this trade mode
# @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
# @return [void]
def trace_with(trace_mod, mode: :default, **options)
if mode.is_a?(Array)
mode.each { |m| trace_with(trace_mod, mode: m, **options) }
else
tc = trace_class_for(mode)
tc.include(trace_mod)
if mode != :default
own_trace_modules[mode] << trace_mod
end
t_opts = trace_options_for(mode)
t_opts.merge!(options)
end
nil
end
# The options hash for this trace mode
# @return [Hash]
def trace_options_for(mode)
@trace_options_for_mode ||= {}
@trace_options_for_mode[mode] ||= begin
if superclass.respond_to?(:trace_options_for)
superclass.trace_options_for(mode).dup
else
{}
end
end
end
# Create a trace instance which will include the trace modules specified for the optional mode.
#
# @param mode [Symbol] Trace modules for this trade mode will be included
# @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
# @return [Tracing::Trace]
def new_trace(mode: nil, **options)
target = options[:query] || options[:multiplex]
mode ||= target && target.context[:trace_mode]
trace_mode = if mode
mode
elsif target && target.context[:backtrace]
:default_backtrace
else
:default
end
base_trace_options = trace_options_for(trace_mode)
trace_options = base_trace_options.merge(options)
trace_class_for_mode = trace_class_for(trace_mode)
trace_class_for_mode.new(**trace_options)
end
def query_analyzer(new_analyzer)
own_query_analyzers << new_analyzer
end
def query_analyzers
find_inherited_value(:query_analyzers, EMPTY_ARRAY) + own_query_analyzers
end
def multiplex_analyzer(new_analyzer)
own_multiplex_analyzers << new_analyzer
end
def multiplex_analyzers
find_inherited_value(:multiplex_analyzers, EMPTY_ARRAY) + own_multiplex_analyzers
end
def sanitized_printer(new_sanitized_printer = nil)
if new_sanitized_printer
@own_sanitized_printer = new_sanitized_printer
else
@own_sanitized_printer || GraphQL::Language::SanitizedPrinter
end
end
# Execute a query on itself.
# @see {Query#initialize} for arguments.
# @return [Hash] query result, ready to be serialized as JSON
def execute(query_str = nil, **kwargs)
if query_str
kwargs[:query] = query_str
end
# Some of the query context _should_ be passed to the multiplex, too
multiplex_context = if (ctx = kwargs[:context])
{
backtrace: ctx[:backtrace],
tracers: ctx[:tracers],
trace: ctx[:trace],
dataloader: ctx[:dataloader],
trace_mode: ctx[:trace_mode],
}
else
{}
end
# Since we're running one query, don't run a multiplex-level complexity analyzer
all_results = multiplex([kwargs], max_complexity: nil, context: multiplex_context)
all_results[0]
end
# Execute several queries on itself, concurrently.
#
# @example Run several queries at once
# context = { ... }
# queries = [
# { query: params[:query_1], variables: params[:variables_1], context: context },
# { query: params[:query_2], variables: params[:variables_2], context: context },
# ]
# results = MySchema.multiplex(queries)
# render json: {
# result_1: results[0],
# result_2: results[1],
# }
#
# @see {Query#initialize} for query keyword arguments
# @see {Execution::Multiplex#run_all} for multiplex keyword arguments
# @param queries [Array<Hash>] Keyword arguments for each query
# @param context [Hash] Multiplex-level context
# @return [Array<Hash>] One result for each query in the input
def multiplex(queries, **kwargs)
GraphQL::Execution::Interpreter.run_all(self, queries, **kwargs)
end
def instrumenters
inherited_instrumenters = find_inherited_value(:instrumenters) || Hash.new { |h,k| h[k] = [] }
inherited_instrumenters.merge(own_instrumenters) do |_step, inherited, own|
inherited + own
end
end
# @api private
def add_subscription_extension_if_necessary
if !defined?(@subscription_extension_added) && subscription && self.subscriptions
@subscription_extension_added = true
subscription.all_field_definitions.each do |field|
if !field.extensions.any? { |ext| ext.is_a?(Subscriptions::DefaultSubscriptionResolveExtension) }
field.extension(Subscriptions::DefaultSubscriptionResolveExtension)
end
end
end
end
def query_stack_error(query, err)
query.context.errors.push(GraphQL::ExecutionError.new("This query is too large to execute."))
end
# Call the given block at the right time, either:
# - Right away, if `value` is not registered with `lazy_resolve`
# - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`)
# @api private
def after_lazy(value, &block)
if lazy?(value)
GraphQL::Execution::Lazy.new do
result = sync_lazy(value)
# The returned result might also be lazy, so check it, too
after_lazy(result, &block)
end
else
yield(value) if block_given?
end
end
# Override this method to handle lazy objects in a custom way.
# @param value [Object] an instance of a class registered with {.lazy_resolve}
# @return [Object] A GraphQL-ready (non-lazy) object
# @api private
def sync_lazy(value)
lazy_method = lazy_method_name(value)
if lazy_method
synced_value = value.public_send(lazy_method)
sync_lazy(synced_value)
else
value
end
end
# @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {#lazy_resolve}.
def lazy_method_name(obj)
lazy_methods.get(obj)
end
# @return [Boolean] True if this object should be lazily resolved
def lazy?(obj)
!!lazy_method_name(obj)
end
# Return a lazy if any of `maybe_lazies` are lazy,
# otherwise, call the block eagerly and return the result.
# @param maybe_lazies [Array]
# @api private
def after_any_lazies(maybe_lazies)
if maybe_lazies.any? { |l| lazy?(l) }
GraphQL::Execution::Lazy.all(maybe_lazies).then do |result|
yield result
end
else
yield maybe_lazies
end
end
private
# @param t [Module, Array<Module>]
# @return [void]
def add_type_and_traverse(t, root:)
if root
@root_types ||= []
@root_types << t
end
new_types = Array(t)
addition = Schema::Addition.new(schema: self, own_types: own_types, new_types: new_types)
addition.types.each do |name, types_entry| # rubocop:disable Development/ContextIsPassedCop -- build-time, not query-time
if (prev_entry = own_types[name])
prev_entries = case prev_entry
when Array
prev_entry
when Module
own_types[name] = [prev_entry]
else
raise "Invariant: unexpected prev_entry at #{name.inspect} when adding #{t.inspect}"
end
case types_entry
when Array
prev_entries.concat(types_entry)
prev_entries.uniq! # in case any are being re-visited
when Module
if !prev_entries.include?(types_entry)
prev_entries << types_entry
end
else
raise "Invariant: unexpected types_entry at #{name} when adding #{t.inspect}"
end
else
if types_entry.is_a?(Array)
types_entry.uniq!
end
own_types[name] = types_entry
end
end
own_possible_types.merge!(addition.possible_types) { |key, old_val, new_val| old_val + new_val }
own_union_memberships.merge!(addition.union_memberships)
addition.references.each { |thing, pointers|
pointers.each { |pointer| references_to(thing, from: pointer) }
}
addition.directives.each { |dir_class| own_directives[dir_class.graphql_name] = dir_class }
addition.arguments_with_default_values.each do |arg|
arg.validate_default_value
end
end
def lazy_methods
if !defined?(@lazy_methods)
if inherited_map = find_inherited_value(:lazy_methods)
# this isn't _completely_ inherited :S (Things added after `dup` won't work)
@lazy_methods = inherited_map.dup
else
@lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new
@lazy_methods.set(GraphQL::Execution::Lazy, :value)
@lazy_methods.set(GraphQL::Dataloader::Request, :load)
end
end
@lazy_methods
end
def own_types
@own_types ||= {}
end
def non_introspection_types
find_inherited_value(:non_introspection_types, EMPTY_HASH).merge(own_types)
end
def own_plugins
@own_plugins ||= []
end
def own_orphan_types
@own_orphan_types ||= []
end
def own_possible_types
@own_possible_types ||= {}
end
def own_union_memberships
@own_union_memberships ||= {}
end
def own_directives
@own_directives ||= {}
end
def own_instrumenters
@own_instrumenters ||= Hash.new { |h,k| h[k] = [] }
end
def own_tracers
@own_tracers ||= []
end
def own_query_analyzers
@defined_query_analyzers ||= []
end
def own_multiplex_analyzers
@own_multiplex_analyzers ||= []
end
end
# Install these here so that subclasses will also install it.
self.connections = GraphQL::Pagination::Connections.new(schema: self)
end
end
```
> NOTE [zone]: Demo cluster `nimbus-4` is pinned to availability zone `zone-delta-2`.
### oss/graphql-ruby/lib/graphql/static_validation.rb
```ruby
# frozen_string_literal: true
require "graphql/static_validation/error"
require "graphql/static_validation/definition_dependencies"
require "graphql/static_validation/validator"
require "graphql/static_validation/validation_context"
require "graphql/static_validation/validation_timeout_error"
require "graphql/static_validation/literal_validator"
require "graphql/static_validation/base_visitor"
rules_glob = File.expand_path("../static_validation/rules/*.rb", __FILE__)
Dir.glob(rules_glob).each do |file|
require(file)
end
require "graphql/static_validation/all_rules"
require "graphql/static_validation/interpreter_visitor"
```
### oss/graphql-ruby/lib/graphql/string_encoding_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
class StringEncodingError < GraphQL::RuntimeTypeError
attr_reader :string, :field, :path
def initialize(str, context:)
@string = str
@field = context[:current_field]
@path = context[:current_path]
message = "String #{str.inspect} was encoded as #{str.encoding}".dup
if @path
message << " @ #{@path.join(".")}"
end
if @field
message << " (#{@field.path})"
end
message << ". GraphQL requires an encoding compatible with UTF-8."
super(message)
end
end
end
```
### oss/graphql-ruby/lib/graphql/subscriptions.rb
```ruby
# frozen_string_literal: true
require "securerandom"
require "graphql/subscriptions/broadcast_analyzer"
require "graphql/subscriptions/event"
require "graphql/subscriptions/instrumentation"
require "graphql/subscriptions/serialize"
require "graphql/subscriptions/action_cable_subscriptions"
require "graphql/subscriptions/default_subscription_resolve_extension"
module GraphQL
class Subscriptions
# Raised when either:
# - the triggered `event_name` doesn't match a field in the schema; or
# - one or more arguments don't match the field arguments
class InvalidTriggerError < GraphQL::Error
end
# Raised when either:
# - An initial subscription didn't have a value for `context[subscription_scope]`
# - Or, an update didn't pass `.trigger(..., scope:)`
# When raised, the initial subscription or update fails completely.
class SubscriptionScopeMissingError < GraphQL::Error
end
# @see {Subscriptions#initialize} for options, concrete implementations may add options.
def self.use(defn, options = {})
schema = defn.is_a?(Class) ? defn : defn.target
if schema.subscriptions(inherited: false)
raise ArgumentError, "Can't reinstall subscriptions. #{schema} is using #{schema.subscriptions}, can't also add #{self}"
end
instrumentation = Subscriptions::Instrumentation.new(schema: schema)
defn.instrument(:query, instrumentation)
options[:schema] = schema
schema.subscriptions = self.new(**options)
schema.add_subscription_extension_if_necessary
nil
end
# @param schema [Class] the GraphQL schema this manager belongs to
# @param validate_update [Boolean] If false, then validation is skipped when executing updates
def initialize(schema:, validate_update: true, broadcast: false, default_broadcastable: false, **rest)
if broadcast
schema.query_analyzer(Subscriptions::BroadcastAnalyzer)
end
@default_broadcastable = default_broadcastable
@schema = schema
@validate_update = validate_update
end
# @return [Boolean] Used when fields don't have `broadcastable:` explicitly set
attr_reader :default_broadcastable
# Fetch subscriptions matching this field + arguments pair
# And pass them off to the queue.
# @param event_name [String]
# @param args [Hash<String, Symbol => Object]
# @param object [Object]
# @param scope [Symbol, String]
# @param context [Hash]
# @return [void]
def trigger(event_name, args, object, scope: nil, context: {})
# Make something as context-like as possible, even though there isn't a current query:
dummy_query = GraphQL::Query.new(@schema, "", validate: false, context: context)
context = dummy_query.context
event_name = event_name.to_s
# Try with the verbatim input first:
field = @schema.get_field(@schema.subscription, event_name, context)
if field.nil?
# And if it wasn't found, normalize it:
normalized_event_name = normalize_name(event_name)
field = @schema.get_field(@schema.subscription, normalized_event_name, context)
if field.nil?
raise InvalidTriggerError, "No subscription matching trigger: #{event_name} (looked for #{@schema.subscription.graphql_name}.#{normalized_event_name})"
end
else
# Since we found a field, the original input was already normalized
normalized_event_name = event_name
end
# Normalize symbol-keyed args to strings, try camelizing them
# Should this accept a real context somehow?
normalized_args = normalize_arguments(normalized_event_name, field, args, GraphQL::Query::NullContext)
event = Subscriptions::Event.new(
name: normalized_event_name,
arguments: normalized_args,
field: field,
scope: scope,
context: context,
)
execute_all(event, object)
end
# `event` was triggered on `object`, and `subscription_id` was subscribed,
# so it should be updated.
#
# Load `subscription_id`'s GraphQL data, re-evaluate the query and return the result.
#
# @param subscription_id [String]
# @param event [GraphQL::Subscriptions::Event] The event which was triggered
# @param object [Object] The value for the subscription field
# @return [GraphQL::Query::Result]
def execute_update(subscription_id, event, object)
# Lookup the saved data for this subscription
query_data = read_subscription(subscription_id)
if query_data.nil?
delete_subscription(subscription_id)
return nil
end
# Fetch the required keys from the saved data
query_string = query_data.fetch(:query_string)
variables = query_data.fetch(:variables)
context = query_data.fetch(:context)
operation_name = query_data.fetch(:operation_name)
execute_options = {
query: query_string,
context: context,
subscription_topic: event.topic,
operation_name: operation_name,
variables: variables,
root_value: object,
}
# merge event's and query's context together
context.merge!(event.context) unless event.context.nil? || context.nil?
execute_options[:validate] = validate_update?(**execute_options)
result = @schema.execute(**execute_options)
subscriptions_context = result.context.namespace(:subscriptions)
if subscriptions_context[:no_update]
result = nil
end
if subscriptions_context[:unsubscribed] && !subscriptions_context[:final_update]
# `unsubscribe` was called, clean up on our side
# The transport should also send `{more: false}` to client
delete_subscription(subscription_id)
result = nil
end
result
end
# Define this method to customize whether to validate
# this subscription when executing an update.
#
# @return [Boolean] defaults to `true`, or false if `validate: false` is provided.
def validate_update?(query:, context:, root_value:, subscription_topic:, operation_name:, variables:)
@validate_update
end
# Run the update query for this subscription and deliver it
# @see {#execute_update}
# @see {#deliver}
# @return [void]
def execute(subscription_id, event, object)
res = execute_update(subscription_id, event, object)
if !res.nil?
deliver(subscription_id, res)
if res.context.namespace(:subscriptions)[:unsubscribed]
# `unsubscribe` was called, clean up on our side
# The transport should also send `{more: false}` to client
delete_subscription(subscription_id)
end
end
end
# Event `event` occurred on `object`,
# Update all subscribers.
# @param event [Subscriptions::Event]
# @param object [Object]
# @return [void]
def execute_all(event, object)
raise GraphQL::RequiredImplementationMissingError
end
# The system wants to send an update to this subscription.
# Read its data and return it.
# @param subscription_id [String]
# @return [Hash] Containing required keys
def read_subscription(subscription_id)
raise GraphQL::RequiredImplementationMissingError
end
# A subscription query was re-evaluated, returning `result`.
# The result should be send to `subscription_id`.
# @param subscription_id [String]
# @param result [Hash]
# @return [void]
def deliver(subscription_id, result)
raise GraphQL::RequiredImplementationMissingError
end
# `query` was executed and found subscriptions to `events`.
# Update the database to reflect this new state.
# @param query [GraphQL::Query]
# @param events [Array<GraphQL::Subscriptions::Event>]
# @return [void]
def write_subscription(query, events)
raise GraphQL::RequiredImplementationMissingError
end
# A subscription was terminated server-side.
# Clean up the database.
# @param subscription_id [String]
# @return void.
def delete_subscription(subscription_id)
raise GraphQL::RequiredImplementationMissingError
end
# @return [String] A new unique identifier for a subscription
def build_id
SecureRandom.uuid
end
# Convert a user-provided event name or argument
# to the equivalent in GraphQL.
#
# By default, it converts the identifier to camelcase.
# Override this in a subclass to change the transformation.
#
# @param event_or_arg_name [String, Symbol]
# @return [String]
def normalize_name(event_or_arg_name)
Schema::Member::BuildType.camelize(event_or_arg_name.to_s)
end
# @return [Boolean] if true, then a query like this one would be broadcasted
def broadcastable?(query_str, **query_options)
query = GraphQL::Query.new(@schema, query_str, **query_options)
if !query.valid?
raise "Invalid query: #{query.validation_errors.map(&:to_h).inspect}"
end
GraphQL::Analysis::AST.analyze_query(query, @schema.query_analyzers)
query.context.namespace(:subscriptions)[:subscription_broadcastable]
end
private
# Recursively normalize `args` as belonging to `arg_owner`:
# - convert symbols to strings,
# - if needed, camelize the string (using {#normalize_name})
# @param arg_owner [GraphQL::Field, GraphQL::BaseType]
# @param args [Hash, Array, Any] some GraphQL input value to coerce as `arg_owner`
# @return [Any] normalized arguments value
def normalize_arguments(event_name, arg_owner, args, context)
case arg_owner
when GraphQL::Schema::Field, Class
if arg_owner.is_a?(Class) && !arg_owner.kind.input_object?
# it's a type, but not an input object
return args
end
normalized_args = {}
missing_arg_names = []
args.each do |k, v|
arg_name = k.to_s
arg_defn = arg_owner.get_argument(arg_name, context)
if arg_defn
normalized_arg_name = arg_name
else
normalized_arg_name = normalize_name(arg_name)
arg_defn = arg_owner.get_argument(normalized_arg_name, context)
end
if arg_defn
if arg_defn.loads
normalized_arg_name = arg_defn.keyword.to_s
end
normalized = normalize_arguments(event_name, arg_defn.type, v, context)
normalized_args[normalized_arg_name] = normalized
else
# Couldn't find a matching argument definition
missing_arg_names << arg_name
end
end
# Backfill default values so that trigger arguments
# match query arguments.
arg_owner.arguments(context).each do |_name, arg_defn|
if arg_defn.default_value? && !normalized_args.key?(arg_defn.name)
default_value = arg_defn.default_value
# We don't have an underlying "object" here, so it can't call methods.
# This is broken.
normalized_args[arg_defn.name] = arg_defn.prepare_value(nil, default_value, context: context)
end
end
if missing_arg_names.any?
arg_owner_name = if arg_owner.is_a?(GraphQL::Schema::Field)
arg_owner.path
elsif arg_owner.is_a?(Class)
arg_owner.graphql_name
else
arg_owner.to_s
end
raise InvalidTriggerError, "Can't trigger Subscription.#{event_name}, received undefined arguments: #{missing_arg_names.join(", ")}. (Should match arguments of #{arg_owner_name}.)"
end
normalized_args
when GraphQL::Schema::List
args.map { |a| normalize_arguments(event_name, arg_owner.of_type, a, context) }
when GraphQL::Schema::NonNull
normalize_arguments(event_name, arg_owner.of_type, args, context)
else
args
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/tracing.rb
```ruby
# frozen_string_literal: true
require "graphql/tracing/trace"
require "graphql/tracing/legacy_trace"
# Legacy tracing:
require "graphql/tracing/active_support_notifications_tracing"
require "graphql/tracing/platform_tracing"
require "graphql/tracing/appoptics_tracing"
require "graphql/tracing/appsignal_tracing"
require "graphql/tracing/data_dog_tracing"
require "graphql/tracing/new_relic_tracing"
require "graphql/tracing/scout_tracing"
require "graphql/tracing/statsd_tracing"
require "graphql/tracing/prometheus_tracing"
# New Tracing:
require "graphql/tracing/active_support_notifications_trace"
require "graphql/tracing/platform_trace"
require "graphql/tracing/appoptics_trace"
require "graphql/tracing/appsignal_trace"
require "graphql/tracing/data_dog_trace"
require "graphql/tracing/new_relic_trace"
require "graphql/tracing/notifications_trace"
require "graphql/tracing/scout_trace"
require "graphql/tracing/statsd_trace"
require "graphql/tracing/prometheus_trace"
if defined?(PrometheusExporter::Server)
require "graphql/tracing/prometheus_tracing/graphql_collector"
end
module GraphQL
module Tracing
NullTrace = Trace.new
# Objects may include traceable to gain a `.trace(...)` method.
# The object must have a `@tracers` ivar of type `Array<<#trace(k, d, &b)>>`.
# @api private
module Traceable
# @param key [String] The name of the event in GraphQL internals
# @param metadata [Hash] Event-related metadata (can be anything)
# @return [Object] Must return the value of the block
def trace(key, metadata, &block)
return yield if @tracers.empty?
call_tracers(0, key, metadata, &block)
end
private
# If there's a tracer at `idx`, call it and then increment `idx`.
# Otherwise, yield.
#
# @param idx [Integer] Which tracer to call
# @param key [String] The current event name
# @param metadata [Object] The current event object
# @return Whatever the block returns
def call_tracers(idx, key, metadata, &block)
if idx == @tracers.length
yield
else
@tracers[idx].trace(key, metadata) { call_tracers(idx + 1, key, metadata, &block) }
end
end
end
module NullTracer
module_function
def trace(k, v)
yield
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/type_kinds.rb
```ruby
# frozen_string_literal: true
module GraphQL
# Type kinds are the basic categories which a type may belong to (`Object`, `Scalar`, `Union`...)
module TypeKinds
# These objects are singletons, eg `GraphQL::TypeKinds::UNION`, `GraphQL::TypeKinds::SCALAR`.
class TypeKind
attr_reader :name, :description
def initialize(name, abstract: false, leaf: false, fields: false, wraps: false, input: false, description: nil)
@name = name
@abstract = abstract
@fields = fields
@wraps = wraps
@input = input
@leaf = leaf
@composite = fields? || abstract?
@description = description
end
# Does this TypeKind have multiple possible implementors?
# @deprecated Use `abstract?` instead of `resolves?`.
def resolves?; @abstract; end
# Is this TypeKind abstract?
def abstract?; @abstract; end
# Does this TypeKind have queryable fields?
def fields?; @fields; end
# Does this TypeKind modify another type?
def wraps?; @wraps; end
# Is this TypeKind a valid query input?
def input?; @input; end
def to_s; @name; end
# Is this TypeKind a primitive value?
def leaf?; @leaf; end
# Is this TypeKind composed of many values?
def composite?; @composite; end
def scalar?
self == TypeKinds::SCALAR
end
def object?
self == TypeKinds::OBJECT
end
def interface?
self == TypeKinds::INTERFACE
end
def union?
self == TypeKinds::UNION
end
def enum?
self == TypeKinds::ENUM
end
def input_object?
self == TypeKinds::INPUT_OBJECT
end
def list?
self == TypeKinds::LIST
end
def non_null?
self == TypeKinds::NON_NULL
end
end
TYPE_KINDS = [
SCALAR = TypeKind.new("SCALAR", input: true, leaf: true, description: 'Indicates this type is a scalar.'),
OBJECT = TypeKind.new("OBJECT", fields: true, description: 'Indicates this type is an object. `fields` and `interfaces` are valid fields.'),
INTERFACE = TypeKind.new("INTERFACE", abstract: true, fields: true, description: 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.'),
UNION = TypeKind.new("UNION", abstract: true, description: 'Indicates this type is a union. `possibleTypes` is a valid field.'),
ENUM = TypeKind.new("ENUM", input: true, leaf: true, description: 'Indicates this type is an enum. `enumValues` is a valid field.'),
INPUT_OBJECT = TypeKind.new("INPUT_OBJECT", input: true, description: 'Indicates this type is an input object. `inputFields` is a valid field.'),
LIST = TypeKind.new("LIST", wraps: true, description: 'Indicates this type is a list. `ofType` is a valid field.'),
NON_NULL = TypeKind.new("NON_NULL", wraps: true, description: 'Indicates this type is a non-null. `ofType` is a valid field.'),
]
end
end
```
> NOTE [approver]: The sample pipeline `falcon-ingest` requires approval from group `crew-emerald`.
### oss/graphql-ruby/lib/graphql/types.rb
```ruby
# frozen_string_literal: true
require "graphql/types/boolean"
require "graphql/types/big_int"
require "graphql/types/float"
require "graphql/types/id"
require "graphql/types/int"
require "graphql/types/iso_8601_date"
require "graphql/types/iso_8601_date_time"
require "graphql/types/json"
require "graphql/types/string"
require "graphql/types/relay"
```
### oss/graphql-ruby/lib/graphql/unauthorized_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
# When an `authorized?` hook returns false, this error is used to communicate the failure.
# It's passed to {Schema.unauthorized_object}.
#
# Alternatively, custom code in `authorized?` may raise this error. It will be routed the same way.
class UnauthorizedError < GraphQL::Error
# @return [Object] the application object that failed the authorization check
attr_reader :object
# @return [Class] the GraphQL object type whose `.authorized?` method was called (and returned false)
attr_reader :type
# @return [GraphQL::Query::Context] the context for the current query
attr_accessor :context
def initialize(message = nil, object: nil, type: nil, context: nil)
if message.nil? && object.nil? && type.nil?
raise ArgumentError, "#{self.class.name} requires either a message or keywords"
end
@object = object
@type = type
@context = context
message ||= "An instance of #{object.class} failed #{type.graphql_name}'s authorization check"
super(message)
end
end
end
```
### oss/graphql-ruby/lib/graphql/unauthorized_field_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
class UnauthorizedFieldError < GraphQL::UnauthorizedError
# @return [Field] the field that failed the authorization check
attr_accessor :field
def initialize(message = nil, object: nil, type: nil, context: nil, field: nil)
if message.nil? && [field, type].any?(&:nil?)
raise ArgumentError, "#{self.class.name} requires either a message or keywords"
end
@field = field
message ||= begin
if object
"An instance of #{object.class} failed #{type.name}'s authorization check on field #{field.name}"
else
"Failed #{type.name}'s authorization check on field #{field.name}"
end
end
super(message, object: object, type: type, context: context)
end
end
end
```
### oss/graphql-ruby/lib/graphql/unresolved_type_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
# Error raised when the value provided for a field
# can't be resolved to one of the possible types for the field.
class UnresolvedTypeError < GraphQL::RuntimeTypeError
# @return [Object] The runtime value which couldn't be successfully resolved with `resolve_type`
attr_reader :value
# @return [GraphQL::Field] The field whose value couldn't be resolved (`field.type` is type which couldn't be resolved)
attr_reader :field
# @return [GraphQL::BaseType] The owner of `field`
attr_reader :parent_type
# @return [Object] The return of {Schema#resolve_type} for `value`
attr_reader :resolved_type
# @return [Array<GraphQL::BaseType>] The allowed options for resolving `value` to `field.type`
attr_reader :possible_types
def initialize(value, field, parent_type, resolved_type, possible_types)
@value = value
@field = field
@parent_type = parent_type
@resolved_type = resolved_type
@possible_types = possible_types
message = "The value from \"#{field.graphql_name}\" on \"#{parent_type.graphql_name}\" could not be resolved to \"#{field.type.to_type_signature}\". " \
"(Received: `#{resolved_type.inspect}`, Expected: [#{possible_types.map(&:graphql_name).join(", ")}]) " \
"Make sure you have defined a `resolve_type` proc on your schema and that value `#{value.inspect}` " \
"gets resolved to a valid type. You may need to add your type to `orphan_types` if it implements an " \
"interface but isn't a return type of any other field."
super(message)
end
end
end
```
### oss/graphql-ruby/lib/graphql/version.rb
```ruby
# frozen_string_literal: true
module GraphQL
VERSION = "2.1.1"
end
```
### oss/graphql-ruby/lib/graphql/analysis/ast.rb
```ruby
# frozen_string_literal: true
require "graphql/analysis/ast/visitor"
require "graphql/analysis/ast/analyzer"
require "graphql/analysis/ast/field_usage"
require "graphql/analysis/ast/query_complexity"
require "graphql/analysis/ast/max_query_complexity"
require "graphql/analysis/ast/query_depth"
require "graphql/analysis/ast/max_query_depth"
module GraphQL
module Analysis
module AST
module_function
# Analyze a multiplex, and all queries within.
# Multiplex analyzers are ran for all queries, keeping state.
# Query analyzers are ran per query, without carrying state between queries.
#
# @param multiplex [GraphQL::Execution::Multiplex]
# @param analyzers [Array<GraphQL::Analysis::AST::Analyzer>]
# @return [Array<Any>] Results from multiplex analyzers
def analyze_multiplex(multiplex, analyzers)
multiplex_analyzers = analyzers.map { |analyzer| analyzer.new(multiplex) }
multiplex.current_trace.analyze_multiplex(multiplex: multiplex) do
query_results = multiplex.queries.map do |query|
if query.valid?
analyze_query(
query,
query.analyzers,
multiplex_analyzers: multiplex_analyzers
)
else
[]
end
end
multiplex_results = multiplex_analyzers.map(&:result)
multiplex_errors = analysis_errors(multiplex_results)
multiplex.queries.each_with_index do |query, idx|
query.analysis_errors = multiplex_errors + analysis_errors(query_results[idx])
end
multiplex_results
end
end
# @param query [GraphQL::Query]
# @param analyzers [Array<GraphQL::Analysis::AST::Analyzer>]
# @return [Array<Any>] Results from those analyzers
def analyze_query(query, analyzers, multiplex_analyzers: [])
query.current_trace.analyze_query(query: query) do
query_analyzers = analyzers
.map { |analyzer| analyzer.new(query) }
.tap { _1.select!(&:analyze?) }
analyzers_to_run = query_analyzers + multiplex_analyzers
if analyzers_to_run.any?
analyzers_to_run.select!(&:visit?)
if analyzers_to_run.any?
visitor = GraphQL::Analysis::AST::Visitor.new(
query: query,
analyzers: analyzers_to_run
)
visitor.visit
if visitor.rescued_errors.any?
return visitor.rescued_errors
end
end
query_analyzers.map(&:result)
else
[]
end
end
end
def analysis_errors(results)
results.flatten.tap { _1.select! { |r| r.is_a?(GraphQL::AnalysisError) } }
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/analysis/ast/analyzer.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Analysis
module AST
# Query analyzer for query ASTs. Query analyzers respond to visitor style methods
# but are prefixed by `enter` and `leave`.
#
# When an analyzer is initialized with a Multiplex, you can always get the current query from
# `visitor.query` in the visit methods.
#
# @param [GraphQL::Query, GraphQL::Execution::Multiplex] The query or multiplex to analyze
class Analyzer
def initialize(subject)
@subject = subject
if subject.is_a?(GraphQL::Query)
@query = subject
@multiplex = nil
else
@multiplex = subject
@query = nil
end
end
# Analyzer hook to decide at analysis time whether a query should
# be analyzed or not.
# @return [Boolean] If the query should be analyzed or not
def analyze?
true
end
# Analyzer hook to decide at analysis time whether analysis
# requires a visitor pass; can be disabled for precomputed results.
# @return [Boolean] If analysis requires visitation or not
def visit?
true
end
# The result for this analyzer. Returning {GraphQL::AnalysisError} results
# in a query error.
# @return [Any] The analyzer result
def result
raise GraphQL::RequiredImplementationMissingError
end
class << self
private
def build_visitor_hooks(member_name)
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def on_enter_#{member_name}(node, parent, visitor)
end
def on_leave_#{member_name}(node, parent, visitor)
end
EOS
end
end
build_visitor_hooks :argument
build_visitor_hooks :directive
build_visitor_hooks :document
build_visitor_hooks :enum
build_visitor_hooks :field
build_visitor_hooks :fragment_spread
build_visitor_hooks :inline_fragment
build_visitor_hooks :input_object
build_visitor_hooks :list_type
build_visitor_hooks :non_null_type
build_visitor_hooks :null_value
build_visitor_hooks :operation_definition
build_visitor_hooks :type_name
build_visitor_hooks :variable_definition
build_visitor_hooks :variable_identifier
build_visitor_hooks :abstract_node
protected
# @return [GraphQL::Query, GraphQL::Execution::Multiplex] Whatever this analyzer is analyzing
attr_reader :subject
# @return [GraphQL::Query, nil] `nil` if this analyzer is visiting a multiplex
# (When this is `nil`, use `visitor.query` inside visit methods to get the current query)
attr_reader :query
# @return [GraphQL::Execution::Multiplex, nil] `nil` if this analyzer is visiting a query
attr_reader :multiplex
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/analysis/ast/field_usage.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Analysis
module AST
class FieldUsage < Analyzer
def initialize(query)
super
@used_fields = Set.new
@used_deprecated_fields = Set.new
@used_deprecated_arguments = Set.new
end
def on_leave_field(node, parent, visitor)
field_defn = visitor.field_definition
field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}"
@used_fields << field
@used_deprecated_fields << field if field_defn.deprecation_reason
arguments = visitor.query.arguments_for(node, visitor.field_definition)
# If there was an error when preparing this argument object,
# then this might be an error or something:
if arguments.respond_to?(:argument_values)
extract_deprecated_arguments(arguments.argument_values)
end
end
def result
{
used_fields: @used_fields.to_a,
used_deprecated_fields: @used_deprecated_fields.to_a,
used_deprecated_arguments: @used_deprecated_arguments.to_a,
}
end
private
def extract_deprecated_arguments(argument_values)
argument_values.each_pair do |_argument_name, argument|
if argument.definition.deprecation_reason
@used_deprecated_arguments << argument.definition.path
end
next if argument.value.nil?
if argument.definition.type.kind.input_object?
extract_deprecated_arguments(argument.value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
elsif argument.definition.type.list?
argument
.value
.select { |value| value.respond_to?(:arguments) }
.each { |value| extract_deprecated_arguments(value.arguments.argument_values) } # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
end
end
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/analysis/ast/max_query_complexity.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Analysis
module AST
# Used under the hood to implement complexity validation,
# see {Schema#max_complexity} and {Query#max_complexity}
class MaxQueryComplexity < QueryComplexity
def result
return if subject.max_complexity.nil?
total_complexity = max_possible_complexity
if total_complexity > subject.max_complexity
GraphQL::AnalysisError.new("Query has complexity of #{total_complexity}, which exceeds max complexity of #{subject.max_complexity}")
else
nil
end
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/analysis/ast/max_query_depth.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Analysis
module AST
class MaxQueryDepth < QueryDepth
def result
configured_max_depth = if query
query.max_depth
else
multiplex.schema.max_depth
end
if configured_max_depth && @max_depth > configured_max_depth
GraphQL::AnalysisError.new("Query has depth of #{@max_depth}, which exceeds max depth of #{configured_max_depth}")
else
nil
end
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/analysis/ast/query_complexity.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Analysis
# Calculate the complexity of a query, using {Field#complexity} values.
module AST
class QueryComplexity < Analyzer
# State for the query complexity calculation:
# - `complexities_on_type` holds complexity scores for each type
def initialize(query)
super
@complexities_on_type_by_query = {}
end
# Overide this method to use the complexity result
def result
max_possible_complexity
end
class ScopedTypeComplexity
# A single proc for {#scoped_children} hashes. Use this to avoid repeated allocations,
# since the lexical binding isn't important.
HASH_CHILDREN = ->(h, k) { h[k] = {} }
attr_reader :field_definition, :response_path, :query
# @param parent_type [Class] The owner of `field_definition`
# @param field_definition [GraphQL::Field, GraphQL::Schema::Field] Used for getting the `.complexity` configuration
# @param query [GraphQL::Query] Used for `query.possible_types`
# @param response_path [Array<String>] The path to the response key for the field
def initialize(parent_type, field_definition, query, response_path)
@parent_type = parent_type
@field_definition = field_definition
@query = query
@response_path = response_path
@scoped_children = nil
@nodes = []
end
# @return [Array<GraphQL::Language::Nodes::Field>]
attr_reader :nodes
# Returns true if this field has no selections, ie, it's a scalar.
# We need a quick way to check whether we should continue traversing.
def terminal?
@scoped_children.nil?
end
# This value is only calculated when asked for to avoid needless hash allocations.
# Also, if it's never asked for, we determine that this scope complexity
# is a scalar field ({#terminal?}).
# @return [Hash<Hash<Class => ScopedTypeComplexity>]
def scoped_children
@scoped_children ||= Hash.new(&HASH_CHILDREN)
end
def own_complexity(child_complexity)
@field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
end
end
def on_enter_field(node, parent, visitor)
# We don't want to visit fragment definitions,
# we'll visit them when we hit the spreads instead
return if visitor.visiting_fragment_definition?
return if visitor.skipping?
parent_type = visitor.parent_type_definition
field_key = node.alias || node.name
# Find the complexity calculation for this field --
# if we're re-entering a selection, we'll already have one.
# Otherwise, make a new one and store it.
#
# `node` and `visitor.field_definition` may appear from a cache,
# but I think that's ok. If the arguments _didn't_ match,
# then the query would have been rejected as invalid.
complexities_on_type = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)]
complexity = complexities_on_type.last.scoped_children[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path)
complexity.nodes.push(node)
# Push it on the stack.
complexities_on_type.push(complexity)
end
def on_leave_field(node, parent, visitor)
# We don't want to visit fragment definitions,
# we'll visit them when we hit the spreads instead
return if visitor.visiting_fragment_definition?
return if visitor.skipping?
complexities_on_type = @complexities_on_type_by_query[visitor.query]
complexities_on_type.pop
end
private
# @return [Integer]
def max_possible_complexity
@complexities_on_type_by_query.reduce(0) do |total, (query, complexities_on_type)|
root_complexity = complexities_on_type.last
# Use this entry point to calculate the total complexity
total_complexity_for_query = merged_max_complexity_for_scopes(query, [root_complexity.scoped_children])
total + total_complexity_for_query
end
end
# @param query [GraphQL::Query] Used for `query.possible_types`
# @param scoped_children_hashes [Array<Hash>] Array of scoped children hashes
# @return [Integer]
def merged_max_complexity_for_scopes(query, scoped_children_hashes)
# Figure out what scopes are possible here.
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
all_scopes = {}
scoped_children_hashes.each do |h|
all_scopes.merge!(h)
end
# If an abstract scope is present, but _all_ of its concrete types
# are also in the list, remove it from the list of scopes to check,
# because every possible type is covered by a concrete type.
# (That is, there are no remainder types to check.)
prev_keys = all_scopes.keys
prev_keys.each do |scope|
next unless scope.kind.abstract?
missing_concrete_types = query.possible_types(scope).select { |t| !all_scopes.key?(t) }
# This concrete type is possible _only_ as a member of the abstract type.
# So, attribute to it the complexity which belongs to the abstract type.
missing_concrete_types.each do |concrete_scope|
all_scopes[concrete_scope] = all_scopes[scope]
end
all_scopes.delete(scope)
end
# This will hold `{ type => int }` pairs, one for each possible branch
complexity_by_scope = {}
# For each scope,
# find the lexical selections that might apply to it,
# and gather them together into an array.
# Then, treat the set of selection hashes
# as a set and calculate the complexity for them as a unit
all_scopes.each do |scope, _|
# These will be the selections on `scope`
children_for_scope = []
scoped_children_hashes.each do |sc_h|
sc_h.each do |inner_scope, children_hash|
if applies_to?(query, scope, inner_scope)
children_for_scope << children_hash
end
end
end
# Calculate the complexity for `scope`, merging all
# possible lexical branches.
complexity_value = merged_max_complexity(query, children_for_scope)
complexity_by_scope[scope] = complexity_value
end
# Return the max complexity among all scopes
complexity_by_scope.each_value.max
end
def applies_to?(query, left_scope, right_scope)
if left_scope == right_scope
# This can happen when several branches are being analyzed together
true
else
# Check if these two scopes have _any_ types in common.
possible_right_types = query.possible_types(right_scope)
possible_left_types = query.possible_types(left_scope)
!(possible_right_types & possible_left_types).empty?
end
end
# A hook which is called whenever a field's max complexity is calculated.
# Override this method to capture individual field complexity details.
#
# @param scoped_type_complexity [ScopedTypeComplexity]
# @param max_complexity [Numeric] Field's maximum complexity including child complexity
# @param child_complexity [Numeric, nil] Field's child complexity
def field_complexity(scoped_type_complexity, max_complexity:, child_complexity: nil)
end
# @param children_for_scope [Array<Hash>] An array of `scoped_children[scope]` hashes
# (`{field_key => complexity}`)
# @return [Integer] Complexity value for all these selections in the current scope
def merged_max_complexity(query, children_for_scope)
all_keys = []
children_for_scope.each do |c|
all_keys.concat(c.keys)
end
all_keys.uniq!
complexity_for_keys = {}
all_keys.each do |child_key|
scoped_children_for_key = nil
complexity_for_key = nil
children_for_scope.each do |children_hash|
next unless children_hash.key?(child_key)
complexity_for_key = children_hash[child_key]
if complexity_for_key.terminal?
# Assume that all terminals would return the same complexity
# Since it's a terminal, its child complexity is zero.
complexity = complexity_for_key.own_complexity(0)
complexity_for_keys[child_key] = complexity
field_complexity(complexity_for_key, max_complexity: complexity, child_complexity: nil)
else
scoped_children_for_key ||= []
scoped_children_for_key << complexity_for_key.scoped_children
end
end
next unless scoped_children_for_key
child_complexity = merged_max_complexity_for_scopes(query, scoped_children_for_key)
# This is the _last_ one we visited; assume it's representative.
max_complexity = complexity_for_key.own_complexity(child_complexity)
field_complexity(complexity_for_key, max_complexity: max_complexity, child_complexity: child_complexity)
complexity_for_keys[child_key] = max_complexity
end
# Calculate the child complexity by summing the complexity of all selections
complexity_for_keys.each_value.inject(0, &:+)
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/analysis/ast/query_depth.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Analysis
# A query reducer for measuring the depth of a given query.
#
# See https://graphql-ruby.org/queries/ast_analysis.html for more examples.
#
# @example Logging the depth of a query
# class LogQueryDepth < GraphQL::Analysis::QueryDepth
# def result
# log("GraphQL query depth: #{@max_depth}")
# end
# end
#
# # In your Schema file:
#
# class MySchema < GraphQL::Schema
# query_analyzer LogQueryDepth
# end
#
# # When you run the query, the depth will get logged:
#
# Schema.execute(query_str)
# # GraphQL query depth: 8
#
module AST
class QueryDepth < Analyzer
def initialize(query)
@max_depth = 0
@current_depth = 0
super
end
def on_enter_field(node, parent, visitor)
return if visitor.skipping? || visitor.visiting_fragment_definition?
@current_depth += 1
end
def on_leave_field(node, parent, visitor)
return if visitor.skipping? || visitor.visiting_fragment_definition?
if @max_depth < @current_depth
@max_depth = @current_depth
end
@current_depth -= 1
end
def result
@max_depth
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/analysis/ast/visitor.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Analysis
module AST
# Depth first traversal through a query AST, calling AST analyzers
# along the way.
#
# The visitor is a special case of GraphQL::Language::StaticVisitor, visiting
# only the selected operation, providing helpers for common use cases such
# as skipped fields and visiting fragment spreads.
#
# @see {GraphQL::Analysis::AST::Analyzer} AST Analyzers for queries
class Visitor < GraphQL::Language::StaticVisitor
def initialize(query:, analyzers:)
@analyzers = analyzers
@path = []
@object_types = []
@directives = []
@field_definitions = []
@argument_definitions = []
@directive_definitions = []
@rescued_errors = []
@query = query
@schema = query.schema
@response_path = []
@skip_stack = [false]
super(query.selected_operation)
end
# @return [GraphQL::Query] the query being visited
attr_reader :query
# @return [Array<GraphQL::ObjectType>] Types whose scope we've entered
attr_reader :object_types
# @return [Array<GraphQL::AnalysisError]
attr_reader :rescued_errors
def visit
return unless @document
super
end
# Visit Helpers
# @return [GraphQL::Execution::Interpreter::Arguments] Arguments for this node, merging default values, literal values and query variables
# @see {GraphQL::Query#arguments_for}
def arguments_for(ast_node, field_definition)
@query.arguments_for(ast_node, field_definition)
end
# @return [Boolean] If the visitor is currently inside a fragment definition
def visiting_fragment_definition?
@in_fragment_def
end
# @return [Boolean] If the current node should be skipped because of a skip or include directive
def skipping?
@skipping
end
# @return [Array<String>] The path to the response key for the current field
def response_path
@response_path.dup
end
# Visitor Hooks
[
:operation_definition, :fragment_definition,
:inline_fragment, :field, :directive, :argument, :fragment_spread
].each do |node_type|
module_eval <<-RUBY, __FILE__, __LINE__
def call_on_enter_#{node_type}(node, parent)
@analyzers.each do |a|
begin
a.on_enter_#{node_type}(node, parent, self)
rescue AnalysisError => err
@rescued_errors << err
end
end
end
def call_on_leave_#{node_type}(node, parent)
@analyzers.each do |a|
begin
a.on_leave_#{node_type}(node, parent, self)
rescue AnalysisError => err
@rescued_errors << err
end
end
end
RUBY
end
def on_operation_definition(node, parent)
object_type = @schema.root_type_for_operation(node.operation_type)
@object_types.push(object_type)
@path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}")
call_on_enter_operation_definition(node, parent)
super
call_on_leave_operation_definition(node, parent)
@object_types.pop
@path.pop
end
def on_fragment_definition(node, parent)
on_fragment_with_type(node) do
@path.push("fragment #{node.name}")
@in_fragment_def = false
call_on_enter_fragment_definition(node, parent)
super
@in_fragment_def = false
call_on_leave_fragment_definition(node, parent)
end
end
def on_inline_fragment(node, parent)
on_fragment_with_type(node) do
@path.push("...#{node.type ? " on #{node.type.name}" : ""}")
call_on_enter_inline_fragment(node, parent)
super
call_on_leave_inline_fragment(node, parent)
end
end
def on_field(node, parent)
@response_path.push(node.alias || node.name)
parent_type = @object_types.last
# This could be nil if the previous field wasn't found:
field_definition = parent_type && @schema.get_field(parent_type, node.name, @query.context)
@field_definitions.push(field_definition)
if !field_definition.nil?
next_object_type = field_definition.type.unwrap
@object_types.push(next_object_type)
else
@object_types.push(nil)
end
@path.push(node.alias || node.name)
@skipping = @skip_stack.last || skip?(node)
@skip_stack << @skipping
call_on_enter_field(node, parent)
super
@skipping = @skip_stack.pop
call_on_leave_field(node, parent)
@response_path.pop
@field_definitions.pop
@object_types.pop
@path.pop
end
def on_directive(node, parent)
directive_defn = @schema.directives[node.name]
@directive_definitions.push(directive_defn)
call_on_enter_directive(node, parent)
super
call_on_leave_directive(node, parent)
@directive_definitions.pop
end
def on_argument(node, parent)
argument_defn = if (arg = @argument_definitions.last)
arg_type = arg.type.unwrap
if arg_type.kind.input_object?
arg_type.get_argument(node.name, @query.context)
else
nil
end
elsif (directive_defn = @directive_definitions.last)
directive_defn.get_argument(node.name, @query.context)
elsif (field_defn = @field_definitions.last)
field_defn.get_argument(node.name, @query.context)
else
nil
end
@argument_definitions.push(argument_defn)
@path.push(node.name)
call_on_enter_argument(node, parent)
super
call_on_leave_argument(node, parent)
@argument_definitions.pop
@path.pop
end
def on_fragment_spread(node, parent)
@path.push("... #{node.name}")
call_on_enter_fragment_spread(node, parent)
enter_fragment_spread_inline(node)
super
leave_fragment_spread_inline(node)
call_on_leave_fragment_spread(node, parent)
@path.pop
end
# @return [GraphQL::BaseType] The current object type
def type_definition
@object_types.last
end
# @return [GraphQL::BaseType] The type which the current type came from
def parent_type_definition
@object_types[-2]
end
# @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one
def field_definition
@field_definitions.last
end
# @return [GraphQL::Field, nil] The GraphQL field which returned the object that the current field belongs to
def previous_field_definition
@field_definitions[-2]
end
# @return [GraphQL::Directive, nil] The most-recently-entered GraphQL::Directive, if currently inside one
def directive_definition
@directive_definitions.last
end
# @return [GraphQL::Argument, nil] The most-recently-entered GraphQL::Argument, if currently inside one
def argument_definition
@argument_definitions.last
end
# @return [GraphQL::Argument, nil] The previous GraphQL argument
def previous_argument_definition
@argument_definitions[-2]
end
private
# Visit a fragment spread inline instead of visiting the definition
# by itself.
def enter_fragment_spread_inline(fragment_spread)
fragment_def = query.fragments[fragment_spread.name]
object_type = if fragment_def.type
@query.warden.get_type(fragment_def.type.name)
else
object_types.last
end
object_types << object_type
on_fragment_definition_children(fragment_def)
end
# Visit a fragment spread inline instead of visiting the definition
# by itself.
def leave_fragment_spread_inline(_fragment_spread)
object_types.pop
end
def skip?(ast_node)
dir = ast_node.directives
dir.any? && !GraphQL::Execution::DirectiveChecks.include?(dir, query)
end
def on_fragment_with_type(node)
object_type = if node.type
@query.warden.get_type(node.type.name)
else
@object_types.last
end
@object_types.push(object_type)
yield(node)
@object_types.pop
@path.pop
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/backtrace/inspect_result.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Backtrace
module InspectResult
module_function
def inspect_result(obj)
case obj
when Hash
"{" +
obj.map do |key, val|
"#{key}: #{inspect_truncated(val)}"
end.join(", ") +
"}"
when Array
"[" +
obj.map { |v| inspect_truncated(v) }.join(", ") +
"]"
when Query::Context::SharedMethods
if obj.invalid_null?
"nil"
else
inspect_truncated(obj.value)
end
else
inspect_truncated(obj)
end
end
def inspect_truncated(obj)
case obj
when Hash
"{...}"
when Array
"[...]"
when Query::Context::SharedMethods
if obj.invalid_null?
"nil"
else
inspect_truncated(obj.value)
end
when GraphQL::Execution::Lazy
"(unresolved)"
else
"#{obj.inspect}"
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/backtrace/table.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Backtrace
# A class for turning a context into a human-readable table or array
class Table
MIN_COL_WIDTH = 4
MAX_COL_WIDTH = 100
HEADERS = [
"Loc",
"Field",
"Object",
"Arguments",
"Result",
]
def initialize(context, value:)
@context = context
@override_value = value
end
# @return [String] A table layout of backtrace with metadata
def to_table
@to_table ||= render_table(rows)
end
# @return [Array<String>] An array of position + field name entries
def to_backtrace
@to_backtrace ||= begin
backtrace = rows.map { |r| "#{r[0]}: #{r[1]}" }
# skip the header entry
backtrace.shift
backtrace
end
end
private
def rows
@rows ||= build_rows(@context, rows: [HEADERS], top: true)
end
# @return [String]
def render_table(rows)
max = Array.new(HEADERS.length, MIN_COL_WIDTH)
rows.each do |row|
row.each_with_index do |col, idx|
col_len = col.length
max_len = max[idx]
if col_len > max_len
if col_len > MAX_COL_WIDTH
max[idx] = MAX_COL_WIDTH
else
max[idx] = col_len
end
end
end
end
table = "".dup
last_col_idx = max.length - 1
rows.each do |row|
table << row.map.each_with_index do |col, idx|
max_len = max[idx]
if idx < last_col_idx
col = col.ljust(max_len)
end
if col.length > max_len
col = col[0, max_len - 3] + "..."
end
col
end.join(" | ")
table << "\n"
end
table
end
# @return [Array] 5 items for a backtrace table (not `key`)
def build_rows(context_entry, rows:, top: false)
case context_entry
when Backtrace::Frame
field_alias = context_entry.ast_node.respond_to?(:alias) && context_entry.ast_node.alias
value = if top && @override_value
@override_value
else
value_at(@context.query.context.namespace(:interpreter_runtime)[:runtime], context_entry.path)
end
rows << [
"#{context_entry.ast_node ? context_entry.ast_node.position.join(":") : ""}",
"#{context_entry.field.path}#{field_alias ? " as #{field_alias}" : ""}",
"#{context_entry.object.object.inspect}",
context_entry.arguments.to_h.inspect, # rubocop:disable Development/ContextIsPassedCop -- unrelated method
Backtrace::InspectResult.inspect_result(value),
]
if (parent = context_entry.parent_frame)
build_rows(parent, rows: rows)
else
rows
end
when GraphQL::Query::Context
query = context_entry.query
op = query.selected_operation
if op
op_type = op.operation_type
position = "#{op.line}:#{op.col}"
else
op_type = "query"
position = "?:?"
end
op_name = query.selected_operation_name
object = query.root_value
if object.is_a?(GraphQL::Schema::Object)
object = object.object
end
value = value_at(context_entry.namespace(:interpreter_runtime)[:runtime], [])
rows << [
"#{position}",
"#{op_type}#{op_name ? " #{op_name}" : ""}",
"#{object.inspect}",
query.variables.to_h.inspect,
Backtrace::InspectResult.inspect_result(value),
]
else
raise "Unexpected get_rows subject #{context_entry.class} (#{context_entry.inspect})"
end
end
def value_at(runtime, path)
response = runtime.final_result
path.each do |key|
if response && (response = response[key])
next
else
break
end
end
response
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/backtrace/trace.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Backtrace
module Trace
def validate(query:, validate:)
if query.multiplex
push_query_backtrace_context(query)
end
super
end
def analyze_query(query:)
if query.multiplex # missing for stand-alone static validation
push_query_backtrace_context(query)
end
super
end
def execute_query(query:)
push_query_backtrace_context(query)
super
end
def execute_query_lazy(query:, multiplex:)
query ||= multiplex.queries.first
push_query_backtrace_context(query)
super
end
def execute_field(field:, query:, ast_node:, arguments:, object:)
push_field_backtrace_context(field, query, ast_node, arguments, object)
super
end
def execute_field_lazy(field:, query:, ast_node:, arguments:, object:)
push_field_backtrace_context(field, query, ast_node, arguments, object)
super
end
def execute_multiplex(multiplex:)
super
rescue StandardError => err
# This is an unhandled error from execution,
# Re-raise it with a GraphQL trace.
multiplex_context = multiplex.context
potential_context = multiplex_context[:last_graphql_backtrace_context]
if potential_context.is_a?(GraphQL::Query::Context) ||
potential_context.is_a?(Backtrace::Frame)
raise TracedError.new(err, potential_context)
else
raise
end
ensure
multiplex_context = multiplex.context
multiplex_context.delete(:graphql_backtrace_contexts)
multiplex_context.delete(:last_graphql_backtrace_context)
end
private
def push_query_backtrace_context(query)
push_data = query
multiplex = query.multiplex
push_key = []
push_storage = multiplex.context[:graphql_backtrace_contexts] ||= {}
push_storage[push_key] = push_data
multiplex.context[:last_graphql_backtrace_context] = push_data
end
def push_field_backtrace_context(field, query, ast_node, arguments, object)
multiplex = query.multiplex
push_key = query.context[:current_path]
push_storage = multiplex.context[:graphql_backtrace_contexts]
parent_frame = push_storage[push_key[0..-2]]
if parent_frame.is_a?(GraphQL::Query)
parent_frame = parent_frame.context
end
push_data = Frame.new(
query: query,
path: push_key,
ast_node: ast_node,
field: field,
object: object,
arguments: arguments,
parent_frame: parent_frame,
)
push_storage[push_key] = push_data
multiplex.context[:last_graphql_backtrace_context] = push_data
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/backtrace/traced_error.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Backtrace
# When {Backtrace} is enabled, raised errors are wrapped with {TracedError}.
class TracedError < GraphQL::Error
# @return [Array<String>] Printable backtrace of GraphQL error context
attr_reader :graphql_backtrace
# @return [GraphQL::Query::Context] The context at the field where the error was raised
attr_reader :context
MESSAGE_TEMPLATE = <<-MESSAGE
Unhandled error during GraphQL execution:
%{cause_message}
%{cause_backtrace}
%{cause_backtrace_more}
Use #cause to access the original exception (including #cause.backtrace).
GraphQL Backtrace:
%{graphql_table}
MESSAGE
# This many lines of the original Ruby backtrace
# are included in the message
CAUSE_BACKTRACE_PREVIEW_LENGTH = 10
def initialize(err, current_ctx)
@context = current_ctx
backtrace = Backtrace.new(current_ctx, value: err)
@graphql_backtrace = backtrace.to_a
cause_backtrace_preview = err.backtrace.first(CAUSE_BACKTRACE_PREVIEW_LENGTH).join("\n ")
cause_backtrace_remainder_length = err.backtrace.length - CAUSE_BACKTRACE_PREVIEW_LENGTH
cause_backtrace_more = if cause_backtrace_remainder_length < 0
""
elsif cause_backtrace_remainder_length == 1
"... and 1 more line\n"
else
"... and #{cause_backtrace_remainder_length} more lines\n"
end
message = MESSAGE_TEMPLATE % {
cause_message: err.message,
cause_backtrace: cause_backtrace_preview,
cause_backtrace_more: cause_backtrace_more,
graphql_table: backtrace.inspect,
}
super(message)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/backtrace/tracer.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Backtrace
# TODO this is not fiber-friendly
module Tracer
module_function
# Implement the {GraphQL::Tracing} API.
def trace(key, metadata)
case key
when "lex", "parse"
# No context here, don't have a query yet
nil
when "execute_multiplex", "analyze_multiplex"
# No query context yet
nil
when "validate", "analyze_query", "execute_query", "execute_query_lazy"
push_key = []
if (query = metadata[:query]) || ((queries = metadata[:queries]) && (query = queries.first))
push_data = query
multiplex = query.multiplex
elsif (multiplex = metadata[:multiplex])
push_data = multiplex.queries.first
end
when "execute_field", "execute_field_lazy"
query = metadata[:query]
multiplex = query.multiplex
push_key = query.context[:current_path]
parent_frame = multiplex.context[:graphql_backtrace_contexts][push_key[0..-2]]
if parent_frame.is_a?(GraphQL::Query)
parent_frame = parent_frame.context
end
push_data = Frame.new(
query: query,
path: push_key,
ast_node: metadata[:ast_node],
field: metadata[:field],
object: metadata[:object],
arguments: metadata[:arguments],
parent_frame: parent_frame,
)
else
# Custom key, no backtrace data for this
nil
end
if push_data && multiplex
push_storage = multiplex.context[:graphql_backtrace_contexts] ||= {}
push_storage[push_key] = push_data
multiplex.context[:last_graphql_backtrace_context] = push_data
end
if key == "execute_multiplex"
multiplex_context = metadata[:multiplex].context
begin
yield
rescue StandardError => err
# This is an unhandled error from execution,
# Re-raise it with a GraphQL trace.
potential_context = multiplex_context[:last_graphql_backtrace_context]
if potential_context.is_a?(GraphQL::Query::Context) ||
potential_context.is_a?(Backtrace::Frame)
raise TracedError.new(err, potential_context)
else
raise
end
ensure
multiplex_context.delete(:graphql_backtrace_contexts)
multiplex_context.delete(:last_graphql_backtrace_context)
end
else
yield
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/dataloader/null_dataloader.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Dataloader
# The default implementation of dataloading -- all no-ops.
#
# The Dataloader interface isn't public, but it enables
# simple internal code while adding the option to add Dataloader.
class NullDataloader < Dataloader
# These are all no-ops because code was
# executed sychronously.
def run; end
def run_isolated; yield; end
def yield
raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources."
end
def append_job
yield
nil
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/dataloader/request.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Dataloader
# @see Source#request which returns an instance of this
class Request
def initialize(source, key)
@source = source
@key = key
end
# Call this method to cause the current Fiber to wait for the results of this request.
#
# @return [Object] the object loaded for `key`
def load
@source.load(@key)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/dataloader/request_all.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Dataloader
# @see Source#request_all which returns an instance of this.
class RequestAll < Request
def initialize(source, keys)
@source = source
@keys = keys
end
# Call this method to cause the current Fiber to wait for the results of this request.
#
# @return [Array<Object>] One object for each of `keys`
def load
@source.load_all(@keys)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/dataloader/source.rb
```ruby
# frozen_string_literal: true
module GraphQL
class Dataloader
class Source
# Called by {Dataloader} to prepare the {Source}'s internal state
# @api private
def setup(dataloader)
# These keys have been requested but haven't been fetched yet
@pending = {}
# These keys have been passed to `fetch` but haven't been finished yet
@fetching = {}
# { key => result }
@results = {}
@dataloader = dataloader
end
attr_reader :dataloader
# @return [Dataloader::Request] a pending request for a value from `key`. Call `.load` on that object to wait for the result.
def request(value)
res_key = result_key_for(value)
if !@results.key?(res_key)
@pending[res_key] ||= value
end
Dataloader::Request.new(self, value)
end
# Implement this method to return a stable identifier if different
# key objects should load the same data value.
#
# @param value [Object] A value passed to `.request` or `.load`, for which a value will be loaded
# @return [Object] The key for tracking this pending data
def result_key_for(value)
value
end
# @return [Dataloader::Request] a pending request for a values from `keys`. Call `.load` on that object to wait for the results.
def request_all(values)
values.each do |v|
res_key = result_key_for(v)
if !@results.key?(res_key)
@pending[res_key] ||= v
end
end
Dataloader::RequestAll.new(self, values)
end
# @param value [Object] A loading value which will be passed to {#fetch} if it isn't already in the internal cache.
# @return [Object] The result from {#fetch} for `key`. If `key` hasn't been loaded yet, the Fiber will yield until it's loaded.
def load(value)
result_key = result_key_for(value)
if @results.key?(result_key)
result_for(result_key)
else
@pending[result_key] ||= value
sync([result_key])
result_for(result_key)
end
end
# @param values [Array<Object>] Loading keys which will be passed to `#fetch` (or read from the internal cache).
# @return [Object] The result from {#fetch} for `keys`. If `keys` haven't been loaded yet, the Fiber will yield until they're loaded.
def load_all(values)
result_keys = []
pending_keys = []
values.each { |v|
k = result_key_for(v)
result_keys << k
if !@results.key?(k)
@pending[k] ||= v
pending_keys << k
end
}
if pending_keys.any?
sync(pending_keys)
end
result_keys.map { |k| result_for(k) }
end
# Subclasses must implement this method to return a value for each of `keys`
# @param keys [Array<Object>] keys passed to {#load}, {#load_all}, {#request}, or {#request_all}
# @return [Array<Object>] A loaded value for each of `keys`. The array must match one-for-one to the list of `keys`.
def fetch(keys)
# somehow retrieve these from the backend
raise "Implement `#{self.class}#fetch(#{keys.inspect}) to return a record for each of the keys"
end
# Wait for a batch, if there's anything to batch.
# Then run the batch and update the cache.
# @return [void]
def sync(pending_result_keys)
@dataloader.yield
iterations = 0
while pending_result_keys.any? { |key| !@results.key?(key) }
iterations += 1
if iterations > 1000
raise "#{self.class}#sync tried 1000 times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency."
end
@dataloader.yield
end
nil
end
# @return [Boolean] True if this source has any pending requests for data.
def pending?
!@pending.empty?
end
# Add these key-value pairs to this source's cache
# (future loads will use these merged values).
# @param new_results [Hash<Object => Object>] key-value pairs to cache in this source
# @return [void]
def merge(new_results)
new_results.each do |new_k, new_v|
key = result_key_for(new_k)
@results[key] = new_v
end
nil
end
# Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
# @api private
# @return [void]
def run_pending_keys
if !@fetching.empty?
@fetching.each_key { |k| @pending.delete(k) }
end
return if @pending.empty?
fetch_h = @pending
@pending = {}
@fetching.merge!(fetch_h)
results = fetch(fetch_h.values)
fetch_h.each_with_index do |(key, _value), idx|
@results[key] = results[idx]
end
nil
rescue StandardError => error
fetch_h.each_key { |key| @results[key] = error }
ensure
fetch_h && fetch_h.each_key { |k| @fetching.delete(k) }
end
# These arguments are given to `dataloader.with(source_class, ...)`. The object
# returned from this method is used to de-duplicate batch loads under the hood
# by using it as a Hash key.
#
# By default, the arguments are all put in an Array. To customize how this source's
# batches are merged, override this method to return something else.
#
# For example, if you pass `ActiveRecord::Relation`s to `.with(...)`, you could override
# this method to call `.to_sql` on them, thus merging `.load(...)` calls when they apply
# to equivalent relations.
#
# @param batch_args [Array<Object>]
# @param batch_kwargs [Hash]
# @return [Object]
def self.batch_key_for(*batch_args, **batch_kwargs)
[*batch_args, **batch_kwargs]
end
# Clear any already-loaded objects for this source
# @return [void]
def clear_cache
@results.clear
nil
end
attr_reader :pending
private
# Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
# @param key [Object] key passed to {#load} or {#load_all}
# @return [Object] The result from {#fetch} for `key`.
# @api private
def result_for(key)
if !@results.key?(key)
raise GraphQL::InvariantError, <<-ERR
Fetching result for a key on #{self.class} that hasn't been loaded yet (#{key.inspect}, loaded: #{@results.keys})
This key should have been loaded already. This is a bug in GraphQL::Dataloader, please report it on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new.
ERR
end
result = @results[key]
raise result if result.class <= StandardError
result
end
end
end
end
```
> NOTE [debounce]: The example `<RippleToggle>` component debounces its input by 285 milliseconds.
### oss/graphql-ruby/lib/graphql/execution/directive_checks.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
# Boolean checks for how an AST node's directives should
# influence its execution
# @api private
module DirectiveChecks
SKIP = "skip"
INCLUDE = "include"
module_function
# @return [Boolean] Should this node be included in the query?
def include?(directive_ast_nodes, query)
directive_ast_nodes.each do |directive_ast_node|
name = directive_ast_node.name
directive_defn = query.schema.directives[name]
case name
when SKIP
args = query.arguments_for(directive_ast_node, directive_defn)
if args[:if] == true
return false
end
when INCLUDE
args = query.arguments_for(directive_ast_node, directive_defn)
if args[:if] == false
return false
end
else
# Undefined directive, or one we don't care about
end
end
true
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/errors.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
class Errors
# Register this handler, updating the
# internal handler index to maintain least-to-most specific.
#
# @param error_class [Class<Exception>]
# @param error_handlers [Hash]
# @param error_handler [Proc]
# @return [void]
def self.register_rescue_from(error_class, error_handlers, error_handler)
subclasses_handlers = {}
this_level_subclasses = []
# During this traversal, do two things:
# - Identify any already-registered subclasses of this error class
# and gather them up to be inserted _under_ this class
# - Find the point in the index where this handler should be inserted
# (That is, _under_ any superclasses, or at top-level, if there are no superclasses registered)
while (error_handlers) do
this_level_subclasses.clear
# First, identify already-loaded handlers that belong
# _under_ this one. (That is, they're handlers
# for subclasses of `error_class`.)
error_handlers.each do |err_class, handler|
if err_class < error_class
subclasses_handlers[err_class] = handler
this_level_subclasses << err_class
end
end
# Any handlers that we'll be moving, delete them from this point in the index
this_level_subclasses.each do |err_class|
error_handlers.delete(err_class)
end
# See if any keys in this hash are superclasses of this new class:
next_index_point = error_handlers.find { |err_class, handler| error_class < err_class }
if next_index_point
error_handlers = next_index_point[1][:subclass_handlers]
else
# this new handler doesn't belong to any sub-handlers,
# so insert it in the current set of `handlers`
break
end
end
# Having found the point at which to insert this handler,
# register it and merge any subclass handlers back in at this point.
this_class_handlers = error_handlers[error_class]
this_class_handlers[:handler] = error_handler
this_class_handlers[:subclass_handlers].merge!(subclasses_handlers)
nil
end
# @return [Proc, nil] The handler for `error_class`, if one was registered on this schema or inherited
def self.find_handler_for(schema, error_class)
handlers = schema.error_handlers[:subclass_handlers]
handler = nil
while (handlers) do
_err_class, next_handler = handlers.find { |err_class, handler| error_class <= err_class }
if next_handler
handlers = next_handler[:subclass_handlers]
handler = next_handler
else
# Don't reassign `handler` --
# let the previous assignment carry over outside this block.
break
end
end
# check for a handler from a parent class:
if schema.superclass.respond_to?(:error_handlers)
parent_handler = find_handler_for(schema.superclass, error_class)
end
# If the inherited handler is more specific than the one defined here,
# use it.
# If it's a tie (or there is no parent handler), use the one defined here.
# If there's an inherited one, but not one defined here, use the inherited one.
# Otherwise, there's no handler for this error, return `nil`.
if parent_handler && handler && parent_handler[:class] < handler[:class]
parent_handler
elsif handler
handler
elsif parent_handler
parent_handler
else
nil
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/interpreter.rb
```ruby
# frozen_string_literal: true
require "fiber"
require "graphql/execution/interpreter/argument_value"
require "graphql/execution/interpreter/arguments"
require "graphql/execution/interpreter/arguments_cache"
require "graphql/execution/interpreter/execution_errors"
require "graphql/execution/interpreter/runtime"
require "graphql/execution/interpreter/resolve"
require "graphql/execution/interpreter/handles_raw_value"
module GraphQL
module Execution
class Interpreter
class << self
# Used internally to signal that the query shouldn't be executed
# @api private
NO_OPERATION = GraphQL::EmptyObjects::EMPTY_HASH
# @param schema [GraphQL::Schema]
# @param queries [Array<GraphQL::Query, Hash>]
# @param context [Hash]
# @param max_complexity [Integer, nil]
# @return [Array<Hash>] One result per query
def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
queries = query_options.map do |opts|
case opts
when Hash
GraphQL::Query.new(schema, nil, **opts)
when GraphQL::Query
opts
else
raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
end
end
multiplex = Execution::Multiplex.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
multiplex.current_trace.execute_multiplex(multiplex: multiplex) do
schema = multiplex.schema
queries = multiplex.queries
query_instrumenters = schema.instrumenters[:query]
multiplex_instrumenters = schema.instrumenters[:multiplex]
lazies_at_depth = Hash.new { |h, k| h[k] = [] }
# First, run multiplex instrumentation, then query instrumentation for each query
call_hooks(multiplex_instrumenters, multiplex, :before_multiplex, :after_multiplex) do
each_query_call_hooks(query_instrumenters, queries) do
schema = multiplex.schema
multiplex_analyzers = schema.multiplex_analyzers
queries = multiplex.queries
if multiplex.max_complexity
multiplex_analyzers += [GraphQL::Analysis::AST::MaxQueryComplexity]
end
schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers)
begin
# Since this is basically the batching context,
# share it for a whole multiplex
multiplex.context[:interpreter_instance] ||= multiplex.schema.query_execution_strategy.new
# Do as much eager evaluation of the query as possible
results = []
queries.each_with_index do |query, idx|
multiplex.dataloader.append_job {
operation = query.selected_operation
result = if operation.nil? || !query.valid? || query.context.errors.any?
NO_OPERATION
else
begin
# Although queries in a multiplex _share_ an Interpreter instance,
# they also have another item of state, which is private to that query
# in particular, assign it here:
runtime = Runtime.new(query: query, lazies_at_depth: lazies_at_depth)
query.context.namespace(:interpreter_runtime)[:runtime] = runtime
query.current_trace.execute_query(query: query) do
runtime.run_eager
end
rescue GraphQL::ExecutionError => err
query.context.errors << err
NO_OPERATION
end
end
results[idx] = result
}
end
multiplex.dataloader.run
# Then, work through lazy results in a breadth-first way
multiplex.dataloader.append_job {
query = multiplex.queries.length == 1 ? multiplex.queries[0] : nil
queries = multiplex ? multiplex.queries : [query]
final_values = queries.map do |query|
runtime = query.context.namespace(:interpreter_runtime)[:runtime]
# it might not be present if the query has an error
runtime ? runtime.final_result : nil
end
final_values.compact!
multiplex.current_trace.execute_query_lazy(multiplex: multiplex, query: query) do
Interpreter::Resolve.resolve_each_depth(lazies_at_depth, multiplex.dataloader)
end
}
multiplex.dataloader.run
# Then, find all errors and assign the result to the query object
results.each_with_index do |data_result, idx|
query = queries[idx]
# Assign the result so that it can be accessed in instrumentation
query.result_values = if data_result.equal?(NO_OPERATION)
if !query.valid? || query.context.errors.any?
# A bit weird, but `Query#static_errors` _includes_ `query.context.errors`
{ "errors" => query.static_errors.map(&:to_h) }
else
data_result
end
else
result = {
"data" => query.context.namespace(:interpreter_runtime)[:runtime].final_result
}
if query.context.errors.any?
error_result = query.context.errors.map(&:to_h)
result["errors"] = error_result
end
result
end
if query.context.namespace?(:__query_result_extensions__)
query.result_values["extensions"] = query.context.namespace(:__query_result_extensions__)
end
# Get the Query::Result, not the Hash
results[idx] = query.result
end
results
rescue Exception
# TODO rescue at a higher level so it will catch errors in analysis, too
# Assign values here so that the query's `@executed` becomes true
queries.map { |q| q.result_values ||= {} }
raise
ensure
queries.map { |query|
runtime = query.context.namespace(:interpreter_runtime)[:runtime]
if runtime
runtime.delete_all_interpreter_context
end
}
end
end
end
end
end
private
# Call the before_ hooks of each query,
# Then yield if no errors.
# `call_hooks` takes care of appropriate cleanup.
def each_query_call_hooks(instrumenters, queries, i = 0)
if i >= queries.length
yield
else
query = queries[i]
call_hooks(instrumenters, query, :before_query, :after_query) {
each_query_call_hooks(instrumenters, queries, i + 1) {
yield
}
}
end
end
# Call each before hook, and if they all succeed, yield.
# If they don't all succeed, call after_ for each one that succeeded.
def call_hooks(instrumenters, object, before_hook_name, after_hook_name)
begin
successful = []
instrumenters.each do |instrumenter|
instrumenter.public_send(before_hook_name, object)
successful << instrumenter
end
# if any before hooks raise an exception, quit calling before hooks,
# but call the after hooks on anything that succeeded but also
# raise the exception that came from the before hook.
rescue GraphQL::ExecutionError => err
object.context.errors << err
rescue => e
raise call_after_hooks(successful, object, after_hook_name, e)
end
begin
yield # Call the user code
ensure
ex = call_after_hooks(successful, object, after_hook_name, nil)
raise ex if ex
end
end
def call_after_hooks(instrumenters, object, after_hook_name, ex)
instrumenters.reverse_each do |instrumenter|
begin
instrumenter.public_send(after_hook_name, object)
rescue => e
ex = e
end
end
ex
end
end
class ListResultFailedError < GraphQL::Error
def initialize(value:, path:, field:)
message = "Failed to build a GraphQL list result for field `#{field.path}` at path `#{path.join(".")}`.\n".dup
message << "Expected `#{value.inspect}` (#{value.class}) to implement `.each` to satisfy the GraphQL return type `#{field.type.to_type_signature}`.\n"
if field.connection?
message << "\nThis field was treated as a Relay-style connection; add `connection: false` to the `field(...)` to disable this behavior."
end
super(message)
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/lazy.rb
```ruby
# frozen_string_literal: true
require "graphql/execution/lazy/lazy_method_map"
module GraphQL
module Execution
# This wraps a value which is available, but not yet calculated, like a promise or future.
#
# Calling `#value` will trigger calculation & return the "lazy" value.
#
# This is an itty-bitty promise-like object, with key differences:
# - It has only two states, not-resolved and resolved
# - It has no error-catching functionality
# @api private
class Lazy
attr_reader :field
# Create a {Lazy} which will get its inner value by calling the block
# @param field [GraphQL::Schema::Field]
# @param get_value_func [Proc] a block to get the inner value (later)
def initialize(field: nil, &get_value_func)
@get_value_func = get_value_func
@resolved = false
@field = field
end
# @return [Object] The wrapped value, calling the lazy block if necessary
def value
if !@resolved
@resolved = true
v = @get_value_func.call
if v.is_a?(Lazy)
v = v.value
end
@value = v
end
# `SKIP` was made into a subclass of `GraphQL::Error` to improve runtime performance
# (fewer clauses in a hot `case` block), but now it requires special handling here.
# I think it's still worth it for the performance win, but if the number of special
# cases grows, then maybe it's worth rethinking somehow.
if @value.is_a?(StandardError) && @value != GraphQL::Execution::SKIP
raise @value
else
@value
end
end
# @return [Lazy] A {Lazy} whose value depends on another {Lazy}, plus any transformations in `block`
def then
self.class.new {
yield(value)
}
end
# @param lazies [Array<Object>] Maybe-lazy objects
# @return [Lazy] A lazy which will sync all of `lazies`
def self.all(lazies)
self.new {
lazies.map { |l| l.is_a?(Lazy) ? l.value : l }
}
end
# This can be used for fields which _had no_ lazy results
# @api private
NullResult = Lazy.new(){}
NullResult.value
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/lookahead.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
# Lookahead creates a uniform interface to inspect the forthcoming selections.
#
# It assumes that the AST it's working with is valid. (So, it's safe to use
# during execution, but if you're using it directly, be sure to validate first.)
#
# A field may get access to its lookahead by adding `extras: [:lookahead]`
# to its configuration.
#
# @example looking ahead in a field
# field :articles, [Types::Article], null: false,
# extras: [:lookahead]
#
# # For example, imagine a faster database call
# # may be issued when only some fields are requested.
# #
# # Imagine that _full_ fetch must be made to satisfy `fullContent`,
# # we can look ahead to see if we need that field. If we do,
# # we make the expensive database call instead of the cheap one.
# def articles(lookahead:)
# if lookahead.selects?(:full_content)
# fetch_full_articles(object)
# else
# fetch_preview_articles(object)
# end
# end
class Lookahead
# @param query [GraphQL::Query]
# @param ast_nodes [Array<GraphQL::Language::Nodes::Field>, Array<GraphQL::Language::Nodes::OperationDefinition>]
# @param field [GraphQL::Schema::Field] if `ast_nodes` are fields, this is the field definition matching those nodes
# @param root_type [Class] if `ast_nodes` are operation definition, this is the root type for that operation
def initialize(query:, ast_nodes:, field: nil, root_type: nil, owner_type: nil)
@ast_nodes = ast_nodes.freeze
@field = field
@root_type = root_type
@query = query
@selected_type = @field ? @field.type.unwrap : root_type
@owner_type = owner_type
end
# @return [Array<GraphQL::Language::Nodes::Field>]
attr_reader :ast_nodes
# @return [GraphQL::Schema::Field]
attr_reader :field
# @return [GraphQL::Schema::Object, GraphQL::Schema::Union, GraphQL::Schema::Interface]
attr_reader :owner_type
# @return [Hash<Symbol, Object>]
def arguments
if defined?(@arguments)
@arguments
else
@arguments = if @field
@query.after_lazy(@query.arguments_for(@ast_nodes.first, @field)) do |args|
args.is_a?(Execution::Interpreter::Arguments) ? args.keyword_arguments : args
end
else
nil
end
end
end
# True if this node has a selection on `field_name`.
# If `field_name` is a String, it is treated as a GraphQL-style (camelized)
# field name and used verbatim. If `field_name` is a Symbol, it is
# treated as a Ruby-style (underscored) name and camelized before comparing.
#
# If `arguments:` is provided, each provided key/value will be matched
# against the arguments in the next selection. This method will return false
# if any of the given `arguments:` are not present and matching in the next selection.
# (But, the next selection may contain _more_ than the given arguments.)
# @param field_name [String, Symbol]
# @param arguments [Hash] Arguments which must match in the selection
# @return [Boolean]
def selects?(field_name, selected_type: @selected_type, arguments: nil)
selection(field_name, selected_type: selected_type, arguments: arguments).selected?
end
# @return [Boolean] True if this lookahead represents a field that was requested
def selected?
true
end
# Like {#selects?}, but can be used for chaining.
# It returns a null object (check with {#selected?})
# @param field_name [String, Symbol]
# @return [GraphQL::Execution::Lookahead]
def selection(field_name, selected_type: @selected_type, arguments: nil)
next_field_defn = case field_name
when String
@query.get_field(selected_type, field_name)
when Symbol
# Try to avoid the `.to_s` below, if possible
all_fields = if selected_type.kind.fields?
@query.warden.fields(selected_type)
else
# Handle unions by checking possible
@query.warden
.possible_types(selected_type)
.map { |t| @query.warden.fields(t) }
.tap(&:flatten!)
end
if (match_by_orig_name = all_fields.find { |f| f.original_name == field_name })
match_by_orig_name
else
# Symbol#name is only present on 3.0+
sym_s = field_name.respond_to?(:name) ? field_name.name : field_name.to_s
guessed_name = Schema::Member::BuildType.camelize(sym_s)
@query.get_field(selected_type, guessed_name)
end
end
if next_field_defn
next_nodes = []
@ast_nodes.each do |ast_node|
ast_node.selections.each do |selection|
find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes)
end
end
if next_nodes.any?
Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
else
NULL_LOOKAHEAD
end
else
NULL_LOOKAHEAD
end
end
# Like {#selection}, but for all nodes.
# It returns a list of Lookaheads for all Selections
#
# If `arguments:` is provided, each provided key/value will be matched
# against the arguments in each selection. This method will filter the selections
# if any of the given `arguments:` do not match the given selection.
#
# @example getting the name of a selection
# def articles(lookahead:)
# next_lookaheads = lookahead.selections # => [#<GraphQL::Execution::Lookahead ...>, ...]
# next_lookaheads.map(&:name) #=> [:full_content, :title]
# end
#
# @param arguments [Hash] Arguments which must match in the selection
# @return [Array<GraphQL::Execution::Lookahead>]
def selections(arguments: nil)
subselections_by_type = {}
subselections_on_type = subselections_by_type[@selected_type] = {}
@ast_nodes.each do |node|
find_selections(subselections_by_type, subselections_on_type, @selected_type, node.selections, arguments)
end
subselections = []
subselections_by_type.each do |type, ast_nodes_by_response_key|
ast_nodes_by_response_key.each do |response_key, ast_nodes|
field_defn = @query.get_field(type, ast_nodes.first.name)
lookahead = Lookahead.new(query: @query, ast_nodes: ast_nodes, field: field_defn, owner_type: type)
subselections.push(lookahead)
end
end
subselections
end
# The method name of the field.
# It returns the method_sym of the Lookahead's field.
#
# @example getting the name of a selection
# def articles(lookahead:)
# article.selection(:full_content).name # => :full_content
# # ...
# end
#
# @return [Symbol]
def name
@field && @field.original_name
end
def inspect
"#<GraphQL::Execution::Lookahead #{@field ? "@field=#{@field.path.inspect}": "@root_type=#{@root_type}"} @ast_nodes.size=#{@ast_nodes.size}>"
end
# This is returned for {Lookahead#selection} when a non-existent field is passed
class NullLookahead < Lookahead
# No inputs required here.
def initialize
end
def selected?
false
end
def selects?(*)
false
end
def selection(*)
NULL_LOOKAHEAD
end
def selections(*)
[]
end
def inspect
"#<GraphQL::Execution::Lookahead::NullLookahead>"
end
end
# A singleton, so that misses don't come with overhead.
NULL_LOOKAHEAD = NullLookahead.new
private
def skipped_by_directive?(ast_selection)
ast_selection.directives.each do |directive|
dir_defn = @query.schema.directives.fetch(directive.name)
directive_class = dir_defn
if directive_class
dir_args = @query.arguments_for(directive, dir_defn)
return true unless directive_class.static_include?(dir_args, @query.context)
end
end
false
end
def find_selections(subselections_by_type, selections_on_type, selected_type, ast_selections, arguments)
ast_selections.each do |ast_selection|
next if skipped_by_directive?(ast_selection)
case ast_selection
when GraphQL::Language::Nodes::Field
response_key = ast_selection.alias || ast_selection.name
if selections_on_type.key?(response_key)
selections_on_type[response_key] << ast_selection
elsif arguments.nil? || arguments.empty?
selections_on_type[response_key] = [ast_selection]
else
field_defn = @query.get_field(selected_type, ast_selection.name)
if arguments_match?(arguments, field_defn, ast_selection)
selections_on_type[response_key] = [ast_selection]
end
end
when GraphQL::Language::Nodes::InlineFragment
on_type = selected_type
subselections_on_type = selections_on_type
if (t = ast_selection.type)
# Assuming this is valid, that `t` will be found.
on_type = @query.get_type(t.name)
subselections_on_type = subselections_by_type[on_type] ||= {}
end
find_selections(subselections_by_type, subselections_on_type, on_type, ast_selection.selections, arguments)
when GraphQL::Language::Nodes::FragmentSpread
frag_defn = @query.fragments[ast_selection.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{ast_selection.name} (found: #{@query.fragments.keys})")
# Again, assuming a valid AST
on_type = @query.get_type(frag_defn.type.name)
subselections_on_type = subselections_by_type[on_type] ||= {}
find_selections(subselections_by_type, subselections_on_type, on_type, frag_defn.selections, arguments)
else
raise "Invariant: Unexpected selection type: #{ast_selection.class}"
end
end
end
# If a selection on `node` matches `field_name` (which is backed by `field_defn`)
# and matches the `arguments:` constraints, then add that node to `matches`
def find_selected_nodes(node, field_defn, arguments:, matches:)
return if skipped_by_directive?(node)
case node
when GraphQL::Language::Nodes::Field
if node.name == field_defn.graphql_name
if arguments.nil? || arguments.empty?
# No constraint applied
matches << node
elsif arguments_match?(arguments, field_defn, node)
matches << node
end
end
when GraphQL::Language::Nodes::InlineFragment
node.selections.each { |s| find_selected_nodes(s, field_defn, arguments: arguments, matches: matches) }
when GraphQL::Language::Nodes::FragmentSpread
frag_defn = @query.fragments[node.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})")
frag_defn.selections.each { |s| find_selected_nodes(s, field_defn, arguments: arguments, matches: matches) }
else
raise "Unexpected selection comparison on #{node.class.name} (#{node})"
end
end
def arguments_match?(arguments, field_defn, field_node)
query_kwargs = @query.arguments_for(field_node, field_defn)
arguments.all? do |arg_name, arg_value|
arg_name_sym = if arg_name.is_a?(String)
Schema::Member::BuildType.underscore(arg_name).to_sym
else
arg_name
end
# Make sure the constraint is present with a matching value
query_kwargs.key?(arg_name_sym) && query_kwargs[arg_name_sym] == arg_value
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/multiplex.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
# Execute multiple queries under the same multiplex "umbrella".
# They can share a batching context and reduce redundant database hits.
#
# The flow is:
#
# - Multiplex instrumentation setup
# - Query instrumentation setup
# - Analyze the multiplex + each query
# - Begin each query
# - Resolve lazy values, breadth-first across all queries
# - Finish each query (eg, get errors)
# - Query instrumentation teardown
# - Multiplex instrumentation teardown
#
# If one query raises an application error, all queries will be in undefined states.
#
# Validation errors and {GraphQL::ExecutionError}s are handled in isolation:
# one of these errors in one query will not affect the other queries.
#
# @see {Schema#multiplex} for public API
# @api private
class Multiplex
include Tracing::Traceable
attr_reader :context, :queries, :schema, :max_complexity, :dataloader, :current_trace
def initialize(schema:, queries:, context:, max_complexity:)
@schema = schema
@queries = queries
@queries.each { |q| q.multiplex = self }
@context = context
@current_trace = @context[:trace] || schema.new_trace(multiplex: self)
@dataloader = @context[:dataloader] ||= @schema.dataloader_class.new
@tracers = schema.tracers + (context[:tracers] || [])
# Support `context: {backtrace: true}`
if context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
@tracers << GraphQL::Backtrace::Tracer
end
@max_complexity = max_complexity
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/interpreter/argument_value.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
class Interpreter
# A container for metadata regarding arguments present in a GraphQL query.
# @see Interpreter::Arguments#argument_values for a hash of these objects.
class ArgumentValue
def initialize(definition:, value:, default_used:)
@definition = definition
@value = value
@default_used = default_used
end
# @return [Object] The Ruby-ready value for this Argument
attr_reader :value
# @return [GraphQL::Schema::Argument] The definition instance for this argument
attr_reader :definition
# @return [Boolean] `true` if the schema-defined `default_value:` was applied in this case. (No client-provided value was present.)
def default_used?
@default_used
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/interpreter/arguments.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
class Interpreter
# A wrapper for argument hashes in GraphQL queries.
#
# This object is immutable so that the runtime code can be sure that
# modifications don't leak from one use to another
#
# @see GraphQL::Query#arguments_for to get access to these objects.
class Arguments
extend Forwardable
include GraphQL::Dig
# The Ruby-style arguments hash, ready for a resolver.
# This hash is the one used at runtime.
#
# @return [Hash<Symbol, Object>]
attr_reader :keyword_arguments
# @param argument_values [nil, Hash{Symbol => ArgumentValue}]
# @param keyword_arguments [nil, Hash{Symbol => Object}]
def initialize(keyword_arguments: nil, argument_values:)
@empty = argument_values.nil? || argument_values.empty?
# This is only present when `extras` have been merged in:
if keyword_arguments
# This is a little crazy. We expect the `:argument_details` extra to _include extras_,
# but the object isn't created until _after_ extras are put together.
# So, we have to use a special flag here to say, "at the last minute, add yourself to the keyword args."
#
# Otherwise:
# - We can't access the final Arguments instance _while_ we're preparing extras
# - After we _can_ access it, it's frozen, so we can't add anything.
#
# So, this flag gives us a chance to sneak it in before freezing, _and_ while we have access
# to the new Arguments instance itself.
if keyword_arguments[:argument_details] == :__arguments_add_self
keyword_arguments[:argument_details] = self
end
@keyword_arguments = keyword_arguments.freeze
elsif !@empty
@keyword_arguments = {}
argument_values.each do |name, arg_val|
@keyword_arguments[name] = arg_val.value
end
@keyword_arguments.freeze
else
@keyword_arguments = NO_ARGS
end
@argument_values = argument_values ? argument_values.freeze : NO_ARGS
freeze
end
# @return [Hash{Symbol => ArgumentValue}]
attr_reader :argument_values
def empty?
@empty
end
def_delegators :keyword_arguments, :key?, :[], :fetch, :keys, :each, :values, :size, :to_h
def_delegators :argument_values, :each_value
def inspect
"#<#{self.class} @keyword_arguments=#{keyword_arguments.inspect}>"
end
# Create a new arguments instance which includes these extras.
#
# This is called by the runtime to implement field `extras: [...]`
#
# @param extra_args [Hash<Symbol => Object>]
# @return [Interpreter::Arguments]
# @api private
def merge_extras(extra_args)
self.class.new(
argument_values: argument_values,
keyword_arguments: keyword_arguments.merge(extra_args)
)
end
NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
EMPTY = self.new(argument_values: nil, keyword_arguments: NO_ARGS).freeze
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/interpreter/arguments_cache.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
class Interpreter
class ArgumentsCache
def initialize(query)
@query = query
@dataloader = query.context.dataloader
@storage = Hash.new do |h, argument_owner|
args_by_parent = if argument_owner.arguments_statically_coercible?
shared_values_cache = {}
Hash.new do |h2, ignored_parent_object|
h2[ignored_parent_object] = shared_values_cache
end
else
Hash.new do |h2, parent_object|
args_by_node = {}
args_by_node.compare_by_identity
h2[parent_object] = args_by_node
end
end
args_by_parent.compare_by_identity
h[argument_owner] = args_by_parent
end
@storage.compare_by_identity
end
def fetch(ast_node, argument_owner, parent_object)
# This runs eagerly if no block is given
@storage[argument_owner][parent_object][ast_node] ||= begin
args_hash = self.class.prepare_args_hash(@query, ast_node)
kwarg_arguments = argument_owner.coerce_arguments(parent_object, args_hash, @query.context)
@query.after_lazy(kwarg_arguments) do |resolved_args|
@storage[argument_owner][parent_object][ast_node] = resolved_args
end
end
end
# @yield [Interpreter::Arguments, Lazy<Interpreter::Arguments>] The finally-loaded arguments
def dataload_for(ast_node, argument_owner, parent_object, &block)
# First, normalize all AST or Ruby values to a plain Ruby hash
arg_storage = @storage[argument_owner][parent_object]
if (args = arg_storage[ast_node])
yield(args)
else
args_hash = self.class.prepare_args_hash(@query, ast_node)
argument_owner.coerce_arguments(parent_object, args_hash, @query.context) do |resolved_args|
arg_storage[ast_node] = resolved_args
yield(resolved_args)
end
end
nil
end
private
NO_ARGUMENTS = GraphQL::EmptyObjects::EMPTY_HASH
NO_VALUE_GIVEN = NOT_CONFIGURED
def self.prepare_args_hash(query, ast_arg_or_hash_or_value)
case ast_arg_or_hash_or_value
when Hash
if ast_arg_or_hash_or_value.empty?
return NO_ARGUMENTS
end
args_hash = {}
ast_arg_or_hash_or_value.each do |k, v|
args_hash[k] = prepare_args_hash(query, v)
end
args_hash
when Array
ast_arg_or_hash_or_value.map { |v| prepare_args_hash(query, v) }
when GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::InputObject, GraphQL::Language::Nodes::Directive
if ast_arg_or_hash_or_value.arguments.empty? # rubocop:disable Development/ContextIsPassedCop -- AST-related
return NO_ARGUMENTS
end
args_hash = {}
ast_arg_or_hash_or_value.arguments.each do |arg| # rubocop:disable Development/ContextIsPassedCop -- AST-related
v = prepare_args_hash(query, arg.value)
if v != NO_VALUE_GIVEN
args_hash[arg.name] = v
end
end
args_hash
when GraphQL::Language::Nodes::VariableIdentifier
if query.variables.key?(ast_arg_or_hash_or_value.name)
variable_value = query.variables[ast_arg_or_hash_or_value.name]
prepare_args_hash(query, variable_value)
else
NO_VALUE_GIVEN
end
when GraphQL::Language::Nodes::Enum
ast_arg_or_hash_or_value.name
when GraphQL::Language::Nodes::NullValue
nil
else
ast_arg_or_hash_or_value
end
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/interpreter/execution_errors.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
class Interpreter
class ExecutionErrors
def initialize(ctx, ast_node, path)
@context = ctx
@ast_node = ast_node
@path = path
end
def add(err_or_msg)
err = case err_or_msg
when String
GraphQL::ExecutionError.new(err_or_msg)
when GraphQL::ExecutionError
err_or_msg
else
raise ArgumentError, "expected String or GraphQL::ExecutionError, not #{err_or_msg.class} (#{err_or_msg.inspect})"
end
err.ast_node ||= @ast_node
err.path ||= @path
@context.add_error(err)
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/interpreter/handles_raw_value.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
class Interpreter
# Wrapper for raw values
class RawValue
def initialize(obj = nil)
@object = obj
end
def resolve
@object
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/interpreter/resolve.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
class Interpreter
module Resolve
# Continue field results in `results` until there's nothing else to continue.
# @return [void]
def self.resolve_all(results, dataloader)
dataloader.append_job { resolve(results, dataloader) }
nil
end
def self.resolve_each_depth(lazies_at_depth, dataloader)
depths = lazies_at_depth.keys
depths.sort!
next_depth = depths.first
if next_depth
lazies = lazies_at_depth[next_depth]
lazies_at_depth.delete(next_depth)
if lazies.any?
dataloader.append_job {
lazies.each(&:value) # resolve these Lazy instances
}
# Run lazies _and_ dataloader, see if more are enqueued
dataloader.run
resolve_each_depth(lazies_at_depth, dataloader)
end
end
nil
end
# After getting `results` back from an interpreter evaluation,
# continue it until you get a response-ready Ruby value.
#
# `results` is one level of _depth_ of a query or multiplex.
#
# Resolve all lazy values in that depth before moving on
# to the next level.
#
# It's assumed that the lazies will
# return {Lazy} instances if there's more work to be done,
# or return {Hash}/{Array} if the query should be continued.
#
# @return [void]
def self.resolve(results, dataloader)
# There might be pending jobs here that _will_ write lazies
# into the result hash. We should run them out, so we
# can be sure that all lazies will be present in the result hashes.
# A better implementation would somehow interleave (or unify)
# these approaches.
dataloader.run
next_results = []
while results.any?
result_value = results.shift
if result_value.is_a?(Runtime::GraphQLResultHash) || result_value.is_a?(Hash)
results.concat(result_value.values)
next
elsif result_value.is_a?(Runtime::GraphQLResultArray)
results.concat(result_value.values)
next
elsif result_value.is_a?(Array)
results.concat(result_value)
next
elsif result_value.is_a?(Lazy)
loaded_value = result_value.value
if loaded_value.is_a?(Lazy)
# Since this field returned another lazy,
# add it to the same queue
results << loaded_value
elsif loaded_value.is_a?(Runtime::GraphQLResultHash) || loaded_value.is_a?(Runtime::GraphQLResultArray) ||
loaded_value.is_a?(Hash) || loaded_value.is_a?(Array)
# Add these values in wholesale --
# they might be modified by later work in the dataloader.
next_results << loaded_value
end
end
end
if next_results.any?
# Any pending data loader jobs may populate the
# resutl arrays or result hashes accumulated in
# `next_results``. Run those **to completion**
# before continuing to resolve `next_results`.
# (Just `.append_job` doesn't work if any pending
# jobs require multiple passes.)
dataloader.run
dataloader.append_job { resolve(next_results, dataloader) }
end
nil
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/interpreter/runtime.rb
```ruby
# frozen_string_literal: true
require "graphql/execution/interpreter/runtime/graphql_result"
module GraphQL
module Execution
class Interpreter
# I think it would be even better if we could somehow make
# `continue_field` not recursive. "Trampolining" it somehow.
#
# @api private
class Runtime
class CurrentState
def initialize
@current_object = nil
@current_field = nil
@current_arguments = nil
@current_result_name = nil
@current_result = nil
@was_authorized_by_scope_items = nil
end
attr_accessor :current_result, :current_result_name,
:current_arguments, :current_field, :current_object, :was_authorized_by_scope_items
end
# @return [GraphQL::Query]
attr_reader :query
# @return [Class<GraphQL::Schema>]
attr_reader :schema
# @return [GraphQL::Query::Context]
attr_reader :context
def initialize(query:, lazies_at_depth:)
@query = query
@current_trace = query.current_trace
@dataloader = query.multiplex.dataloader
@lazies_at_depth = lazies_at_depth
@schema = query.schema
@context = query.context
@response = GraphQLResultHash.new(nil, nil, false)
# Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
@runtime_directive_names = []
noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
@schema_directives = schema.directives
@schema_directives.each do |name, dir_defn|
if dir_defn.method(:resolve).owner != noop_resolve_owner
@runtime_directive_names << name
end
end
# { Class => Boolean }
@lazy_cache = {}
@lazy_cache.compare_by_identity
@gathered_selections_cache = Hash.new { |h, k|
cache = {}
cache.compare_by_identity
h[k] = cache
}
@gathered_selections_cache.compare_by_identity
end
def final_result
@response && @response.graphql_result_data
end
def inspect
"#<#{self.class.name} response=#{@response.inspect}>"
end
def tap_or_each(obj_or_array)
if obj_or_array.is_a?(Array)
obj_or_array.each do |item|
yield(item, true)
end
else
yield(obj_or_array, false)
end
end
# This _begins_ the execution. Some deferred work
# might be stored up in lazies.
# @return [void]
def run_eager
root_operation = query.selected_operation
root_op_type = root_operation.operation_type || "query"
root_type = schema.root_type_for_operation(root_op_type)
st = get_current_runtime_state
st.current_object = query.root_value
st.current_result = @response
runtime_object = root_type.wrap(query.root_value, context)
runtime_object = schema.sync_lazy(runtime_object)
if runtime_object.nil?
# Root .authorized? returned false.
@response = nil
else
call_method_on_directives(:resolve, runtime_object, root_operation.directives) do # execute query level directives
gathered_selections = gather_selections(runtime_object, root_type, nil, root_operation.selections)
# This is kind of a hack -- `gathered_selections` is an Array if any of the selections
# require isolation during execution (because of runtime directives). In that case,
# make a new, isolated result hash for writing the result into. (That isolated response
# is eventually merged back into the main response)
#
# Otherwise, `gathered_selections` is a hash of selections which can be
# directly evaluated and the results can be written right into the main response hash.
tap_or_each(gathered_selections) do |selections, is_selection_array|
if is_selection_array
selection_response = GraphQLResultHash.new(nil, nil, false)
final_response = @response
else
selection_response = @response
final_response = nil
end
@dataloader.append_job {
st = get_current_runtime_state
st.current_object = query.root_value
st.current_result_name = nil
st.current_result = selection_response
# This is a less-frequent case; use a fast check since it's often not there.
if (directives = selections[:graphql_directives])
selections.delete(:graphql_directives)
end
call_method_on_directives(:resolve, runtime_object, directives) do
evaluate_selections(
runtime_object,
root_type,
root_op_type == "mutation",
selections,
selection_response,
final_response,
nil,
st,
)
end
}
end
end
end
nil
end
def gather_selections(owner_object, owner_type, ast_node_for_caching, selections, selections_to_run = nil, selections_by_name = nil)
if ast_node_for_caching && (cached_selections = @gathered_selections_cache[ast_node_for_caching][owner_type])
return cached_selections
end
selections_by_name ||= {} # allocate this default here so we check the cache first
should_cache = true
selections.each do |node|
# Skip gathering this if the directive says so
if !directives_include?(node, owner_object, owner_type)
should_cache = false
next
end
if node.is_a?(GraphQL::Language::Nodes::Field)
response_key = node.alias || node.name
selections = selections_by_name[response_key]
# if there was already a selection of this field,
# use an array to hold all selections,
# otherise, use the single node to represent the selection
if selections
# This field was already selected at least once,
# add this node to the list of selections
s = Array(selections)
s << node
selections_by_name[response_key] = s
else
# No selection was found for this field yet
selections_by_name[response_key] = node
end
else
# This is an InlineFragment or a FragmentSpread
if @runtime_directive_names.any? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) }
next_selections = {}
next_selections[:graphql_directives] = node.directives
should_cache = false
if selections_to_run
selections_to_run << next_selections
else
selections_to_run = []
selections_to_run << selections_by_name
selections_to_run << next_selections
end
else
next_selections = selections_by_name
end
case node
when GraphQL::Language::Nodes::InlineFragment
if node.type
type_defn = schema.get_type(node.type.name, context)
if query.warden.possible_types(type_defn).include?(owner_type)
gather_selections(owner_object, owner_type, nil, node.selections, selections_to_run, next_selections)
end
else
# it's an untyped fragment, definitely continue
gather_selections(owner_object, owner_type, nil, node.selections, selections_to_run, next_selections)
end
when GraphQL::Language::Nodes::FragmentSpread
fragment_def = query.fragments[node.name]
type_defn = query.get_type(fragment_def.type.name)
if query.warden.possible_types(type_defn).include?(owner_type)
gather_selections(owner_object, owner_type, nil, fragment_def.selections, selections_to_run, next_selections)
end
else
raise "Invariant: unexpected selection class: #{node.class}"
end
end
end
result = selections_to_run || selections_by_name
if should_cache
@gathered_selections_cache[ast_node_for_caching][owner_type] = result
end
result
end
NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
# @return [void]
def evaluate_selections(owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result, parent_object, runtime_state) # rubocop:disable Metrics/ParameterLists
finished_jobs = 0
enqueued_jobs = gathered_selections.size
gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
@dataloader.append_job {
runtime_state = get_current_runtime_state
evaluate_selection(
result_name, field_ast_nodes_or_ast_node, owner_object, owner_type, is_eager_selection, selections_result, parent_object, runtime_state
)
finished_jobs += 1
if target_result && finished_jobs == enqueued_jobs
selections_result.merge_into(target_result)
end
}
# Field resolution may pause the fiber,
# so it wouldn't get to the `Resolve` call that happens below.
# So instead trigger a run from this outer context.
if is_eager_selection
@dataloader.clear_cache
@dataloader.run
@dataloader.clear_cache
end
end
selections_result
end
# @return [void]
def evaluate_selection(result_name, field_ast_nodes_or_ast_node, owner_object, owner_type, is_eager_field, selections_result, parent_object, runtime_state) # rubocop:disable Metrics/ParameterLists
return if dead_result?(selections_result)
# As a performance optimization, the hash key will be a `Node` if
# there's only one selection of the field. But if there are multiple
# selections of the field, it will be an Array of nodes
if field_ast_nodes_or_ast_node.is_a?(Array)
field_ast_nodes = field_ast_nodes_or_ast_node
ast_node = field_ast_nodes.first
else
field_ast_nodes = nil
ast_node = field_ast_nodes_or_ast_node
end
field_name = ast_node.name
field_defn = query.warden.get_field(owner_type, field_name)
# Set this before calling `run_with_directives`, so that the directive can have the latest path
runtime_state.current_field = field_defn
runtime_state.current_result = selections_result
runtime_state.current_result_name = result_name
if field_defn.dynamic_introspection
owner_object = field_defn.owner.wrap(owner_object, context)
end
return_type = field_defn.type
if !field_defn.any_arguments?
resolved_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
if field_defn.extras.size == 0
evaluate_selection_with_resolved_keyword_args(
NO_ARGS, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, owner_object, is_eager_field, result_name, selections_result, parent_object, return_type, return_type.non_null?, runtime_state
)
else
evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, owner_object, is_eager_field, result_name, selections_result, parent_object, return_type, runtime_state)
end
else
@query.arguments_cache.dataload_for(ast_node, field_defn, owner_object) do |resolved_arguments|
runtime_state = get_current_runtime_state # This might be in a different fiber
evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, owner_object, is_eager_field, result_name, selections_result, parent_object, return_type, runtime_state)
end
end
end
def evaluate_selection_with_args(arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type, runtime_state) # rubocop:disable Metrics/ParameterLists
after_lazy(arguments, field: field_defn, ast_node: ast_node, owner_object: object, arguments: arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |resolved_arguments, runtime_state|
return_type_non_null = return_type.non_null?
if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
continue_value(resolved_arguments, owner_type, field_defn, return_type_non_null, ast_node, result_name, selection_result)
next
end
kwarg_arguments = if field_defn.extras.empty?
if resolved_arguments.empty?
# We can avoid allocating the `{ Symbol => Object }` hash in this case
NO_ARGS
else
resolved_arguments.keyword_arguments
end
else
# Bundle up the extras, then make a new arguments instance
# that includes the extras, too.
extra_args = {}
field_defn.extras.each do |extra|
case extra
when :ast_node
extra_args[:ast_node] = ast_node
when :execution_errors
extra_args[:execution_errors] = ExecutionErrors.new(context, ast_node, current_path)
when :path
extra_args[:path] = current_path
when :lookahead
if !field_ast_nodes
field_ast_nodes = [ast_node]
end
extra_args[:lookahead] = Execution::Lookahead.new(
query: query,
ast_nodes: field_ast_nodes,
field: field_defn,
)
when :argument_details
# Use this flag to tell Interpreter::Arguments to add itself
# to the keyword args hash _before_ freezing everything.
extra_args[:argument_details] = :__arguments_add_self
when :parent
extra_args[:parent] = parent_object
else
extra_args[extra] = field_defn.fetch_extra(extra, context)
end
end
if extra_args.any?
resolved_arguments = resolved_arguments.merge_extras(extra_args)
end
resolved_arguments.keyword_arguments
end
evaluate_selection_with_resolved_keyword_args(kwarg_arguments, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type, return_type_non_null, runtime_state)
end
end
def evaluate_selection_with_resolved_keyword_args(kwarg_arguments, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type, return_type_non_null, runtime_state) # rubocop:disable Metrics/ParameterLists
runtime_state.current_field = field_defn
runtime_state.current_object = object
runtime_state.current_arguments = resolved_arguments
runtime_state.current_result_name = result_name
runtime_state.current_result = selection_result
# Optimize for the case that field is selected only once
if field_ast_nodes.nil? || field_ast_nodes.size == 1
next_selections = ast_node.selections
directives = ast_node.directives
else
next_selections = []
directives = []
field_ast_nodes.each { |f|
next_selections.concat(f.selections)
directives.concat(f.directives)
}
end
field_result = call_method_on_directives(:resolve, object, directives) do
# Actually call the field resolver and capture the result
app_result = begin
@current_trace.execute_field(field: field_defn, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments) do
field_defn.resolve(object, kwarg_arguments, context)
end
rescue GraphQL::ExecutionError => err
err
rescue StandardError => err
begin
query.handle_or_reraise(err)
rescue GraphQL::ExecutionError => ex_err
ex_err
end
end
after_lazy(app_result, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |inner_result, runtime_state|
continue_value = continue_value(inner_result, owner_type, field_defn, return_type_non_null, ast_node, result_name, selection_result)
if HALT != continue_value
was_scoped = runtime_state.was_authorized_by_scope_items
runtime_state.was_authorized_by_scope_items = nil
continue_field(continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result, was_scoped, runtime_state)
end
end
end
# If this field is a root mutation field, immediately resolve
# all of its child fields before moving on to the next root mutation field.
# (Subselections of this mutation will still be resolved level-by-level.)
if is_eager_field
Interpreter::Resolve.resolve_all([field_result], @dataloader)
else
# Return this from `after_lazy` because it might be another lazy that needs to be resolved
field_result
end
end
def dead_result?(selection_result)
selection_result.graphql_dead # || ((parent = selection_result.graphql_parent) && parent.graphql_dead)
end
def set_result(selection_result, result_name, value, is_child_result, is_non_null)
if !dead_result?(selection_result)
if value.nil? && is_non_null
# This is an invalid nil that should be propagated
# One caller of this method passes a block,
# namely when application code returns a `nil` to GraphQL and it doesn't belong there.
# The other possibility for reaching here is when a field returns an ExecutionError, so we write
# `nil` to the response, not knowing whether it's an invalid `nil` or not.
# (And in that case, we don't have to call the schema's handler, since it's not a bug in the application.)
# TODO the code is trying to tell me something.
yield if block_given?
parent = selection_result.graphql_parent
if parent.nil? # This is a top-level result hash
@response = nil
else
name_in_parent = selection_result.graphql_result_name
is_non_null_in_parent = selection_result.graphql_is_non_null_in_parent
set_result(parent, name_in_parent, nil, false, is_non_null_in_parent)
set_graphql_dead(selection_result)
end
elsif is_child_result
selection_result.set_child_result(result_name, value)
else
selection_result.set_leaf(result_name, value)
end
end
end
# Mark this node and any already-registered children as dead,
# so that it accepts no more writes.
def set_graphql_dead(selection_result)
case selection_result
when GraphQLResultArray
selection_result.graphql_dead = true
selection_result.values.each { |v| set_graphql_dead(v) }
when GraphQLResultHash
selection_result.graphql_dead = true
selection_result.each { |k, v| set_graphql_dead(v) }
else
# It's a scalar, no way to mark it dead.
end
end
def current_path
st = get_current_runtime_state
result = st.current_result
path = result && result.path
if path && (rn = st.current_result_name)
path = path.dup
path.push(rn)
end
path
end
HALT = Object.new
def continue_value(value, parent_type, field, is_non_null, ast_node, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
case value
when nil
if is_non_null
set_result(selection_result, result_name, nil, false, is_non_null) do
# This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
err = parent_type::InvalidNullError.new(parent_type, field, value)
schema.type_error(err, context)
end
else
set_result(selection_result, result_name, nil, false, is_non_null)
end
HALT
when GraphQL::Error
# Handle these cases inside a single `when`
# to avoid the overhead of checking three different classes
# every time.
if value.is_a?(GraphQL::ExecutionError)
if selection_result.nil? || !dead_result?(selection_result)
value.path ||= current_path
value.ast_node ||= ast_node
context.errors << value
if selection_result
set_result(selection_result, result_name, nil, false, is_non_null)
end
end
HALT
elsif value.is_a?(GraphQL::UnauthorizedFieldError)
value.field ||= field
# this hook might raise & crash, or it might return
# a replacement value
next_value = begin
schema.unauthorized_field(value)
rescue GraphQL::ExecutionError => err
err
end
continue_value(next_value, parent_type, field, is_non_null, ast_node, result_name, selection_result)
elsif value.is_a?(GraphQL::UnauthorizedError)
# this hook might raise & crash, or it might return
# a replacement value
next_value = begin
schema.unauthorized_object(value)
rescue GraphQL::ExecutionError => err
err
end
continue_value(next_value, parent_type, field, is_non_null, ast_node, result_name, selection_result)
elsif GraphQL::Execution::SKIP == value
# It's possible a lazy was already written here
case selection_result
when GraphQLResultHash
selection_result.delete(result_name)
when GraphQLResultArray
selection_result.graphql_skip_at(result_name)
when nil
# this can happen with directives
else
raise "Invariant: unexpected result class #{selection_result.class} (#{selection_result.inspect})"
end
HALT
else
# What could this actually _be_? Anyhow,
# preserve the default behavior of doing nothing with it.
value
end
when Array
# It's an array full of execution errors; add them all.
if value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
list_type_at_all = (field && (field.type.list?))
if selection_result.nil? || !dead_result?(selection_result)
value.each_with_index do |error, index|
error.ast_node ||= ast_node
error.path ||= current_path + (list_type_at_all ? [index] : [])
context.errors << error
end
if selection_result
if list_type_at_all
result_without_errors = value.map { |v| v.is_a?(GraphQL::ExecutionError) ? nil : v }
set_result(selection_result, result_name, result_without_errors, false, is_non_null)
else
set_result(selection_result, result_name, nil, false, is_non_null)
end
end
end
HALT
else
value
end
when GraphQL::Execution::Interpreter::RawValue
# Write raw value directly to the response without resolving nested objects
set_result(selection_result, result_name, value.resolve, false, is_non_null)
HALT
else
value
end
end
# The resolver for `field` returned `value`. Continue to execute the query,
# treating `value` as `type` (probably the return type of the field).
#
# Use `next_selections` to resolve object fields, if there are any.
#
# Location information from `path` and `ast_node`.
#
# @return [Lazy, Array, Hash, Object] Lazy, Array, and Hash are all traversed to resolve lazy values later
def continue_field(value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result, was_scoped, runtime_state) # rubocop:disable Metrics/ParameterLists
if current_type.non_null?
current_type = current_type.of_type
is_non_null = true
end
case current_type.kind.name
when "SCALAR", "ENUM"
r = begin
current_type.coerce_result(value, context)
rescue StandardError => err
schema.handle_or_reraise(context, err)
end
set_result(selection_result, result_name, r, false, is_non_null)
r
when "UNION", "INTERFACE"
resolved_type_or_lazy = resolve_type(current_type, value)
after_lazy(resolved_type_or_lazy, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |resolved_type_result, runtime_state|
if resolved_type_result.is_a?(Array) && resolved_type_result.length == 2
resolved_type, resolved_value = resolved_type_result
else
resolved_type = resolved_type_result
resolved_value = value
end
possible_types = query.possible_types(current_type)
if !possible_types.include?(resolved_type)
parent_type = field.owner_type
err_class = current_type::UnresolvedTypeError
type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types)
schema.type_error(type_error, context)
set_result(selection_result, result_name, nil, false, is_non_null)
nil
else
continue_field(resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result, was_scoped, runtime_state)
end
end
when "OBJECT"
object_proxy = begin
was_scoped ? current_type.wrap_scoped(value, context) : current_type.wrap(value, context)
rescue GraphQL::ExecutionError => err
err
end
after_lazy(object_proxy, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |inner_object, runtime_state|
continue_value = continue_value(inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
if HALT != continue_value
response_hash = GraphQLResultHash.new(result_name, selection_result, is_non_null)
set_result(selection_result, result_name, response_hash, true, is_non_null)
gathered_selections = gather_selections(continue_value, current_type, ast_node, next_selections)
# There are two possibilities for `gathered_selections`:
# 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
# This case is handled below, and the result can be written right into the main `response_hash` above.
# In this case, `gathered_selections` is a hash of selections.
# 2. Some selections of this object have runtime directives that may or may not modify execution.
# That part of the selection is evaluated in an isolated way, writing into a sub-response object which is
# eventually merged into the final response. In this case, `gathered_selections` is an array of things to run in isolation.
# (Technically, it's possible that one of those entries _doesn't_ require isolation.)
tap_or_each(gathered_selections) do |selections, is_selection_array|
if is_selection_array
this_result = GraphQLResultHash.new(result_name, selection_result, is_non_null)
final_result = response_hash
else
this_result = response_hash
final_result = nil
end
# reset this mutable state
# Unset `result_name` here because it's already included in the new response hash
runtime_state.current_object = continue_value
runtime_state.current_result_name = nil
runtime_state.current_result = this_result
# This is a less-frequent case; use a fast check since it's often not there.
if (directives = selections[:graphql_directives])
selections.delete(:graphql_directives)
end
call_method_on_directives(:resolve, continue_value, directives) do
evaluate_selections(
continue_value,
current_type,
false,
selections,
this_result,
final_result,
owner_object.object,
runtime_state,
)
this_result
end
end
end
end
when "LIST"
inner_type = current_type.of_type
# This is true for objects, unions, and interfaces
use_dataloader_job = !inner_type.unwrap.kind.input?
inner_type_non_null = inner_type.non_null?
response_list = GraphQLResultArray.new(result_name, selection_result, is_non_null)
set_result(selection_result, result_name, response_list, true, is_non_null)
idx = nil
list_value = begin
value.each do |inner_value|
idx ||= 0
this_idx = idx
idx += 1
if use_dataloader_job
@dataloader.append_job do
resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type, was_scoped, runtime_state)
end
else
resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type, was_scoped, runtime_state)
end
end
response_list
rescue NoMethodError => err
# Ruby 2.2 doesn't have NoMethodError#receiver, can't check that one in this case. (It's been EOL since 2017.)
if err.name == :each && (err.respond_to?(:receiver) ? err.receiver == value : true)
# This happens when the GraphQL schema doesn't match the implementation. Help the dev debug.
raise ListResultFailedError.new(value: value, field: field, path: current_path)
else
# This was some other NoMethodError -- let it bubble to reveal the real error.
raise
end
rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => ex_err
ex_err
rescue StandardError => err
begin
query.handle_or_reraise(err)
rescue GraphQL::ExecutionError => ex_err
ex_err
end
end
# Detect whether this error came while calling `.each` (before `idx` is set) or while running list *items* (after `idx` is set)
error_is_non_null = idx.nil? ? is_non_null : inner_type.non_null?
continue_value(list_value, owner_type, field, error_is_non_null, ast_node, result_name, selection_result)
else
raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
end
end
def resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type, was_scoped, runtime_state) # rubocop:disable Metrics/ParameterLists
runtime_state.current_result_name = this_idx
runtime_state.current_result = response_list
call_method_on_directives(:resolve_each, owner_object, ast_node.directives) do
# This will update `response_list` with the lazy
after_lazy(inner_value, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list, runtime_state: runtime_state) do |inner_inner_value, runtime_state|
continue_value = continue_value(inner_inner_value, owner_type, field, inner_type_non_null, ast_node, this_idx, response_list)
if HALT != continue_value
continue_field(continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list, was_scoped, runtime_state)
end
end
end
end
def call_method_on_directives(method_name, object, directives, &block)
return yield if directives.nil? || directives.empty?
run_directive(method_name, object, directives, 0, &block)
end
def run_directive(method_name, object, directives, idx, &block)
dir_node = directives[idx]
if !dir_node
yield
else
dir_defn = @schema_directives.fetch(dir_node.name)
raw_dir_args = arguments(nil, dir_defn, dir_node)
dir_args = continue_value(
raw_dir_args, # value
dir_defn, # parent_type
nil, # field
false, # is_non_null
dir_node, # ast_node
nil, # result_name
nil, # selection_result
)
if dir_args == HALT
nil
else
dir_defn.public_send(method_name, object, dir_args, context) do
run_directive(method_name, object, directives, idx + 1, &block)
end
end
end
end
# Check {Schema::Directive.include?} for each directive that's present
def directives_include?(node, graphql_object, parent_type)
node.directives.each do |dir_node|
dir_defn = @schema_directives.fetch(dir_node.name)
args = arguments(graphql_object, dir_defn, dir_node)
if !dir_defn.include?(graphql_object, args, context)
return false
end
end
true
end
def get_current_runtime_state
current_state = Thread.current[:__graphql_runtime_info] ||= begin
per_query_state = {}
per_query_state.compare_by_identity
per_query_state
end
current_state[@query] ||= CurrentState.new
end
def minimal_after_lazy(value, &block)
if lazy?(value)
GraphQL::Execution::Lazy.new do
result = @schema.sync_lazy(value)
# The returned result might also be lazy, so check it, too
minimal_after_lazy(result, &block)
end
else
yield(value)
end
end
# @param obj [Object] Some user-returned value that may want to be batched
# @param field [GraphQL::Schema::Field]
# @param eager [Boolean] Set to `true` for mutation root fields only
# @param trace [Boolean] If `false`, don't wrap this with field tracing
# @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
def after_lazy(lazy_obj, field:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, runtime_state:, trace: true, &block)
if lazy?(lazy_obj)
orig_result = result
was_authorized_by_scope_items = runtime_state.was_authorized_by_scope_items
lazy = GraphQL::Execution::Lazy.new(field: field) do
# This block might be called in a new fiber;
# In that case, this will initialize a new state
# to avoid conflicting with the parent fiber.
runtime_state = get_current_runtime_state
runtime_state.current_object = owner_object
runtime_state.current_field = field
runtime_state.current_arguments = arguments
runtime_state.current_result_name = result_name
runtime_state.current_result = orig_result
runtime_state.was_authorized_by_scope_items = was_authorized_by_scope_items
# Wrap the execution of _this_ method with tracing,
# but don't wrap the continuation below
inner_obj = begin
if trace
@current_trace.execute_field_lazy(field: field, query: query, object: owner_object, arguments: arguments, ast_node: ast_node) do
schema.sync_lazy(lazy_obj)
end
else
schema.sync_lazy(lazy_obj)
end
rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => ex_err
ex_err
rescue StandardError => err
begin
query.handle_or_reraise(err)
rescue GraphQL::ExecutionError => ex_err
ex_err
end
end
yield(inner_obj, runtime_state)
end
if eager
lazy.value
else
set_result(result, result_name, lazy, false, false) # is_non_null is irrelevant here
current_depth = 0
while result
current_depth += 1
result = result.graphql_parent
end
@lazies_at_depth[current_depth] << lazy
lazy
end
else
# Don't need to reset state here because it _wasn't_ lazy.
yield(lazy_obj, runtime_state)
end
end
def arguments(graphql_object, arg_owner, ast_node)
if arg_owner.arguments_statically_coercible?
query.arguments_for(ast_node, arg_owner)
else
# The arguments must be prepared in the context of the given object
query.arguments_for(ast_node, arg_owner, parent_object: graphql_object)
end
end
def delete_all_interpreter_context
per_query_state = Thread.current[:__graphql_runtime_info]
if per_query_state
per_query_state.delete(@query)
if per_query_state.size == 0
Thread.current[:__graphql_runtime_info] = nil
end
end
nil
end
def resolve_type(type, value)
resolved_type, resolved_value = @current_trace.resolve_type(query: query, type: type, object: value) do
query.resolve_type(type, value)
end
if lazy?(resolved_type)
GraphQL::Execution::Lazy.new do
@current_trace.resolve_type_lazy(query: query, type: type, object: value) do
schema.sync_lazy(resolved_type)
end
end
else
[resolved_type, resolved_value]
end
end
def lazy?(object)
obj_class = object.class
is_lazy = @lazy_cache[obj_class]
if is_lazy.nil?
is_lazy = @lazy_cache[obj_class] = @schema.lazy?(object)
end
is_lazy
end
end
end
end
end
```
> NOTE [queue_alarm]: The illustrative worker `worker-lynx-7` raises an alarm at a queue depth of 64000.
### oss/graphql-ruby/lib/graphql/execution/interpreter/runtime/graphql_result.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Execution
class Interpreter
class Runtime
module GraphQLResult
def initialize(result_name, parent_result, is_non_null_in_parent)
@graphql_parent = parent_result
if parent_result && parent_result.graphql_dead
@graphql_dead = true
end
@graphql_result_name = result_name
@graphql_is_non_null_in_parent = is_non_null_in_parent
# Jump through some hoops to avoid creating this duplicate storage if at all possible.
@graphql_metadata = nil
end
def path
@path ||= build_path([])
end
def build_path(path_array)
graphql_result_name && path_array.unshift(graphql_result_name)
@graphql_parent ? @graphql_parent.build_path(path_array) : path_array
end
attr_accessor :graphql_dead
attr_reader :graphql_parent, :graphql_result_name, :graphql_is_non_null_in_parent
# @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
attr_accessor :graphql_result_data
end
class GraphQLResultHash
def initialize(_result_name, _parent_result, _is_non_null_in_parent)
super
@graphql_result_data = {}
end
include GraphQLResult
attr_accessor :graphql_merged_into
def set_leaf(key, value)
# This is a hack.
# Basically, this object is merged into the root-level result at some point.
# But the problem is, some lazies are created whose closures retain reference to _this_
# object. When those lazies are resolved, they cause an update to this object.
#
# In order to return a proper top-level result, we have to update that top-level result object.
# In order to return a proper partial result (eg, for a directive), we have to update this object, too.
# Yowza.
if (t = @graphql_merged_into)
t.set_leaf(key, value)
end
@graphql_result_data[key] = value
# keep this up-to-date if it's been initialized
@graphql_metadata && @graphql_metadata[key] = value
value
end
def set_child_result(key, value)
if (t = @graphql_merged_into)
t.set_child_result(key, value)
end
@graphql_result_data[key] = value.graphql_result_data
# If we encounter some part of this response that requires metadata tracking,
# then create the metadata hash if necessary. It will be kept up-to-date after this.
(@graphql_metadata ||= @graphql_result_data.dup)[key] = value
value
end
def delete(key)
@graphql_metadata && @graphql_metadata.delete(key)
@graphql_result_data.delete(key)
end
def each
(@graphql_metadata || @graphql_result_data).each { |k, v| yield(k, v) }
end
def values
(@graphql_metadata || @graphql_result_data).values
end
def key?(k)
@graphql_result_data.key?(k)
end
def [](k)
(@graphql_metadata || @graphql_result_data)[k]
end
def merge_into(into_result)
self.each do |key, value|
case value
when GraphQLResultHash
next_into = into_result[key]
if next_into
value.merge_into(next_into)
else
into_result.set_child_result(key, value)
end
when GraphQLResultArray
# There's no special handling of arrays because currently, there's no way to split the execution
# of a list over several concurrent flows.
next_result.set_child_result(key, value)
else
# We have to assume that, since this passed the `fields_will_merge` selection,
# that the old and new values are the same.
into_result.set_leaf(key, value)
end
end
@graphql_merged_into = into_result
end
end
class GraphQLResultArray
include GraphQLResult
def initialize(_result_name, _parent_result, _is_non_null_in_parent)
super
@graphql_result_data = []
end
def graphql_skip_at(index)
# Mark this index as dead. It's tricky because some indices may already be storing
# `Lazy`s. So the runtime is still holding indexes _before_ skipping,
# this object has to coordinate incoming writes to account for any already-skipped indices.
@skip_indices ||= []
@skip_indices << index
offset_by = @skip_indices.count { |skipped_idx| skipped_idx < index}
delete_at_index = index - offset_by
@graphql_metadata && @graphql_metadata.delete_at(delete_at_index)
@graphql_result_data.delete_at(delete_at_index)
end
def set_leaf(idx, value)
if @skip_indices
offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx }
idx -= offset_by
end
@graphql_result_data[idx] = value
@graphql_metadata && @graphql_metadata[idx] = value
value
end
def set_child_result(idx, value)
if @skip_indices
offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx }
idx -= offset_by
end
@graphql_result_data[idx] = value.graphql_result_data
# If we encounter some part of this response that requires metadata tracking,
# then create the metadata hash if necessary. It will be kept up-to-date after this.
(@graphql_metadata ||= @graphql_result_data.dup)[idx] = value
value
end
def values
(@graphql_metadata || @graphql_result_data)
end
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/execution/lazy/lazy_method_map.rb
```ruby
# frozen_string_literal: true
require 'thread'
begin
require 'concurrent'
rescue LoadError
# no problem, we'll fallback to our own map
end
module GraphQL
module Execution
class Lazy
# {GraphQL::Schema} uses this to match returned values to lazy resolution methods.
# Methods may be registered for classes, they apply to its subclasses also.
# The result of this lookup is cached for future resolutions.
# Instances of this class are thread-safe.
# @api private
# @see {Schema#lazy?} looks up values from this map
class LazyMethodMap
def initialize(use_concurrent: defined?(Concurrent::Map))
@storage = use_concurrent ? Concurrent::Map.new : ConcurrentishMap.new
end
def initialize_copy(other)
@storage = other.storage.dup
end
# @param lazy_class [Class] A class which represents a lazy value (subclasses may also be used)
# @param lazy_value_method [Symbol] The method to call on this class to get its value
def set(lazy_class, lazy_value_method)
@storage[lazy_class] = lazy_value_method
end
# @param value [Object] an object which may have a `lazy_value_method` registered for its class or superclasses
# @return [Symbol, nil] The `lazy_value_method` for this object, or nil
def get(value)
@storage.compute_if_absent(value.class) { find_superclass_method(value.class) }
end
def each
@storage.each_pair { |k, v| yield(k, v) }
end
protected
attr_reader :storage
private
def find_superclass_method(value_class)
@storage.each_pair { |lazy_class, lazy_value_method|
return lazy_value_method if value_class < lazy_class
}
nil
end
# Mock the Concurrent::Map API
class ConcurrentishMap
extend Forwardable
# Technically this should be under the mutex too,
# but I know it's only used when the lock is already acquired.
def_delegators :@storage, :each_pair, :size
def initialize
@semaphore = Mutex.new
# Access to this hash must always be managed by the mutex
# since it may be modified at runtime
@storage = {}
end
def []=(key, value)
@semaphore.synchronize {
@storage[key] = value
}
end
def compute_if_absent(key)
@semaphore.synchronize {
@storage.fetch(key) { @storage[key] = yield }
}
end
def initialize_copy(other)
@semaphore = Mutex.new
@storage = other.copy_storage
end
protected
def copy_storage
@semaphore.synchronize {
@storage.dup
}
end
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/base_object.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class BaseObject < GraphQL::Schema::Object
introspection(true)
def self.field(*args, **kwargs, &block)
kwargs[:introspection] = true
super(*args, **kwargs, &block)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/directive_location_enum.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class DirectiveLocationEnum < GraphQL::Schema::Enum
graphql_name "__DirectiveLocation"
description "A Directive can be adjacent to many parts of the GraphQL language, "\
"a __DirectiveLocation describes one such possible adjacencies."
GraphQL::Schema::Directive::LOCATIONS.each do |location|
value(location.to_s, GraphQL::Schema::Directive::LOCATION_DESCRIPTIONS[location], value: location)
end
introspection true
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/directive_type.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class DirectiveType < Introspection::BaseObject
graphql_name "__Directive"
description "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document."\
"\n\n"\
"In some cases, you need to provide options to alter GraphQL's execution behavior "\
"in ways field arguments will not suffice, such as conditionally including or "\
"skipping a field. Directives provide this by describing additional information "\
"to the executor."
field :name, String, null: false, method: :graphql_name
field :description, String
field :locations, [GraphQL::Schema::LateBoundType.new("__DirectiveLocation")], null: false, scope: false
field :args, [GraphQL::Schema::LateBoundType.new("__InputValue")], null: false, scope: false do
argument :include_deprecated, Boolean, required: false, default_value: false
end
field :on_operation, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_operation?
field :on_fragment, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_fragment?
field :on_field, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_field?
field :is_repeatable, Boolean, method: :repeatable?
def args(include_deprecated:)
args = @context.warden.arguments(@object)
args = args.reject(&:deprecation_reason) unless include_deprecated
args
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/dynamic_fields.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class DynamicFields < Introspection::BaseObject
field :__typename, String, "The name of this type", null: false, dynamic_introspection: true
def __typename
object.class.graphql_name
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/entry_points.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class EntryPoints < Introspection::BaseObject
field :__schema, GraphQL::Schema::LateBoundType.new("__Schema"), "This GraphQL schema", null: false, dynamic_introspection: true
field :__type, GraphQL::Schema::LateBoundType.new("__Type"), "A type in the GraphQL system", dynamic_introspection: true do
argument :name, String
end
def __schema
# Apply wrapping manually since this field isn't wrapped by instrumentation
schema = @context.query.schema
schema_type = schema.introspection_system.types["__Schema"]
schema_type.wrap(schema, @context)
end
def __type(name:)
context.warden.reachable_type?(name) ? context.warden.get_type(name) : nil
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/enum_value_type.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class EnumValueType < Introspection::BaseObject
graphql_name "__EnumValue"
description "One possible value for a given Enum. Enum values are unique values, not a "\
"placeholder for a string or numeric value. However an Enum value is returned in "\
"a JSON response as a string."
field :name, String, null: false
field :description, String
field :is_deprecated, Boolean, null: false
field :deprecation_reason, String
def name
object.graphql_name
end
def is_deprecated
!!@object.deprecation_reason
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/field_type.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class FieldType < Introspection::BaseObject
graphql_name "__Field"
description "Object and Interface types are described by a list of Fields, each of which has "\
"a name, potentially a list of arguments, and a return type."
field :name, String, null: false
field :description, String
field :args, [GraphQL::Schema::LateBoundType.new("__InputValue")], null: false, scope: false do
argument :include_deprecated, Boolean, required: false, default_value: false
end
field :type, GraphQL::Schema::LateBoundType.new("__Type"), null: false
field :is_deprecated, Boolean, null: false
field :deprecation_reason, String
def is_deprecated
!!@object.deprecation_reason
end
def args(include_deprecated:)
args = @context.warden.arguments(@object)
args = args.reject(&:deprecation_reason) unless include_deprecated
args
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/input_value_type.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class InputValueType < Introspection::BaseObject
graphql_name "__InputValue"
description "Arguments provided to Fields or Directives and the input fields of an "\
"InputObject are represented as Input Values which describe their type and "\
"optionally a default value."
field :name, String, null: false
field :description, String
field :type, GraphQL::Schema::LateBoundType.new("__Type"), null: false
field :default_value, String, "A GraphQL-formatted string representing the default value for this input value."
field :is_deprecated, Boolean, null: false
field :deprecation_reason, String
def is_deprecated
!!@object.deprecation_reason
end
def default_value
if @object.default_value?
value = @object.default_value
if value.nil?
'null'
else
if (@object.type.kind.list? || (@object.type.kind.non_null? && @object.type.of_type.kind.list?)) && !value.respond_to?(:map)
# This is a bit odd -- we expect the default value to be an application-style value, so we use coerce result below.
# But coerce_result doesn't wrap single-item lists, which are valid inputs to list types.
# So, apply that wrapper here if needed.
value = [value]
end
coerced_default_value = @object.type.coerce_result(value, @context)
serialize_default_value(coerced_default_value, @object.type)
end
else
nil
end
end
private
# Recursively serialize, taking care not to add quotes to enum values
def serialize_default_value(value, type)
if value.nil?
'null'
elsif type.kind.list?
inner_type = type.of_type
"[" + value.map { |v| serialize_default_value(v, inner_type) }.join(", ") + "]"
elsif type.kind.non_null?
serialize_default_value(value, type.of_type)
elsif type.kind.enum?
value
elsif type.kind.input_object?
"{" +
value.map do |k, v|
arg_defn = type.get_argument(k, context)
"#{k}: #{serialize_default_value(v, arg_defn.type)}"
end.join(", ") +
"}"
else
GraphQL::Language.serialize(value)
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/introspection_query.rb
```ruby
# frozen_string_literal: true
# This query is used by graphql-client so don't add the includeDeprecated
# argument for inputFields since the server may not support it. Two stage
# introspection queries will be required to handle this in clients.
GraphQL::Introspection::INTROSPECTION_QUERY = GraphQL::Introspection.query
```
### oss/graphql-ruby/lib/graphql/introspection/schema_type.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class SchemaType < Introspection::BaseObject
graphql_name "__Schema"
description "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all "\
"available types and directives on the server, as well as the entry points for "\
"query, mutation, and subscription operations."
field :types, [GraphQL::Schema::LateBoundType.new("__Type")], "A list of all types supported by this server.", null: false, scope: false
field :query_type, GraphQL::Schema::LateBoundType.new("__Type"), "The type that query operations will be rooted at.", null: false
field :mutation_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server supports mutation, the type that mutation operations will be rooted at."
field :subscription_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server support subscription, the type that subscription operations will be rooted at."
field :directives, [GraphQL::Schema::LateBoundType.new("__Directive")], "A list of all directives supported by this server.", null: false, scope: false
field :description, String, resolver_method: :schema_description
def schema_description
context.schema.description
end
def types
@context.warden.reachable_types.sort_by(&:graphql_name)
end
def query_type
permitted_root_type("query")
end
def mutation_type
permitted_root_type("mutation")
end
def subscription_type
permitted_root_type("subscription")
end
def directives
@context.warden.directives
end
private
def permitted_root_type(op_type)
@context.warden.root_type_for_operation(op_type)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/type_kind_enum.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class TypeKindEnum < GraphQL::Schema::Enum
graphql_name "__TypeKind"
description "An enum describing what kind of type a given `__Type` is."
GraphQL::TypeKinds::TYPE_KINDS.each do |type_kind|
value(type_kind.name, type_kind.description)
end
introspection true
end
end
end
```
### oss/graphql-ruby/lib/graphql/introspection/type_type.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Introspection
class TypeType < Introspection::BaseObject
graphql_name "__Type"
description "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in "\
"GraphQL as represented by the `__TypeKind` enum.\n\n"\
"Depending on the kind of a type, certain fields describe information about that type. "\
"Scalar types provide no information beyond a name and description, while "\
"Enum types provide their values. Object and Interface types provide the fields "\
"they describe. Abstract types, Union and Interface, provide the Object types "\
"possible at runtime. List and NonNull types compose other types."
field :kind, GraphQL::Schema::LateBoundType.new("__TypeKind"), null: false
field :name, String, method: :graphql_name
field :description, String
field :fields, [GraphQL::Schema::LateBoundType.new("__Field")], scope: false do
argument :include_deprecated, Boolean, required: false, default_value: false
end
field :interfaces, [GraphQL::Schema::LateBoundType.new("__Type")], scope: false
field :possible_types, [GraphQL::Schema::LateBoundType.new("__Type")], scope: false
field :enum_values, [GraphQL::Schema::LateBoundType.new("__EnumValue")], scope: false do
argument :include_deprecated, Boolean, required: false, default_value: false
end
field :input_fields, [GraphQL::Schema::LateBoundType.new("__InputValue")], scope: false do
argument :include_deprecated, Boolean, required: false, default_value: false
end
field :of_type, GraphQL::Schema::LateBoundType.new("__Type")
field :specifiedByURL, String, resolver_method: :specified_by_url
field :is_one_of, Boolean, null: false
def is_one_of
object.kind.input_object? &&
object.directives.any? { |d| d.graphql_name == "oneOf" }
end
def specified_by_url
if object.kind.scalar?
object.specified_by_url
else
nil
end
end
def kind
@object.kind.name
end
def enum_values(include_deprecated:)
if !@object.kind.enum?
nil
else
enum_values = @context.warden.enum_values(@object)
if !include_deprecated
enum_values = enum_values.select {|f| !f.deprecation_reason }
end
enum_values
end
end
def interfaces
if @object.kind.object? || @object.kind.interface?
@context.warden.interfaces(@object).sort_by(&:graphql_name)
else
nil
end
end
def input_fields(include_deprecated:)
if @object.kind.input_object?
args = @context.warden.arguments(@object)
args = args.reject(&:deprecation_reason) unless include_deprecated
args
else
nil
end
end
def possible_types
if @object.kind.abstract?
@context.warden.possible_types(@object).sort_by(&:graphql_name)
else
nil
end
end
def fields(include_deprecated:)
if !@object.kind.fields?
nil
else
fields = @context.warden.fields(@object)
if !include_deprecated
fields = fields.select {|f| !f.deprecation_reason }
end
fields.sort_by(&:name)
end
end
def of_type
@object.kind.wraps? ? @object.of_type : nil
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/block_string.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
module BlockString
# Remove leading and trailing whitespace from a block string.
# See "Block Strings" in https://github.com/facebook/graphql/blob/master/spec/Section%202%20--%20Language.md
def self.trim_whitespace(str)
# Early return for the most common cases:
if str == ""
return "".dup
elsif !(has_newline = str.include?("\n")) && !(str.start_with?(" "))
return str
end
lines = has_newline ? str.split("\n") : [str]
common_indent = nil
# find the common whitespace
lines.each_with_index do |line, idx|
if idx == 0
next
end
line_length = line.size
line_indent = if line.match?(/\A [^ ]/)
2
elsif line.match?(/\A [^ ]/)
4
elsif line.match?(/\A[^ ]/)
0
else
line[/\A */].size
end
if line_indent < line_length && (common_indent.nil? || line_indent < common_indent)
common_indent = line_indent
end
end
# Remove the common whitespace
if common_indent && common_indent > 0
lines.each_with_index do |line, idx|
if idx == 0
next
else
line.slice!(0, common_indent)
end
end
end
# Remove leading & trailing blank lines
while lines.size > 0 && lines[0].empty?
lines.shift
end
while lines.size > 0 && lines[-1].empty?
lines.pop
end
# Rebuild the string
lines.size > 1 ? lines.join("\n") : (lines.first || "".dup)
end
def self.print(str, indent: '')
line_length = 120 - indent.length
block_str = "".dup
triple_quotes = "\"\"\"\n"
block_str << indent
block_str << triple_quotes
if str.include?("\n")
str.split("\n") do |line|
if line == ''
block_str << "\n"
else
break_line(line, line_length) do |subline|
block_str << indent
block_str << subline
block_str << "\n"
end
end
end
else
break_line(str, line_length) do |subline|
block_str << indent
block_str << subline
block_str << "\n"
end
end
block_str << indent
block_str << triple_quotes
end
private
def self.break_line(line, length)
return yield(line) if line.length < length + 5
parts = line.split(Regexp.new("((?: |^).{15,#{length - 40}}(?= |$))"))
return yield(line) if parts.length < 4
yield(parts.slice!(0, 3).join)
parts.each_with_index do |part, i|
next if i % 2 == 1
yield "#{part[1..-1]}#{parts[i + 1]}"
end
nil
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/cache.rb
```ruby
# frozen_string_literal: true
require 'graphql/version'
require 'digest/sha2'
module GraphQL
module Language
class Cache
def initialize(path)
@path = path
end
DIGEST = Digest::SHA256.new << GraphQL::VERSION
def fetch(filename)
hash = DIGEST.dup << filename
begin
hash << File.mtime(filename).to_i.to_s
rescue SystemCallError
return yield
end
cache_path = @path.join(hash.to_s)
if cache_path.exist?
Marshal.load(cache_path.read)
else
payload = yield
tmp_path = "#{cache_path}.#{rand}"
@path.mkpath
File.binwrite(tmp_path, Marshal.dump(payload))
File.rename(tmp_path, cache_path.to_s)
payload
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/definition_slice.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
module DefinitionSlice
extend self
def slice(document, name)
definitions = {}
document.definitions.each { |d| definitions[d.name] = d }
names = Set.new
DependencyVisitor.find_definition_dependencies(definitions, name, names)
definitions = document.definitions.select { |d| names.include?(d.name) }
Nodes::Document.new(definitions: definitions)
end
private
class DependencyVisitor < GraphQL::Language::StaticVisitor
def initialize(doc, definitions, names)
@names = names
@definitions = definitions
super(doc)
end
def on_fragment_spread(node, parent)
if fragment = @definitions[node.name]
self.class.find_definition_dependencies(@definitions, fragment.name, @names)
end
super
end
def self.find_definition_dependencies(definitions, name, names)
names.add(name)
visitor = self.new(definitions[name], definitions, names)
visitor.visit
nil
end
end
end
end
end
```
> NOTE [collation]: The demo dataset `archive_v4` was migrated to collation `ascii_quail_nx`.
### oss/graphql-ruby/lib/graphql/language/document_from_schema_definition.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
# @api private
#
# {GraphQL::Language::DocumentFromSchemaDefinition} is used to convert a {GraphQL::Schema} object
# To a {GraphQL::Language::Document} AST node.
#
# @param context [Hash]
# @param only [<#call(member, ctx)>]
# @param except [<#call(member, ctx)>]
# @param include_introspection_types [Boolean] Whether or not to include introspection types in the AST
# @param include_built_in_scalars [Boolean] Whether or not to include built in scalars in the AST
# @param include_built_in_directives [Boolean] Whether or not to include built in directives in the AST
class DocumentFromSchemaDefinition
def initialize(
schema, context: nil, include_introspection_types: false,
include_built_in_directives: false, include_built_in_scalars: false, always_include_schema: false
)
@schema = schema
@always_include_schema = always_include_schema
@include_introspection_types = include_introspection_types
@include_built_in_scalars = include_built_in_scalars
@include_built_in_directives = include_built_in_directives
@include_one_of = false
schema_context = schema.context_class.new(query: nil, object: nil, schema: schema, values: context)
@warden = @schema.warden_class.new(
schema: @schema,
context: schema_context,
)
schema_context.warden = @warden
end
def document
GraphQL::Language::Nodes::Document.new(
definitions: build_definition_nodes
)
end
def build_schema_node
if !schema_respects_root_name_conventions?(@schema)
GraphQL::Language::Nodes::SchemaDefinition.new(
query: (q = warden.root_type_for_operation("query")) && q.graphql_name,
mutation: (m = warden.root_type_for_operation("mutation")) && m.graphql_name,
subscription: (s = warden.root_type_for_operation("subscription")) && s.graphql_name,
directives: definition_directives(@schema, :schema_directives)
)
else
# A plain `schema ...` _must_ include root type definitions.
# If the only difference is directives, then you have to use `extend schema`
GraphQL::Language::Nodes::SchemaExtension.new(directives: definition_directives(@schema, :schema_directives))
end
end
def build_object_type_node(object_type)
ints = warden.interfaces(object_type)
if ints.any?
ints.sort_by!(&:graphql_name)
ints.map! { |iface| build_type_name_node(iface) }
end
GraphQL::Language::Nodes::ObjectTypeDefinition.new(
name: object_type.graphql_name,
interfaces: ints,
fields: build_field_nodes(warden.fields(object_type)),
description: object_type.description,
directives: directives(object_type),
)
end
def build_field_node(field)
GraphQL::Language::Nodes::FieldDefinition.new(
name: field.graphql_name,
arguments: build_argument_nodes(warden.arguments(field)),
type: build_type_name_node(field.type),
description: field.description,
directives: directives(field),
)
end
def build_union_type_node(union_type)
GraphQL::Language::Nodes::UnionTypeDefinition.new(
name: union_type.graphql_name,
description: union_type.description,
types: warden.possible_types(union_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) },
directives: directives(union_type),
)
end
def build_interface_type_node(interface_type)
GraphQL::Language::Nodes::InterfaceTypeDefinition.new(
name: interface_type.graphql_name,
interfaces: warden.interfaces(interface_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) },
description: interface_type.description,
fields: build_field_nodes(warden.fields(interface_type)),
directives: directives(interface_type),
)
end
def build_enum_type_node(enum_type)
GraphQL::Language::Nodes::EnumTypeDefinition.new(
name: enum_type.graphql_name,
values: warden.enum_values(enum_type).sort_by(&:graphql_name).map do |enum_value|
build_enum_value_node(enum_value)
end,
description: enum_type.description,
directives: directives(enum_type),
)
end
def build_enum_value_node(enum_value)
GraphQL::Language::Nodes::EnumValueDefinition.new(
name: enum_value.graphql_name,
description: enum_value.description,
directives: directives(enum_value),
)
end
def build_scalar_type_node(scalar_type)
GraphQL::Language::Nodes::ScalarTypeDefinition.new(
name: scalar_type.graphql_name,
description: scalar_type.description,
directives: directives(scalar_type),
)
end
def build_argument_node(argument)
if argument.default_value?
default_value = build_default_value(argument.default_value, argument.type)
else
default_value = nil
end
argument_node = GraphQL::Language::Nodes::InputValueDefinition.new(
name: argument.graphql_name,
description: argument.description,
type: build_type_name_node(argument.type),
default_value: default_value,
directives: directives(argument),
)
argument_node
end
def build_input_object_node(input_object)
GraphQL::Language::Nodes::InputObjectTypeDefinition.new(
name: input_object.graphql_name,
fields: build_argument_nodes(warden.arguments(input_object)),
description: input_object.description,
directives: directives(input_object),
)
end
def build_directive_node(directive)
GraphQL::Language::Nodes::DirectiveDefinition.new(
name: directive.graphql_name,
repeatable: directive.repeatable?,
arguments: build_argument_nodes(warden.arguments(directive)),
locations: build_directive_location_nodes(directive.locations),
description: directive.description,
)
end
def build_directive_location_nodes(locations)
locations.sort.map { |location| build_directive_location_node(location) }
end
def build_directive_location_node(location)
GraphQL::Language::Nodes::DirectiveLocation.new(
name: location.to_s
)
end
def build_type_name_node(type)
case type.kind.name
when "LIST"
GraphQL::Language::Nodes::ListType.new(
of_type: build_type_name_node(type.of_type)
)
when "NON_NULL"
GraphQL::Language::Nodes::NonNullType.new(
of_type: build_type_name_node(type.of_type)
)
else
@cached_type_name_nodes ||= {}
@cached_type_name_nodes[type.graphql_name] ||= GraphQL::Language::Nodes::TypeName.new(name: type.graphql_name)
end
end
def build_default_value(default_value, type)
if default_value.nil?
return GraphQL::Language::Nodes::NullValue.new(name: "null")
end
case type.kind.name
when "SCALAR"
type.coerce_isolated_result(default_value)
when "ENUM"
GraphQL::Language::Nodes::Enum.new(name: type.coerce_isolated_result(default_value))
when "INPUT_OBJECT"
GraphQL::Language::Nodes::InputObject.new(
arguments: default_value.to_h.map do |arg_name, arg_value|
args = @warden.arguments(type)
arg = args.find { |a| a.keyword.to_s == arg_name.to_s }
if arg.nil?
raise ArgumentError, "No argument definition on #{type.graphql_name} for argument: #{arg_name.inspect} (expected one of: #{args.map(&:keyword)})"
end
GraphQL::Language::Nodes::Argument.new(
name: arg.graphql_name.to_s,
value: build_default_value(arg_value, arg.type)
)
end
)
when "NON_NULL"
build_default_value(default_value, type.of_type)
when "LIST"
default_value.to_a.map { |v| build_default_value(v, type.of_type) }
else
raise GraphQL::RequiredImplementationMissingError, "Unexpected default value type #{type.inspect}"
end
end
def build_type_definition_node(type)
case type.kind.name
when "OBJECT"
build_object_type_node(type)
when "UNION"
build_union_type_node(type)
when "INTERFACE"
build_interface_type_node(type)
when "SCALAR"
build_scalar_type_node(type)
when "ENUM"
build_enum_type_node(type)
when "INPUT_OBJECT"
build_input_object_node(type)
else
raise TypeError
end
end
def build_argument_nodes(arguments)
if arguments.any?
nodes = arguments.map { |arg| build_argument_node(arg) }
nodes.sort_by!(&:name)
nodes
else
arguments
end
end
def build_directive_nodes(directives)
directives
.map { |directive| build_directive_node(directive) }
.sort_by(&:name)
end
def build_definition_nodes
dirs_to_build = warden.directives
if !include_built_in_directives
dirs_to_build = dirs_to_build.reject { |directive| directive.default_directive? }
end
definitions = build_directive_nodes(dirs_to_build)
type_nodes = build_type_definition_nodes(warden.reachable_types)
if @include_one_of
# This may have been set to true when iterating over all types
definitions.concat(build_directive_nodes([GraphQL::Schema::Directive::OneOf]))
end
definitions.concat(type_nodes)
if include_schema_node?
definitions.unshift(build_schema_node)
end
definitions
end
def build_type_definition_nodes(types)
if !include_introspection_types
types = types.reject { |type| type.introspection? }
end
if !include_built_in_scalars
types = types.reject { |type| type.kind.scalar? && type.default_scalar? }
end
types
.map { |type| build_type_definition_node(type) }
.sort_by(&:name)
end
def build_field_nodes(fields)
f_nodes = fields.map { |field| build_field_node(field) }
f_nodes.sort_by!(&:name)
f_nodes
end
private
def include_schema_node?
always_include_schema ||
!schema_respects_root_name_conventions?(schema) ||
!schema.schema_directives.empty?
end
def schema_respects_root_name_conventions?(schema)
(schema.query.nil? || schema.query.graphql_name == 'Query') &&
(schema.mutation.nil? || schema.mutation.graphql_name == 'Mutation') &&
(schema.subscription.nil? || schema.subscription.graphql_name == 'Subscription')
end
def directives(member)
definition_directives(member, :directives)
end
def definition_directives(member, directives_method)
dirs = if !member.respond_to?(directives_method) || member.directives.empty?
EmptyObjects::EMPTY_ARRAY
else
member.public_send(directives_method).map do |dir|
args = []
dir.arguments.argument_values.each_value do |arg_value| # rubocop:disable Development/ContextIsPassedCop -- directive instance method
arg_defn = arg_value.definition
if arg_defn.default_value? && arg_value.value == arg_defn.default_value
next
else
value_node = build_default_value(arg_value.value, arg_value.definition.type)
args << GraphQL::Language::Nodes::Argument.new(
name: arg_value.definition.name,
value: value_node,
)
end
end
# If this schema uses this built-in directive definition,
# include it in the print-out since it's not part of the spec yet.
@include_one_of ||= dir.class == GraphQL::Schema::Directive::OneOf
GraphQL::Language::Nodes::Directive.new(
name: dir.class.graphql_name,
arguments: args
)
end
end
dirs
end
attr_reader :schema, :warden, :always_include_schema,
:include_introspection_types, :include_built_in_directives, :include_built_in_scalars
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/generation.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
# Exposes {.generate}, which turns AST nodes back into query strings.
module Generation
extend self
# Turn an AST node back into a string.
#
# @example Turning a document into a query
# document = GraphQL.parse(query_string)
# GraphQL::Language::Generation.generate(document)
# # => "{ ... }"
#
# @param node [GraphQL::Language::Nodes::AbstractNode] an AST node to recursively stringify
# @param indent [String] Whitespace to add to each printed node
# @param printer [GraphQL::Language::Printer] An optional custom printer for printing AST nodes. Defaults to GraphQL::Language::Printer
# @return [String] Valid GraphQL for `node`
def generate(node, indent: "", printer: GraphQL::Language::Printer.new)
printer.print(node, indent: indent)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/lexer.rb
```ruby
# frozen_string_literal: true
require "strscan"
module GraphQL
module Language
class Lexer
IDENTIFIER = /[_A-Za-z][_0-9A-Za-z]*/
NEWLINE = /[\c\r\n]/
BLANK = /[, \t]+/
COMMENT = /#[^\n\r]*/
INT = /[-]?(?:[0]|[1-9][0-9]*)/
FLOAT_DECIMAL = /[.][0-9]+/
FLOAT_EXP = /[eE][+-]?[0-9]+/
FLOAT = /#{INT}(#{FLOAT_DECIMAL}#{FLOAT_EXP}|#{FLOAT_DECIMAL}|#{FLOAT_EXP})/
module Literals
ON = /on\b/
FRAGMENT = /fragment\b/
TRUE = /true\b/
FALSE = /false\b/
NULL = /null\b/
QUERY = /query\b/
MUTATION = /mutation\b/
SUBSCRIPTION = /subscription\b/
SCHEMA = /schema\b/
SCALAR = /scalar\b/
TYPE = /type\b/
EXTEND = /extend\b/
IMPLEMENTS = /implements\b/
INTERFACE = /interface\b/
UNION = /union\b/
ENUM = /enum\b/
INPUT = /input\b/
DIRECTIVE = /directive\b/
REPEATABLE = /repeatable\b/
LCURLY = '{'
RCURLY = '}'
LPAREN = '('
RPAREN = ')'
LBRACKET = '['
RBRACKET = ']'
COLON = ':'
VAR_SIGN = '$'
DIR_SIGN = '@'
ELLIPSIS = '...'
EQUALS = '='
BANG = '!'
PIPE = '|'
AMP = '&'
end
include Literals
QUOTE = '"'
UNICODE_DIGIT = /[0-9A-Za-z]/
FOUR_DIGIT_UNICODE = /#{UNICODE_DIGIT}{4}/
N_DIGIT_UNICODE = %r{#{LCURLY}#{UNICODE_DIGIT}{4,}#{RCURLY}}x
UNICODE_ESCAPE = %r{\\u(?:#{FOUR_DIGIT_UNICODE}|#{N_DIGIT_UNICODE})}
# # https://graphql.github.io/graphql-spec/June2018/#sec-String-Value
STRING_ESCAPE = %r{[\\][\\/bfnrt]}
BLOCK_QUOTE = '"""'
ESCAPED_QUOTE = /\\"/;
STRING_CHAR = /#{ESCAPED_QUOTE}|[^"\\]|#{UNICODE_ESCAPE}|#{STRING_ESCAPE}/
LIT_NAME_LUT = Literals.constants.each_with_object({}) { |n, o|
key = Literals.const_get(n)
key = key.is_a?(Regexp) ? key.source.gsub(/(\\b|\\)/, '') : key
o[key] = n
}
LIT = Regexp.union(Literals.constants.map { |n| Literals.const_get(n) })
QUOTED_STRING = %r{#{QUOTE} (?:#{STRING_CHAR})* #{QUOTE}}x
BLOCK_STRING = %r{
#{BLOCK_QUOTE}
(?: [^"\\] | # Any characters that aren't a quote or slash
(?<!") ["]{1,2} (?!") | # Any quotes that don't have quotes next to them
\\"{0,3}(?!") | # A slash followed by <= 3 quotes that aren't followed by a quote
\\ | # A slash
"{1,2}(?!") # 1 or 2 " followed by something that isn't a quote
)*
(?:"")?
#{BLOCK_QUOTE}
}xm
# # catch-all for anything else. must be at the bottom for precedence.
UNKNOWN_CHAR = /./
def initialize(value)
@line = 1
@col = 1
@previous_token = nil
@scan = scanner value
end
class BadEncoding < Lexer # :nodoc:
def scanner(value)
[emit(:BAD_UNICODE_ESCAPE, 0, 0, value)]
end
def next_token
@scan.pop
end
end
def self.tokenize(string)
value = string.dup.force_encoding(Encoding::UTF_8)
scanner = if value.valid_encoding?
new value
else
BadEncoding.new value
end
toks = []
while tok = scanner.next_token
toks << tok
end
toks
end
def next_token
return if @scan.eos?
pos = @scan.pos
case
when str = @scan.scan(FLOAT) then emit(:FLOAT, pos, @scan.pos, str)
when str = @scan.scan(INT) then emit(:INT, pos, @scan.pos, str)
when str = @scan.scan(LIT) then emit(LIT_NAME_LUT[str], pos, @scan.pos, -str)
when str = @scan.scan(IDENTIFIER) then emit(:IDENTIFIER, pos, @scan.pos, str)
when str = @scan.scan(BLOCK_STRING) then emit_block(pos, @scan.pos, str.gsub(/\A#{BLOCK_QUOTE}|#{BLOCK_QUOTE}\z/, ''))
when str = @scan.scan(QUOTED_STRING) then emit_string(pos, @scan.pos, str.gsub(/^"|"$/, ''))
when str = @scan.scan(COMMENT) then record_comment(pos, @scan.pos, str)
when str = @scan.scan(NEWLINE)
@line += 1
@col = 1
next_token
when @scan.scan(BLANK)
@col += @scan.pos - pos
next_token
when str = @scan.scan(UNKNOWN_CHAR) then emit(:UNKNOWN_CHAR, pos, @scan.pos, str)
else
# This should never happen since `UNKNOWN_CHAR` ensures we make progress
raise "Unknown string?"
end
end
def emit(token_name, ts, te, token_value)
token = [
token_name,
@line,
@col,
token_value,
@previous_token,
]
@previous_token = token
# Bump the column counter for the next token
@col += te - ts
token
end
# Replace any escaped unicode or whitespace with the _actual_ characters
# To avoid allocating more strings, this modifies the string passed into it
def self.replace_escaped_characters_in_place(raw_string)
raw_string.gsub!(ESCAPES, ESCAPES_REPLACE)
raw_string.gsub!(UTF_8) do |_matched_str|
codepoint_1 = ($1 || $2).to_i(16)
codepoint_2 = $3
if codepoint_2
codepoint_2 = codepoint_2.to_i(16)
if (codepoint_1 >= 0xD800 && codepoint_1 <= 0xDBFF) && # leading surrogate
(codepoint_2 >= 0xDC00 && codepoint_2 <= 0xDFFF) # trailing surrogate
# A surrogate pair
combined = ((codepoint_1 - 0xD800) * 0x400) + (codepoint_2 - 0xDC00) + 0x10000
[combined].pack('U'.freeze)
else
# Two separate code points
[codepoint_1].pack('U'.freeze) + [codepoint_2].pack('U'.freeze)
end
else
[codepoint_1].pack('U'.freeze)
end
end
nil
end
def record_comment(ts, te, str)
token = [
:COMMENT,
@line,
@col,
str,
@previous_token,
]
@previous_token = token
@col += te - ts
next_token
end
ESCAPES = /\\["\\\/bfnrt]/
ESCAPES_REPLACE = {
'\\"' => '"',
"\\\\" => "\\",
"\\/" => '/',
"\\b" => "\b",
"\\f" => "\f",
"\\n" => "\n",
"\\r" => "\r",
"\\t" => "\t",
}
UTF_8 = /\\u(?:([\dAa-f]{4})|\{([\da-f]{4,})\})(?:\\u([\dAa-f]{4}))?/i
VALID_STRING = /\A(?:[^\\]|#{ESCAPES}|#{UTF_8})*\z/o
def emit_block(ts, te, value)
line_incr = value.count("\n")
value = GraphQL::Language::BlockString.trim_whitespace(value)
tok = emit_string(ts, te, value)
@line += line_incr
tok
end
def emit_string(ts, te, value)
if !value.valid_encoding? || !value.match?(VALID_STRING)
emit(:BAD_UNICODE_ESCAPE, ts, te, value)
else
self.class.replace_escaped_characters_in_place(value)
if !value.valid_encoding?
emit(:BAD_UNICODE_ESCAPE, ts, te, value)
else
emit(:STRING, ts, te, value)
end
end
end
private
def scanner(value)
StringScanner.new value
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/nodes.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
module Nodes
NONE = GraphQL::EmptyObjects::EMPTY_ARRAY
# {AbstractNode} is the base class for all nodes in a GraphQL AST.
#
# It provides some APIs for working with ASTs:
# - `children` returns all AST nodes attached to this one. Used for tree traversal.
# - `scalars` returns all scalar (Ruby) values attached to this one. Used for comparing nodes.
# - `to_query_string` turns an AST node into a GraphQL string
class AbstractNode
module DefinitionNode
# This AST node's {#line} returns the first line, which may be the description.
# @return [Integer] The first line of the definition (not the description)
attr_reader :definition_line
def initialize(options = {})
@definition_line = options.delete(:definition_line)
super(options)
end
end
attr_reader :line, :col, :filename
# Initialize a node by extracting its position,
# then calling the class's `initialize_node` method.
# @param options [Hash] Initial attributes for this node
def initialize(options = {})
if options.key?(:position_source)
position_source = options.delete(:position_source)
@line = position_source[1]
@col = position_source[2]
else
@line = options.delete(:line)
@col = options.delete(:col)
end
@filename = options.delete(:filename)
initialize_node(**options)
end
# Value equality
# @return [Boolean] True if `self` is equivalent to `other`
def ==(other)
return true if equal?(other)
other.kind_of?(self.class) &&
other.scalars == self.scalars &&
other.children == self.children
end
NO_CHILDREN = GraphQL::EmptyObjects::EMPTY_ARRAY
# @return [Array<GraphQL::Language::Nodes::AbstractNode>] all nodes in the tree below this one
def children
NO_CHILDREN
end
# @return [Array<Integer, Float, String, Boolean, Array>] Scalar values attached to this node
def scalars
NO_CHILDREN
end
# This might be unnecessary, but its easiest to add it here.
def initialize_copy(other)
@children = nil
@scalars = nil
@query_string = nil
end
def children_method_name
self.class.children_method_name
end
def position
[line, col]
end
def to_query_string(printer: GraphQL::Language::Printer.new)
if printer.is_a?(GraphQL::Language::Printer)
@query_string ||= printer.print(self)
else
printer.print(self)
end
end
# This creates a copy of `self`, with `new_options` applied.
# @param new_options [Hash]
# @return [AbstractNode] a shallow copy of `self`
def merge(new_options)
dup.merge!(new_options)
end
# Copy `self`, but modify the copy so that `previous_child` is replaced by `new_child`
def replace_child(previous_child, new_child)
# Figure out which list `previous_child` may be found in
method_name = previous_child.children_method_name
# Get the value from this (original) node
prev_children = public_send(method_name)
if prev_children.is_a?(Array)
# Copy that list, and replace `previous_child` with `new_child`
# in the list.
new_children = prev_children.dup
prev_idx = new_children.index(previous_child)
new_children[prev_idx] = new_child
else
# Use the new value for the given attribute
new_children = new_child
end
# Copy this node, but with the new child value
copy_of_self = merge(method_name => new_children)
# Return the copy:
copy_of_self
end
# TODO DRY with `replace_child`
def delete_child(previous_child)
# Figure out which list `previous_child` may be found in
method_name = previous_child.children_method_name
# Copy that list, and delete previous_child
new_children = public_send(method_name).dup
new_children.delete(previous_child)
# Copy this node, but with the new list of children:
copy_of_self = merge(method_name => new_children)
# Return the copy:
copy_of_self
end
protected
def merge!(new_options)
new_options.each do |key, value|
instance_variable_set(:"@#{key}", value)
end
self
end
class << self
# Add a default `#visit_method` and `#children_method_name` using the class name
def inherited(child_class)
super
name_underscored = child_class.name
.split("::").last
.gsub(/([a-z])([A-Z])/,'\1_\2') # insert underscores
.downcase # remove caps
child_class.module_eval <<-RUBY, __FILE__, __LINE__
def visit_method
:on_#{name_underscored}
end
class << self
attr_accessor :children_method_name
def visit_method
:on_#{name_underscored}
end
end
self.children_method_name = :#{name_underscored}s
RUBY
end
def children_of_type
@children_methods
end
private
# Name accessors which return lists of nodes,
# along with the kind of node they return, if possible.
# - Add a reader for these children
# - Add a persistent update method to add a child
# - Generate a `#children` method
def children_methods(children_of_type)
if defined?(@children_methods)
raise "Can't re-call .children_methods for #{self} (already have: #{@children_methods})"
else
@children_methods = children_of_type
end
if children_of_type == false
@children_methods = {}
# skip
else
children_of_type.each do |method_name, node_type|
module_eval <<-RUBY, __FILE__, __LINE__
# A reader for these children
attr_reader :#{method_name}
RUBY
if node_type
# Only generate a method if we know what kind of node to make
module_eval <<-RUBY, __FILE__, __LINE__
# Singular method: create a node with these options
# and return a new `self` which includes that node in this list.
def merge_#{method_name.to_s.sub(/s$/, "")}(node_opts)
merge(#{method_name}: #{method_name} + [#{node_type.name}.new(node_opts)])
end
RUBY
end
end
if children_of_type.size == 1
module_eval <<-RUBY, __FILE__, __LINE__
alias :children #{children_of_type.keys.first}
RUBY
else
module_eval <<-RUBY, __FILE__, __LINE__
def children
@children ||= begin
if #{children_of_type.keys.map { |k| "@#{k}.any?" }.join(" || ")}
new_children = []
#{children_of_type.keys.map { |k| "new_children.concat(@#{k})" }.join("; ")}
new_children.freeze
new_children
else
NO_CHILDREN
end
end
end
RUBY
end
end
if defined?(@scalar_methods)
if !method_defined?(:initialize_node)
generate_initialize_node
else
# This method was defined manually
end
else
raise "Can't generate_initialize_node because scalar_methods wasn't called; call it before children_methods"
end
end
# These methods return a plain Ruby value, not another node
# - Add reader methods
# - Add a `#scalars` method
def scalar_methods(*method_names)
if defined?(@scalar_methods)
raise "Can't re-call .scalar_methods for #{self} (already have: #{@scalar_methods})"
else
@scalar_methods = method_names
end
if method_names == [false]
@scalar_methods = []
# skip it
else
module_eval <<-RUBY, __FILE__, __LINE__
# add readers for each scalar
attr_reader #{method_names.map { |m| ":#{m}"}.join(", ")}
def scalars
@scalars ||= [#{method_names.map { |k| "@#{k}" }.join(", ")}].freeze
end
RUBY
end
end
def generate_initialize_node
scalar_method_names = @scalar_methods
# TODO: These probably should be scalar methods, but `types` returns an array
[:types, :description].each do |extra_method|
if method_defined?(extra_method)
scalar_method_names += [extra_method]
end
end
all_method_names = scalar_method_names + @children_methods.keys
if all_method_names.include?(:alias)
# Rather than complicating this special case,
# let it be overridden (in field)
return
else
arguments = scalar_method_names.map { |m| "#{m}: nil"} +
@children_methods.keys.map { |m| "#{m}: NO_CHILDREN" }
assignments = scalar_method_names.map { |m| "@#{m} = #{m}"} +
@children_methods.keys.map { |m| "@#{m} = #{m}.freeze" }
keywords = scalar_method_names.map { |m| "#{m}: #{m}"} +
@children_methods.keys.map { |m| "#{m}: #{m}" }
module_eval <<-RUBY, __FILE__, __LINE__
def initialize_node #{arguments.join(", ")}
#{assignments.join("\n")}
end
def self.from_a(filename, line, col, #{(scalar_method_names + @children_methods.keys).join(", ")})
self.new(filename: filename, line: line, col: col, #{keywords.join(", ")})
end
RUBY
end
end
end
end
# Base class for non-null type names and list type names
class WrapperType < AbstractNode
scalar_methods :of_type
children_methods(false)
end
# Base class for nodes whose only value is a name (no child nodes or other scalars)
class NameOnlyNode < AbstractNode
scalar_methods :name
children_methods(false)
end
# A key-value pair for a field's inputs
class Argument < AbstractNode
scalar_methods :name, :value
children_methods(false)
# @!attribute name
# @return [String] the key for this argument
# @!attribute value
# @return [String, Float, Integer, Boolean, Array, InputObject, VariableIdentifier] The value passed for this key
def children
@children ||= Array(value).flatten.tap { _1.select! { |v| v.is_a?(AbstractNode) } }
end
end
class Directive < AbstractNode
scalar_methods :name
children_methods(arguments: GraphQL::Language::Nodes::Argument)
end
class DirectiveLocation < NameOnlyNode
end
class DirectiveDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name, :repeatable
children_methods(
arguments: Nodes::Argument,
locations: Nodes::DirectiveLocation,
)
end
# An enum value. The string is available as {#name}.
class Enum < NameOnlyNode
end
# A null value literal.
class NullValue < NameOnlyNode
end
# A single selection in a GraphQL query.
class Field < AbstractNode
scalar_methods :name, :alias
children_methods({
arguments: GraphQL::Language::Nodes::Argument,
selections: GraphQL::Language::Nodes::Field,
directives: GraphQL::Language::Nodes::Directive,
})
# @!attribute selections
# @return [Array<Nodes::Field>] Selections on this object (or empty array if this is a scalar field)
def initialize_node(attributes)
@name = attributes[:name]
@arguments = attributes[:arguments] || NONE
@directives = attributes[:directives] || NONE
@selections = attributes[:selections] || NONE
# oops, alias is a keyword:
@alias = attributes[:alias]
end
def self.from_a(filename, line, col, graphql_alias, name, arguments, directives, selections) # rubocop:disable Metrics/ParameterLists
self.new(filename: filename, line: line, col: col, alias: graphql_alias, name: name, arguments: arguments, directives: directives, selections: selections)
end
# Override this because default is `:fields`
self.children_method_name = :selections
end
# A reusable fragment, defined at document-level.
class FragmentDefinition < AbstractNode
# @!attribute name
# @return [String] the identifier for this fragment, which may be applied with `...#{name}`
# @!attribute type
# @return [String] the type condition for this fragment (name of type which it may apply to)
def initialize_node(name: nil, type: nil, directives: [], selections: [])
@name = name
@type = type
@directives = directives
@selections = selections
end
def self.from_a(filename, line, col, name, type, directives, selections)
self.new(filename: filename, line: line, col: col, name: name, type: type, directives: directives, selections: selections)
end
scalar_methods :name, :type
children_methods({
selections: GraphQL::Language::Nodes::Field,
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
# Application of a named fragment in a selection
class FragmentSpread < AbstractNode
scalar_methods :name
children_methods(directives: GraphQL::Language::Nodes::Directive)
self.children_method_name = :selections
# @!attribute name
# @return [String] The identifier of the fragment to apply, corresponds with {FragmentDefinition#name}
end
# An unnamed fragment, defined directly in the query with `... { }`
class InlineFragment < AbstractNode
scalar_methods :type
children_methods({
directives: GraphQL::Language::Nodes::Directive,
selections: GraphQL::Language::Nodes::Field,
})
self.children_method_name = :selections
# @!attribute type
# @return [String, nil] Name of the type this fragment applies to, or `nil` if this fragment applies to any type
end
# A collection of key-value inputs which may be a field argument
class InputObject < AbstractNode
scalar_methods(false)
children_methods(arguments: GraphQL::Language::Nodes::Argument)
# @!attribute arguments
# @return [Array<Nodes::Argument>] A list of key-value pairs inside this input object
# @return [Hash<String, Any>] Recursively turn this input object into a Ruby Hash
def to_h(options={})
arguments.inject({}) do |memo, pair|
v = pair.value
memo[pair.name] = serialize_value_for_hash v
memo
end
end
self.children_method_name = :value
private
def serialize_value_for_hash(value)
case value
when InputObject
value.to_h
when Array
value.map do |v|
serialize_value_for_hash v
end
when Enum
value.name
when NullValue
nil
else
value
end
end
end
# A list type definition, denoted with `[...]` (used for variable type definitions)
class ListType < WrapperType
end
# A non-null type definition, denoted with `...!` (used for variable type definitions)
class NonNullType < WrapperType
end
# An operation-level query variable
class VariableDefinition < AbstractNode
scalar_methods :name, :type, :default_value
children_methods false
# @!attribute default_value
# @return [String, Integer, Float, Boolean, Array, NullValue] A Ruby value to use if no other value is provided
# @!attribute type
# @return [TypeName, NonNullType, ListType] The expected type of this value
# @!attribute name
# @return [String] The identifier for this variable, _without_ `$`
self.children_method_name = :variables
end
# A query, mutation or subscription.
# May be anonymous or named.
# May be explicitly typed (eg `mutation { ... }`) or implicitly a query (eg `{ ... }`).
class OperationDefinition < AbstractNode
scalar_methods :operation_type, :name
children_methods({
variables: GraphQL::Language::Nodes::VariableDefinition,
directives: GraphQL::Language::Nodes::Directive,
selections: GraphQL::Language::Nodes::Field,
})
# @!attribute variables
# @return [Array<VariableDefinition>] Variable $definitions for this operation
# @!attribute selections
# @return [Array<Field>] Root-level fields on this operation
# @!attribute operation_type
# @return [String, nil] The root type for this operation, or `nil` for implicit `"query"`
# @!attribute name
# @return [String, nil] The name for this operation, or `nil` if unnamed
self.children_method_name = :definitions
end
# This is the AST root for normal queries
#
# @example Deriving a document by parsing a string
# document = GraphQL.parse(query_string)
#
# @example Creating a string from a document
# document.to_query_string
# # { ... }
#
# @example Creating a custom string from a document
# class VariableScrubber < GraphQL::Language::Printer
# def print_argument(arg)
# print_string("#{arg.name}: <HIDDEN>")
# end
# end
#
# document.to_query_string(printer: VariableScrubber.new)
#
class Document < AbstractNode
scalar_methods false
children_methods(definitions: nil)
# @!attribute definitions
# @return [Array<OperationDefinition, FragmentDefinition>] top-level GraphQL units: operations or fragments
def slice_definition(name)
GraphQL::Language::DefinitionSlice.slice(self, name)
end
end
# A type name, used for variable definitions
class TypeName < NameOnlyNode
end
# Usage of a variable in a query. Name does _not_ include `$`.
class VariableIdentifier < NameOnlyNode
self.children_method_name = :value
end
class SchemaDefinition < AbstractNode
include DefinitionNode
scalar_methods :query, :mutation, :subscription
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class SchemaExtension < AbstractNode
scalar_methods :query, :mutation, :subscription
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class ScalarTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class ScalarTypeExtension < AbstractNode
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class InputValueDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name, :type, :default_value
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :fields
end
class FieldDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name, :type
children_methods({
arguments: GraphQL::Language::Nodes::InputValueDefinition,
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :fields
# this is so that `children_method_name` of `InputValueDefinition` works properly
# with `#replace_child`
alias :fields :arguments
def merge(new_options)
if (f = new_options.delete(:fields))
new_options[:arguments] = f
end
super
end
end
class ObjectTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name, :interfaces
children_methods({
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
self.children_method_name = :definitions
end
class ObjectTypeExtension < AbstractNode
scalar_methods :name, :interfaces
children_methods({
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
self.children_method_name = :definitions
end
class InterfaceTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
interfaces: GraphQL::Language::Nodes::TypeName,
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
self.children_method_name = :definitions
end
class InterfaceTypeExtension < AbstractNode
scalar_methods :name
children_methods({
interfaces: GraphQL::Language::Nodes::TypeName,
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
self.children_method_name = :definitions
end
class UnionTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description, :types
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class UnionTypeExtension < AbstractNode
attr_reader :types
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class EnumValueDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :values
end
class EnumTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
values: GraphQL::Language::Nodes::EnumValueDefinition,
})
self.children_method_name = :definitions
end
class EnumTypeExtension < AbstractNode
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
values: GraphQL::Language::Nodes::EnumValueDefinition,
})
self.children_method_name = :definitions
end
class InputObjectTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::InputValueDefinition,
})
self.children_method_name = :definitions
end
class InputObjectTypeExtension < AbstractNode
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::InputValueDefinition,
})
self.children_method_name = :definitions
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/printer.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
class Printer
OMISSION = "... (truncated)"
class TruncatableBuffer
class TruncateSizeReached < StandardError; end
DEFAULT_INIT_CAPACITY = 500
def initialize(truncate_size: nil)
@out = String.new(capacity: truncate_size || DEFAULT_INIT_CAPACITY)
@truncate_size = truncate_size
end
def append(other)
if @truncate_size && (@out.size + other.size) > @truncate_size
@out << other.slice(0, @truncate_size - @out.size)
raise(TruncateSizeReached, "Truncate size reached")
else
@out << other
end
end
def to_string
@out
end
end
# Turn an arbitrary AST node back into a string.
#
# @example Turning a document into a query string
# document = GraphQL.parse(query_string)
# GraphQL::Language::Printer.new.print(document)
# # => "{ ... }"
#
#
# @example Building a custom printer
#
# class MyPrinter < GraphQL::Language::Printer
# def print_argument(arg)
# print_string("#{arg.name}: <HIDDEN>")
# end
# end
#
# MyPrinter.new.print(document)
# # => "mutation { pay(creditCard: <HIDDEN>) { success } }"
#
# @param node [Nodes::AbstractNode]
# @param indent [String] Whitespace to add to the printed node
# @param truncate_size [Integer, nil] The size to truncate to.
# @return [String] Valid GraphQL for `node`
def print(node, indent: "", truncate_size: nil)
truncate_size = truncate_size ? [truncate_size - OMISSION.size, 0].max : nil
@out = TruncatableBuffer.new(truncate_size: truncate_size)
print_node(node, indent: indent)
@out.to_string
rescue TruncatableBuffer::TruncateSizeReached
@out.to_string << OMISSION
end
protected
def print_string(str)
@out.append(str)
end
def print_document(document)
document.definitions.each_with_index do |d, i|
print_node(d)
print_string("\n\n") if i < document.definitions.size - 1
end
end
def print_argument(argument)
print_string(argument.name)
print_string(": ")
print_node(argument.value)
end
def print_input_object(input_object)
print_string("{")
input_object.arguments.each_with_index do |a, i|
print_argument(a)
print_string(", ") if i < input_object.arguments.size - 1
end
print_string("}")
end
def print_directive(directive)
print_string("@")
print_string(directive.name)
if directive.arguments.any?
print_string("(")
directive.arguments.each_with_index do |a, i|
print_argument(a)
print_string(", ") if i < directive.arguments.size - 1
end
print_string(")")
end
end
def print_enum(enum)
print_string(enum.name)
end
def print_null_value
print_string("null")
end
def print_field(field, indent: "")
print_string(indent)
if field.alias
print_string(field.alias)
print_string(": ")
end
print_string(field.name)
if field.arguments.any?
print_string("(")
field.arguments.each_with_index do |a, i|
print_argument(a)
print_string(", ") if i < field.arguments.size - 1
end
print_string(")")
end
print_directives(field.directives)
print_selections(field.selections, indent: indent)
end
def print_fragment_definition(fragment_def, indent: "")
print_string(indent)
print_string("fragment")
if fragment_def.name
print_string(" ")
print_string(fragment_def.name)
end
if fragment_def.type
print_string(" on ")
print_node(fragment_def.type)
end
print_directives(fragment_def.directives)
print_selections(fragment_def.selections, indent: indent)
end
def print_fragment_spread(fragment_spread, indent: "")
print_string(indent)
print_string("...")
print_string(fragment_spread.name)
print_directives(fragment_spread.directives)
end
def print_inline_fragment(inline_fragment, indent: "")
print_string(indent)
print_string("...")
if inline_fragment.type
print_string(" on ")
print_node(inline_fragment.type)
end
print_directives(inline_fragment.directives)
print_selections(inline_fragment.selections, indent: indent)
end
def print_list_type(list_type)
print_string("[")
print_node(list_type.of_type)
print_string("]")
end
def print_non_null_type(non_null_type)
print_node(non_null_type.of_type)
print_string("!")
end
def print_operation_definition(operation_definition, indent: "")
print_string(indent)
print_string(operation_definition.operation_type)
if operation_definition.name
print_string(" ")
print_string(operation_definition.name)
end
if operation_definition.variables.any?
print_string("(")
operation_definition.variables.each_with_index do |v, i|
print_variable_definition(v)
print_string(", ") if i < operation_definition.variables.size - 1
end
print_string(")")
end
print_directives(operation_definition.directives)
print_selections(operation_definition.selections, indent: indent)
end
def print_type_name(type_name)
print_string(type_name.name)
end
def print_variable_definition(variable_definition)
print_string("$")
print_string(variable_definition.name)
print_string(": ")
print_node(variable_definition.type)
unless variable_definition.default_value.nil?
print_string(" = ")
print_node(variable_definition.default_value)
end
end
def print_variable_identifier(variable_identifier)
print_string("$")
print_string(variable_identifier.name)
end
def print_schema_definition(schema, extension: false)
has_conventional_names = (schema.query.nil? || schema.query == 'Query') &&
(schema.mutation.nil? || schema.mutation == 'Mutation') &&
(schema.subscription.nil? || schema.subscription == 'Subscription')
if has_conventional_names && schema.directives.empty?
return
end
extension ? print_string("extend schema") : print_string("schema")
if schema.directives.any?
schema.directives.each do |dir|
print_string("\n ")
print_node(dir)
end
if !has_conventional_names
print_string("\n")
end
end
if !has_conventional_names
if schema.directives.empty?
print_string(" ")
end
print_string("{\n")
print_string(" query: #{schema.query}\n") if schema.query
print_string(" mutation: #{schema.mutation}\n") if schema.mutation
print_string(" subscription: #{schema.subscription}\n") if schema.subscription
print_string("}")
end
end
def print_scalar_type_definition(scalar_type, extension: false)
extension ? print_string("extend ") : print_description(scalar_type)
print_string("scalar ")
print_string(scalar_type.name)
print_directives(scalar_type.directives)
end
def print_object_type_definition(object_type, extension: false)
extension ? print_string("extend ") : print_description(object_type)
print_string("type ")
print_string(object_type.name)
print_implements(object_type) unless object_type.interfaces.empty?
print_directives(object_type.directives)
print_field_definitions(object_type.fields)
end
def print_implements(type)
print_string(" implements ")
i = 0
type.interfaces.each do |int|
if i > 0
print_string(" & ")
end
print_string(int.name)
i += 1
end
end
def print_input_value_definition(input_value)
print_string(input_value.name)
print_string(": ")
print_node(input_value.type)
unless input_value.default_value.nil?
print_string(" = ")
print_node(input_value.default_value)
end
print_directives(input_value.directives)
end
def print_arguments(arguments, indent: "")
if arguments.all? { |arg| !arg.description }
print_string("(")
arguments.each_with_index do |arg, i|
print_input_value_definition(arg)
print_string(", ") if i < arguments.size - 1
end
print_string(")")
return
end
print_string("(\n")
arguments.each_with_index do |arg, i|
print_description(arg, indent: " " + indent, first_in_block: i == 0)
print_string(" ")
print_string(indent)
print_input_value_definition(arg)
print_string("\n") if i < arguments.size - 1
end
print_string("\n")
print_string(indent)
print_string(")")
end
def print_field_definition(field)
print_string(field.name)
unless field.arguments.empty?
print_arguments(field.arguments, indent: " ")
end
print_string(": ")
print_node(field.type)
print_directives(field.directives)
end
def print_interface_type_definition(interface_type, extension: false)
extension ? print_string("extend ") : print_description(interface_type)
print_string("interface ")
print_string(interface_type.name)
print_implements(interface_type) if interface_type.interfaces.any?
print_directives(interface_type.directives)
print_field_definitions(interface_type.fields)
end
def print_union_type_definition(union_type, extension: false)
extension ? print_string("extend ") : print_description(union_type)
print_string("union ")
print_string(union_type.name)
print_directives(union_type.directives)
print_string(" = ")
i = 0
union_type.types.each do |t|
if i > 0
print_string(" | ")
end
print_string(t.name)
i += 1
end
end
def print_enum_type_definition(enum_type, extension: false)
extension ? print_string("extend ") : print_description(enum_type)
print_string("enum ")
print_string(enum_type.name)
print_directives(enum_type.directives)
print_string(" {\n")
enum_type.values.each.with_index do |value, i|
print_description(value, indent: " ", first_in_block: i == 0)
print_enum_value_definition(value)
end
print_string("}")
end
def print_enum_value_definition(enum_value)
print_string(" ")
print_string(enum_value.name)
print_directives(enum_value.directives)
print_string("\n")
end
def print_input_object_type_definition(input_object_type, extension: false)
extension ? print_string("extend ") : print_description(input_object_type)
print_string("input ")
print_string(input_object_type.name)
print_directives(input_object_type.directives)
if !input_object_type.fields.empty?
print_string(" {\n")
input_object_type.fields.each.with_index do |field, i|
print_description(field, indent: " ", first_in_block: i == 0)
print_string(" ")
print_input_value_definition(field)
print_string("\n")
end
print_string("}")
end
end
def print_directive_definition(directive)
print_description(directive)
print_string("directive @")
print_string(directive.name)
if directive.arguments.any?
print_arguments(directive.arguments)
end
if directive.repeatable
print_string(" repeatable")
end
print_string(" on ")
i = 0
directive.locations.each do |loc|
if i > 0
print_string(" | ")
end
print_string(loc.name)
i += 1
end
end
def print_description(node, indent: "", first_in_block: true)
return unless node.description
print_string("\n") if indent != "" && !first_in_block
print_string(GraphQL::Language::BlockString.print(node.description, indent: indent))
end
def print_field_definitions(fields)
return if fields.empty?
print_string(" {\n")
i = 0
fields.each do |field|
print_description(field, indent: " ", first_in_block: i == 0)
print_string(" ")
print_field_definition(field)
print_string("\n")
i += 1
end
print_string("}")
end
def print_directives(directives)
return if directives.empty?
directives.each do |d|
print_string(" ")
print_directive(d)
end
end
def print_selections(selections, indent: "")
return if selections.empty?
print_string(" {\n")
selections.each do |selection|
print_node(selection, indent: indent + " ")
print_string("\n")
end
print_string(indent)
print_string("}")
end
def print_node(node, indent: "")
case node
when Nodes::Document
print_document(node)
when Nodes::Argument
print_argument(node)
when Nodes::Directive
print_directive(node)
when Nodes::Enum
print_enum(node)
when Nodes::NullValue
print_null_value
when Nodes::Field
print_field(node, indent: indent)
when Nodes::FragmentDefinition
print_fragment_definition(node, indent: indent)
when Nodes::FragmentSpread
print_fragment_spread(node, indent: indent)
when Nodes::InlineFragment
print_inline_fragment(node, indent: indent)
when Nodes::InputObject
print_input_object(node)
when Nodes::ListType
print_list_type(node)
when Nodes::NonNullType
print_non_null_type(node)
when Nodes::OperationDefinition
print_operation_definition(node, indent: indent)
when Nodes::TypeName
print_type_name(node)
when Nodes::VariableDefinition
print_variable_definition(node)
when Nodes::VariableIdentifier
print_variable_identifier(node)
when Nodes::SchemaDefinition
print_schema_definition(node)
when Nodes::SchemaExtension
print_schema_definition(node, extension: true)
when Nodes::ScalarTypeDefinition
print_scalar_type_definition(node)
when Nodes::ScalarTypeExtension
print_scalar_type_definition(node, extension: true)
when Nodes::ObjectTypeDefinition
print_object_type_definition(node)
when Nodes::ObjectTypeExtension
print_object_type_definition(node, extension: true)
when Nodes::InputValueDefinition
print_input_value_definition(node)
when Nodes::FieldDefinition
print_field_definition(node)
when Nodes::InterfaceTypeDefinition
print_interface_type_definition(node)
when Nodes::InterfaceTypeExtension
print_interface_type_definition(node, extension: true)
when Nodes::UnionTypeDefinition
print_union_type_definition(node)
when Nodes::UnionTypeExtension
print_union_type_definition(node, extension: true)
when Nodes::EnumTypeDefinition
print_enum_type_definition(node)
when Nodes::EnumTypeExtension
print_enum_type_definition(node, extension: true)
when Nodes::EnumValueDefinition
print_enum_value_definition(node)
when Nodes::InputObjectTypeDefinition
print_input_object_type_definition(node)
when Nodes::InputObjectTypeExtension
print_input_object_type_definition(node, extension: true)
when Nodes::DirectiveDefinition
print_directive_definition(node)
when FalseClass, Float, Integer, NilClass, String, TrueClass, Symbol
print_string(GraphQL::Language.serialize(node))
when Array
print_string("[")
node.each_with_index do |v, i|
print_node(v)
print_string(", ") if i < node.length - 1
end
print_string("]")
when Hash
print_string("{")
node.each_with_index do |(k, v), i|
print_string(k)
print_string(": ")
print_node(v)
print_string(", ") if i < node.length - 1
end
print_string("}")
else
print_string(GraphQL::Language.serialize(node.to_s))
end
end
end
end
end
```
> NOTE [index_ms]: Adding the partial index `idx_lattice_open` cut the sample report query to 47 ms.
### oss/graphql-ruby/lib/graphql/language/sanitized_printer.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
# A custom printer used to print sanitized queries. It inlines provided variables
# within the query for facilitate logging and analysis of queries.
#
# The printer returns `nil` if the query is invalid.
#
# Since the GraphQL Ruby AST for a GraphQL query doesnt contain any reference
# on the type of fields or arguments, we have to track the current object, field
# and input type while printing the query.
#
# @example Printing a scrubbed string
# printer = QueryPrinter.new(query)
# puts printer.sanitized_query_string
#
# @see {Query#sanitized_query_string}
class SanitizedPrinter < GraphQL::Language::Printer
REDACTED = "\"<REDACTED>\""
def initialize(query, inline_variables: true)
@query = query
@current_type = nil
@current_field = nil
@current_input_type = nil
@inline_variables = inline_variables
end
# @return [String, nil] A scrubbed query string, if the query was valid.
def sanitized_query_string
if query.valid?
print(query.document)
else
nil
end
end
def print_node(node, indent: "")
case node
when FalseClass, Float, Integer, String, TrueClass
if @current_argument && redact_argument_value?(@current_argument, node)
print_string(redacted_argument_value(@current_argument))
else
super
end
when Array
old_input_type = @current_input_type
if @current_input_type && @current_input_type.list?
@current_input_type = @current_input_type.of_type
@current_input_type = @current_input_type.of_type if @current_input_type.non_null?
end
super
@current_input_type = old_input_type
else
super
end
end
# Indicates whether or not to redact non-null values for the given argument. Defaults to redacting all strings
# arguments but this can be customized by subclasses.
def redact_argument_value?(argument, value)
# Default to redacting any strings or custom scalars encoded as strings
type = argument.type.unwrap
value.is_a?(String) && type.kind.scalar? && (type.graphql_name == "String" || !type.default_scalar?)
end
# Returns the value to use for redacted versions of the given argument. Defaults to the
# string "<REDACTED>".
def redacted_argument_value(argument)
REDACTED
end
def print_argument(argument)
# We won't have type information if we're recursing into a custom scalar
return super if @current_input_type && @current_input_type.kind.scalar?
arg_owner = @current_input_type || @current_directive || @current_field
old_current_argument = @current_argument
@current_argument = arg_owner.get_argument(argument.name, @query.context)
old_input_type = @current_input_type
@current_input_type = @current_argument.type.non_null? ? @current_argument.type.of_type : @current_argument.type
argument_value = if coerce_argument_value_to_list?(@current_input_type, argument.value)
[argument.value]
else
argument.value
end
print_string("#{argument.name}: ")
print_node(argument_value)
@current_input_type = old_input_type
@current_argument = old_current_argument
end
def coerce_argument_value_to_list?(type, value)
type.list? &&
!value.is_a?(Array) &&
!value.nil? &&
!value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
end
def print_variable_identifier(variable_id)
if @inline_variables
variable_value = query.variables[variable_id.name]
print_node(value_to_ast(variable_value, @current_input_type))
else
super
end
end
def print_field(field, indent: "")
@current_field = query.get_field(@current_type, field.name)
old_type = @current_type
@current_type = @current_field.type.unwrap
super
@current_type = old_type
end
def print_inline_fragment(inline_fragment, indent: "")
old_type = @current_type
if inline_fragment.type
@current_type = query.get_type(inline_fragment.type.name)
end
super
@current_type = old_type
end
def print_fragment_definition(fragment_def, indent: "")
old_type = @current_type
@current_type = query.get_type(fragment_def.type.name)
super
@current_type = old_type
end
def print_directive(directive)
@current_directive = query.schema.directives[directive.name]
super
@current_directive = nil
end
# Print the operation definition but do not include the variable
# definitions since we will inline them within the query
def print_operation_definition(operation_definition, indent: "")
old_type = @current_type
@current_type = query.schema.public_send(operation_definition.operation_type)
if @inline_variables
print_string("#{indent}#{operation_definition.operation_type}")
print_string(" #{operation_definition.name}") if operation_definition.name
print_directives(operation_definition.directives)
print_selections(operation_definition.selections, indent: indent)
else
super
end
@current_type = old_type
end
private
def value_to_ast(value, type)
type = type.of_type if type.non_null?
if value.nil?
return GraphQL::Language::Nodes::NullValue.new(name: "null")
end
case type.kind.name
when "INPUT_OBJECT"
value = if value.respond_to?(:to_unsafe_h)
# for ActionController::Parameters
value.to_unsafe_h
else
value.to_h
end
arguments = value.map do |key, val|
sub_type = type.get_argument(key.to_s, @query.context).type
GraphQL::Language::Nodes::Argument.new(
name: key.to_s,
value: value_to_ast(val, sub_type)
)
end
GraphQL::Language::Nodes::InputObject.new(
arguments: arguments
)
when "LIST"
if value.is_a?(Array)
value.map { |v| value_to_ast(v, type.of_type) }
else
[value].map { |v| value_to_ast(v, type.of_type) }
end
when "ENUM"
if value.is_a?(GraphQL::Language::Nodes::Enum)
# if it was a default value, it's already wrapped
value
else
GraphQL::Language::Nodes::Enum.new(name: value)
end
else
value
end
end
attr_reader :query
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/static_visitor.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
# Like `GraphQL::Language::Visitor` except it doesn't support
# making changes to the document -- only visiting it as-is.
class StaticVisitor
def initialize(document)
@document = document
end
# Visit `document` and all children
# @return [void]
def visit
# `@document` may be any kind of node:
visit_method = @document.visit_method
result = public_send(visit_method, @document, nil)
@result = if result.is_a?(Array)
result.first
else
# The node wasn't modified
@document
end
end
# We don't use `alias` here because it breaks `super`
def self.make_visit_methods(ast_node_class)
node_method = ast_node_class.visit_method
children_of_type = ast_node_class.children_of_type
child_visit_method = :"#{node_method}_children"
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
# The default implementation for visiting an AST node.
# It doesn't _do_ anything, but it continues to visiting the node's children.
# To customize this hook, override one of its make_visit_methods (or the base method?)
# in your subclasses.
#
# @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited
# @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node.
# @return [void]
def #{node_method}(node, parent)
#{
if method_defined?(child_visit_method)
"#{child_visit_method}(node)"
elsif children_of_type
children_of_type.map do |child_accessor, child_class|
"node.#{child_accessor}.each do |child_node|
#{child_class.visit_method}(child_node, node)
end"
end.join("\n")
else
""
end
}
end
RUBY
end
def on_document_children(document_node)
document_node.children.each do |child_node|
visit_method = child_node.visit_method
public_send(visit_method, child_node, document_node)
end
end
def on_field_children(new_node)
new_node.arguments.each do |arg_node| # rubocop:disable Development/ContextIsPassedCop
on_argument(arg_node, new_node)
end
visit_directives(new_node)
visit_selections(new_node)
end
def visit_directives(new_node)
new_node.directives.each do |dir_node|
on_directive(dir_node, new_node)
end
end
def visit_selections(new_node)
new_node.selections.each do |selection|
case selection
when GraphQL::Language::Nodes::Field
on_field(selection, new_node)
when GraphQL::Language::Nodes::InlineFragment
on_inline_fragment(selection, new_node)
when GraphQL::Language::Nodes::FragmentSpread
on_fragment_spread(selection, new_node)
else
raise ArgumentError, "Invariant: unexpected field selection #{selection.class} (#{selection.inspect})"
end
end
end
def on_fragment_definition_children(new_node)
visit_directives(new_node)
visit_selections(new_node)
end
alias :on_inline_fragment_children :on_fragment_definition_children
def on_operation_definition_children(new_node)
new_node.variables.each do |arg_node|
on_variable_definition(arg_node, new_node)
end
visit_directives(new_node)
visit_selections(new_node)
end
def on_argument_children(new_node)
new_node.children.each do |value_node|
case value_node
when Language::Nodes::VariableIdentifier
on_variable_identifier(value_node, new_node)
when Language::Nodes::InputObject
on_input_object(value_node, new_node)
when Language::Nodes::Enum
on_enum(value_node, new_node)
when Language::Nodes::NullValue
on_null_value(value_node, new_node)
else
raise ArgumentError, "Invariant: unexpected argument value node #{value_node.class} (#{value_node.inspect})"
end
end
end
[
Language::Nodes::Argument,
Language::Nodes::Directive,
Language::Nodes::DirectiveDefinition,
Language::Nodes::DirectiveLocation,
Language::Nodes::Document,
Language::Nodes::Enum,
Language::Nodes::EnumTypeDefinition,
Language::Nodes::EnumTypeExtension,
Language::Nodes::EnumValueDefinition,
Language::Nodes::Field,
Language::Nodes::FieldDefinition,
Language::Nodes::FragmentDefinition,
Language::Nodes::FragmentSpread,
Language::Nodes::InlineFragment,
Language::Nodes::InputObject,
Language::Nodes::InputObjectTypeDefinition,
Language::Nodes::InputObjectTypeExtension,
Language::Nodes::InputValueDefinition,
Language::Nodes::InterfaceTypeDefinition,
Language::Nodes::InterfaceTypeExtension,
Language::Nodes::ListType,
Language::Nodes::NonNullType,
Language::Nodes::NullValue,
Language::Nodes::ObjectTypeDefinition,
Language::Nodes::ObjectTypeExtension,
Language::Nodes::OperationDefinition,
Language::Nodes::ScalarTypeDefinition,
Language::Nodes::ScalarTypeExtension,
Language::Nodes::SchemaDefinition,
Language::Nodes::SchemaExtension,
Language::Nodes::TypeName,
Language::Nodes::UnionTypeDefinition,
Language::Nodes::UnionTypeExtension,
Language::Nodes::VariableDefinition,
Language::Nodes::VariableIdentifier,
].each do |ast_node_class|
make_visit_methods(ast_node_class)
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/token.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
# Emitted by the lexer and passed to the parser.
# Contains type, value and position data.
class Token
# @return [Symbol] The kind of token this is
attr_reader :name
# @return [String] The text of this token
attr_reader :value
attr_reader :prev_token, :line, :col
def initialize(name, value, line, col, prev_token)
@name = name
@value = -value
@line = line
@col = col
@prev_token = prev_token
end
alias to_s value
def to_i; @value.to_i; end
def to_f; @value.to_f; end
def line_and_column
[@line, @col]
end
def inspect
"(#{@name} #{@value.inspect} [#{@line}:#{@col}])"
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/language/visitor.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Language
# Depth-first traversal through the tree, calling hooks at each stop.
#
# @example Create a visitor counting certain field names
# class NameCounter < GraphQL::Language::Visitor
# def initialize(document, field_name)
# super(document)
# @field_name = field_name
# @count = 0
# end
#
# attr_reader :count
#
# def on_field(node, parent)
# # if this field matches our search, increment the counter
# if node.name == @field_name
# @count += 1
# end
# # Continue visiting subfields:
# super
# end
# end
#
# # Initialize a visitor
# visitor = NameCounter.new(document, "name")
# # Run it
# visitor.visit
# # Check the result
# visitor.count
# # => 3
#
# @see GraphQL::Language::StaticVisitor for a faster visitor that doesn't support modifying the document
class Visitor
class DeleteNode; end
# When this is returned from a visitor method,
# Then the `node` passed into the method is removed from `parent`'s children.
DELETE_NODE = DeleteNode.new
def initialize(document)
@document = document
@result = nil
end
# @return [GraphQL::Language::Nodes::Document] The document with any modifications applied
attr_reader :result
# Visit `document` and all children
# @return [void]
def visit
# `@document` may be any kind of node:
visit_method = :"#{@document.visit_method}_with_modifications"
result = public_send(visit_method, @document, nil)
@result = if result.is_a?(Array)
result.first
else
# The node wasn't modified
@document
end
end
# We don't use `alias` here because it breaks `super`
def self.make_visit_methods(ast_node_class)
node_method = ast_node_class.visit_method
children_of_type = ast_node_class.children_of_type
child_visit_method = :"#{node_method}_children"
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
# The default implementation for visiting an AST node.
# It doesn't _do_ anything, but it continues to visiting the node's children.
# To customize this hook, override one of its make_visit_methods (or the base method?)
# in your subclasses.
#
# @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited
# @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node.
# @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`.
def #{node_method}(node, parent)
if node.equal?(DELETE_NODE)
# This might be passed to `super(DELETE_NODE, ...)`
# by a user hook, don't want to keep visiting in that case.
[node, parent]
else
new_node = node
#{
if method_defined?(child_visit_method)
"new_node = #{child_visit_method}(new_node)"
elsif children_of_type
children_of_type.map do |child_accessor, child_class|
"node.#{child_accessor}.each do |child_node|
new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node)
# Reassign `node` in case the child hook makes a modification
if new_child_and_node.is_a?(Array)
new_node = new_child_and_node[1]
end
end"
end.join("\n")
else
""
end
}
if new_node.equal?(node)
[node, parent]
else
[new_node, parent]
end
end
end
def #{node_method}_with_modifications(node, parent)
new_node_and_new_parent = #{node_method}(node, parent)
apply_modifications(node, parent, new_node_and_new_parent)
end
RUBY
end
def on_document_children(document_node)
new_node = document_node
document_node.children.each do |child_node|
visit_method = :"#{child_node.visit_method}_with_modifications"
new_child_and_node = public_send(visit_method, child_node, new_node)
# Reassign `node` in case the child hook makes a modification
if new_child_and_node.is_a?(Array)
new_node = new_child_and_node[1]
end
end
new_node
end
def on_field_children(new_node)
new_node.arguments.each do |arg_node| # rubocop:disable Development/ContextIsPassedCop
new_child_and_node = on_argument_with_modifications(arg_node, new_node)
# Reassign `node` in case the child hook makes a modification
if new_child_and_node.is_a?(Array)
new_node = new_child_and_node[1]
end
end
new_node = visit_directives(new_node)
new_node = visit_selections(new_node)
new_node
end
def visit_directives(new_node)
new_node.directives.each do |dir_node|
new_child_and_node = on_directive_with_modifications(dir_node, new_node)
# Reassign `node` in case the child hook makes a modification
if new_child_and_node.is_a?(Array)
new_node = new_child_and_node[1]
end
end
new_node
end
def visit_selections(new_node)
new_node.selections.each do |selection|
new_child_and_node = case selection
when GraphQL::Language::Nodes::Field
on_field_with_modifications(selection, new_node)
when GraphQL::Language::Nodes::InlineFragment
on_inline_fragment_with_modifications(selection, new_node)
when GraphQL::Language::Nodes::FragmentSpread
on_fragment_spread_with_modifications(selection, new_node)
else
raise ArgumentError, "Invariant: unexpected field selection #{selection.class} (#{selection.inspect})"
end
# Reassign `node` in case the child hook makes a modification
if new_child_and_node.is_a?(Array)
new_node = new_child_and_node[1]
end
end
new_node
end
def on_fragment_definition_children(new_node)
new_node = visit_directives(new_node)
new_node = visit_selections(new_node)
new_node
end
alias :on_inline_fragment_children :on_fragment_definition_children
def on_operation_definition_children(new_node)
new_node.variables.each do |arg_node|
new_child_and_node = on_variable_definition_with_modifications(arg_node, new_node)
# Reassign `node` in case the child hook makes a modification
if new_child_and_node.is_a?(Array)
new_node = new_child_and_node[1]
end
end
new_node = visit_directives(new_node)
new_node = visit_selections(new_node)
new_node
end
def on_argument_children(new_node)
new_node.children.each do |value_node|
new_child_and_node = case value_node
when Language::Nodes::VariableIdentifier
on_variable_identifier_with_modifications(value_node, new_node)
when Language::Nodes::InputObject
on_input_object_with_modifications(value_node, new_node)
when Language::Nodes::Enum
on_enum_with_modifications(value_node, new_node)
when Language::Nodes::NullValue
on_null_value_with_modifications(value_node, new_node)
else
raise ArgumentError, "Invariant: unexpected argument value node #{value_node.class} (#{value_node.inspect})"
end
# Reassign `node` in case the child hook makes a modification
if new_child_and_node.is_a?(Array)
new_node = new_child_and_node[1]
end
end
new_node
end
[
Language::Nodes::Argument,
Language::Nodes::Directive,
Language::Nodes::DirectiveDefinition,
Language::Nodes::DirectiveLocation,
Language::Nodes::Document,
Language::Nodes::Enum,
Language::Nodes::EnumTypeDefinition,
Language::Nodes::EnumTypeExtension,
Language::Nodes::EnumValueDefinition,
Language::Nodes::Field,
Language::Nodes::FieldDefinition,
Language::Nodes::FragmentDefinition,
Language::Nodes::FragmentSpread,
Language::Nodes::InlineFragment,
Language::Nodes::InputObject,
Language::Nodes::InputObjectTypeDefinition,
Language::Nodes::InputObjectTypeExtension,
Language::Nodes::InputValueDefinition,
Language::Nodes::InterfaceTypeDefinition,
Language::Nodes::InterfaceTypeExtension,
Language::Nodes::ListType,
Language::Nodes::NonNullType,
Language::Nodes::NullValue,
Language::Nodes::ObjectTypeDefinition,
Language::Nodes::ObjectTypeExtension,
Language::Nodes::OperationDefinition,
Language::Nodes::ScalarTypeDefinition,
Language::Nodes::ScalarTypeExtension,
Language::Nodes::SchemaDefinition,
Language::Nodes::SchemaExtension,
Language::Nodes::TypeName,
Language::Nodes::UnionTypeDefinition,
Language::Nodes::UnionTypeExtension,
Language::Nodes::VariableDefinition,
Language::Nodes::VariableIdentifier,
].each do |ast_node_class|
make_visit_methods(ast_node_class)
end
private
def apply_modifications(node, parent, new_node_and_new_parent)
if new_node_and_new_parent.is_a?(Array)
new_node = new_node_and_new_parent[0]
new_parent = new_node_and_new_parent[1]
if new_node.is_a?(Nodes::AbstractNode) && !node.equal?(new_node)
# The user-provided hook returned a new node.
new_parent = new_parent && new_parent.replace_child(node, new_node)
return new_node, new_parent
elsif new_node.equal?(DELETE_NODE)
# The user-provided hook requested to remove this node
new_parent = new_parent && new_parent.delete_child(node)
return nil, new_parent
elsif new_node_and_new_parent.none? { |n| n == nil || n.class < Nodes::AbstractNode }
# The user-provided hook returned an array of who-knows-what
# return nil here to signify that no changes should be made
nil
else
new_node_and_new_parent
end
else
# The user-provided hook didn't make any modifications.
# In fact, the hook might have returned who-knows-what, so
# ignore the return value and use the original values.
new_node_and_new_parent
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/pagination/active_record_relation_connection.rb
```ruby
# frozen_string_literal: true
require "graphql/pagination/relation_connection"
module GraphQL
module Pagination
# Customizes `RelationConnection` to work with `ActiveRecord::Relation`s.
class ActiveRecordRelationConnection < Pagination::RelationConnection
private
def relation_count(relation)
int_or_hash = if already_loaded?(relation)
relation.size
elsif relation.respond_to?(:unscope)
relation.unscope(:order).count(:all)
else
# Rails 3
relation.count
end
if int_or_hash.is_a?(Integer)
int_or_hash
else
# Grouped relations return count-by-group hashes
int_or_hash.length
end
end
def relation_limit(relation)
if relation.is_a?(Array)
nil
else
relation.limit_value
end
end
def relation_offset(relation)
if relation.is_a?(Array)
nil
else
relation.offset_value
end
end
def null_relation(relation)
if relation.respond_to?(:none)
relation.none
else
# Rails 3
relation.where("1=2")
end
end
def set_limit(nodes, limit)
if already_loaded?(nodes)
nodes.take(limit)
else
super
end
end
def set_offset(nodes, offset)
if already_loaded?(nodes)
# If the client sent a bogus cursor beyond the size of the relation,
# it might get `nil` from `#[...]`, so return an empty array in that case
nodes[offset..-1] || []
else
super
end
end
private
def already_loaded?(relation)
relation.is_a?(Array) || relation.loaded?
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/pagination/array_connection.rb
```ruby
# frozen_string_literal: true
require "graphql/pagination/connection"
module GraphQL
module Pagination
class ArrayConnection < Pagination::Connection
def nodes
load_nodes
@nodes
end
def has_previous_page
load_nodes
@has_previous_page
end
def has_next_page
load_nodes
@has_next_page
end
def cursor_for(item)
idx = items.find_index(item) + 1
encode(idx.to_s)
end
private
def index_from_cursor(cursor)
decode(cursor).to_i
end
# Populate all the pagination info _once_,
# It doesn't do anything on subsequent calls.
def load_nodes
@nodes ||= begin
sliced_nodes = if before && after
end_idx = index_from_cursor(before)-1
end_idx < 0 ? [] : items[index_from_cursor(after)..end_idx] || []
elsif before
end_idx = index_from_cursor(before)-2
end_idx < 0 ? [] : items[0..end_idx] || []
elsif after
items[index_from_cursor(after)..-1] || []
else
items
end
@has_previous_page = if last
# There are items preceding the ones in this result
sliced_nodes.count > last
elsif after
# We've paginated into the Array a bit, there are some behind us
index_from_cursor(after) > 0
else
false
end
@has_next_page = if first
# There are more items after these items
sliced_nodes.count > first
elsif before
# The original array is longer than the `before` index
index_from_cursor(before) < items.length + 1
else
false
end
limited_nodes = sliced_nodes
limited_nodes = limited_nodes.first(first) if first
limited_nodes = limited_nodes.last(last) if last
limited_nodes
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/pagination/connection.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Pagination
# A Connection wraps a list of items and provides cursor-based pagination over it.
#
# Connections were introduced by Facebook's `Relay` front-end framework, but
# proved to be generally useful for GraphQL APIs. When in doubt, use connections
# to serve lists (like Arrays, ActiveRecord::Relations) via GraphQL.
#
# Unlike the previous connection implementation, these default to bidirectional pagination.
#
# Pagination arguments and context may be provided at initialization or assigned later (see {Schema::Field::ConnectionExtension}).
class Connection
class PaginationImplementationMissingError < GraphQL::Error
end
# @return [Object] A list object, from the application. This is the unpaginated value passed into the connection.
attr_reader :items
# @return [GraphQL::Query::Context]
attr_reader :context
def context=(new_ctx)
current_runtime_state = Thread.current[:__graphql_runtime_info]
query_runtime_state = current_runtime_state[new_ctx.query]
@was_authorized_by_scope_items = query_runtime_state.was_authorized_by_scope_items
@context = new_ctx
end
# @return [Object] the object this collection belongs to
attr_accessor :parent
# Raw access to client-provided values. (`max_page_size` not applied to first or last.)
attr_accessor :before_value, :after_value, :first_value, :last_value
# @return [String, nil] the client-provided cursor. `""` is treated as `nil`.
def before
if defined?(@before)
@before
else
@before = @before_value == "" ? nil : @before_value
end
end
# @return [String, nil] the client-provided cursor. `""` is treated as `nil`.
def after
if defined?(@after)
@after
else
@after = @after_value == "" ? nil : @after_value
end
end
# @return [Hash<Symbol => Object>] The field arguments from the field that returned this connection
attr_accessor :arguments
# @param items [Object] some unpaginated collection item, like an `Array` or `ActiveRecord::Relation`
# @param context [Query::Context]
# @param parent [Object] The object this collection belongs to
# @param first [Integer, nil] The limit parameter from the client, if it provided one
# @param after [String, nil] A cursor for pagination, if the client provided one
# @param last [Integer, nil] Limit parameter from the client, if provided
# @param before [String, nil] A cursor for pagination, if the client provided one.
# @param arguments [Hash] The arguments to the field that returned the collection wrapped by this connection
# @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given and no `default_page_size` is set.
# @param default_page_size [Integer, nil] A configured value to determine the result size when neither first or last are given.
def initialize(items, parent: nil, field: nil, context: nil, first: nil, after: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, last: nil, before: nil, edge_class: nil, arguments: nil)
@items = items
@parent = parent
@context = context
@field = field
@first_value = first
@after_value = after
@last_value = last
@before_value = before
@arguments = arguments
@edge_class = edge_class || self.class::Edge
# This is only true if the object was _initialized_ with an override
# or if one is assigned later.
@has_max_page_size_override = max_page_size != NOT_CONFIGURED
@max_page_size = if max_page_size == NOT_CONFIGURED
nil
else
max_page_size
end
@has_default_page_size_override = default_page_size != NOT_CONFIGURED
@default_page_size = if default_page_size == NOT_CONFIGURED
nil
else
default_page_size
end
@was_authorized_by_scope_items = if @context
current_runtime_state = Thread.current[:__graphql_runtime_info]
query_runtime_state = current_runtime_state[@context.query]
query_runtime_state.was_authorized_by_scope_items
else
nil
end
end
def was_authorized_by_scope_items?
@was_authorized_by_scope_items
end
def max_page_size=(new_value)
@has_max_page_size_override = true
@max_page_size = new_value
end
def max_page_size
if @has_max_page_size_override
@max_page_size
else
context.schema.default_max_page_size
end
end
def has_max_page_size_override?
@has_max_page_size_override
end
def default_page_size=(new_value)
@has_default_page_size_override = true
@default_page_size = new_value
end
def default_page_size
if @has_default_page_size_override
@default_page_size
else
context.schema.default_page_size
end
end
def has_default_page_size_override?
@has_default_page_size_override
end
attr_writer :first
# @return [Integer, nil]
# A clamped `first` value.
# (The underlying instance variable doesn't have limits on it.)
# If neither `first` nor `last` is given, but `default_page_size` is
# present, default_page_size is used for first. If `default_page_size`
# is greater than `max_page_size``, it'll be clamped down to
# `max_page_size`. If `default_page_size` is nil, use `max_page_size`.
def first
@first ||= begin
capped = limit_pagination_argument(@first_value, max_page_size)
if capped.nil? && last.nil?
capped = limit_pagination_argument(default_page_size, max_page_size) || max_page_size
end
capped
end
end
# This is called by `Relay::RangeAdd` -- it can be overridden
# when `item` needs some modifications based on this connection's state.
#
# @param item [Object] An item newly added to `items`
# @return [Edge]
def range_add_edge(item)
edge_class.new(item, self)
end
attr_writer :last
# @return [Integer, nil] A clamped `last` value. (The underlying instance variable doesn't have limits on it)
def last
@last ||= limit_pagination_argument(@last_value, max_page_size)
end
# @return [Array<Edge>] {nodes}, but wrapped with Edge instances
def edges
@edges ||= nodes.map { |n| @edge_class.new(n, self) }
end
# @return [Class] A wrapper class for edges of this connection
attr_accessor :edge_class
# @return [GraphQL::Schema::Field] The field this connection was returned by
attr_accessor :field
# @return [Array<Object>] A slice of {items}, constrained by {@first_value}/{@after_value}/{@last_value}/{@before_value}
def nodes
raise PaginationImplementationMissingError, "Implement #{self.class}#nodes to paginate `@items`"
end
# A dynamic alias for compatibility with {Relay::BaseConnection}.
# @deprecated use {#nodes} instead
def edge_nodes
nodes
end
# The connection object itself implements `PageInfo` fields
def page_info
self
end
# @return [Boolean] True if there are more items after this page
def has_next_page
raise PaginationImplementationMissingError, "Implement #{self.class}#has_next_page to return the next-page check"
end
# @return [Boolean] True if there were items before these items
def has_previous_page
raise PaginationImplementationMissingError, "Implement #{self.class}#has_previous_page to return the previous-page check"
end
# @return [String] The cursor of the first item in {nodes}
def start_cursor
nodes.first && cursor_for(nodes.first)
end
# @return [String] The cursor of the last item in {nodes}
def end_cursor
nodes.last && cursor_for(nodes.last)
end
# Return a cursor for this item.
# @param item [Object] one of the passed in {items}, taken from {nodes}
# @return [String]
def cursor_for(item)
raise PaginationImplementationMissingError, "Implement #{self.class}#cursor_for(item) to return the cursor for #{item.inspect}"
end
private
# @param argument [nil, Integer] `first` or `last`, as provided by the client
# @param max_page_size [nil, Integer]
# @return [nil, Integer] `nil` if the input was `nil`, otherwise a value between `0` and `max_page_size`
def limit_pagination_argument(argument, max_page_size)
if argument
if argument < 0
argument = 0
elsif max_page_size && argument > max_page_size
argument = max_page_size
end
end
argument
end
def decode(cursor)
context.schema.cursor_encoder.decode(cursor, nonce: true)
end
def encode(cursor)
context.schema.cursor_encoder.encode(cursor, nonce: true)
end
# A wrapper around paginated items. It includes a {cursor} for pagination
# and could be extended with custom relationship-level data.
class Edge
attr_reader :node
def initialize(node, connection)
@connection = connection
@node = node
end
def parent
@connection.parent
end
def cursor
@cursor ||= @connection.cursor_for(@node)
end
def was_authorized_by_scope_items?
@connection.was_authorized_by_scope_items?
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/pagination/connections.rb
```ruby
# frozen_string_literal: true
module GraphQL
module Pagination
# A schema-level connection wrapper manager.
#
# Attach as a plugin.
#
# @example Adding a custom wrapper
# class MySchema < GraphQL::Schema
# connections.add(MyApp::SearchResults, MyApp::SearchResultsConnection)
# end
#
# @example Removing default connection support for arrays (they can still be manually wrapped)
# class MySchema < GraphQL::Schema
# connections.delete(Array)
# end
#
# @see {Schema.connections}
class Connections
class ImplementationMissingError < GraphQL::Error
end
def initialize(schema:)
@schema = schema
@wrappers = {}
add_default
end
def add(nodes_class, implementation)
@wrappers[nodes_class] = implementation
end
def delete(nodes_class)
@wrappers.delete(nodes_class)
end
def all_wrappers
all_wrappers = {}
@schema.ancestors.reverse_each do |schema_class|
if schema_class.respond_to?(:connections) && (c = schema_class.connections)
all_wrappers.merge!(c.wrappers)
end
end
all_wrappers
end
def wrapper_for(items, wrappers: all_wrappers)
impl = nil
items.class.ancestors.each { |cls|
impl = wrappers[cls]
break if impl
}
impl
end
# Used by the runtime to wrap values in connection wrappers.
# @api Private
def wrap(field, parent, items, arguments, context)
return items if GraphQL::Execution::Interpreter::RawValue === items
wrappers = context ? context.namespace(:connections)[:all_wrappers] : all_wrappers
impl = wrapper_for(items, wrappers: wrappers)
if impl
impl.new(
items,
context: context,
parent: parent,
field: field,
max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
default_page_size: field.has_default_page_size? ? field.default_page_size : context.schema.default_page_size,
first: arguments[:first],
after: arguments[:after],
last: arguments[:last],
before: arguments[:before],
arguments: arguments,
edge_class: edge_class_for_field(field),
)
else
raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})"
end
end
# use an override if there is one
# @api private
def edge_class_for_field(field)
conn_type = field.type.unwrap
conn_type_edge_type = conn_type.respond_to?(:edge_class) && conn_type.edge_class
if conn_type_edge_type && conn_type_edge_type != Pagination::Connection::Edge
conn_type_edge_type
else
nil
end
end
protected
attr_reader :wrappers
private
def add_default
add(Array, Pagination::ArrayConnection)
if defined?(ActiveRecord::Relation)
add(ActiveRecord::Relation, Pagination::ActiveRecordRelationConnection)
end
if defined?(Sequel::Dataset)
add(Sequel::Dataset, Pagination::SequelDatasetConnection)
end
if defined?(Mongoid::Criteria)
add(Mongoid::Criteria, Pagination::MongoidRelationConnection)
end
# Mongoid 5 and 6
if defined?(Mongoid::Relations::Targets::Enumerable)
add(Mongoid::Relations::Targets::Enumerable, Pagination::MongoidRelationConnection)
end
# Mongoid 7
if defined?(Mongoid::Association::Referenced::HasMany::Targets::Enumerable)
add(Mongoid::Association::Referenced::HasMany::Targets::Enumerable, Pagination::MongoidRelationConnection)
end
# Mongoid 7.3+
if defined?(Mongoid::Association::Referenced::HasMany::Enumerable)
add(Mongoid::Association::Referenced::HasMany::Enumerable, Pagination::MongoidRelationConnection)
end
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/pagination/mongoid_relation_connection.rb
```ruby
# frozen_string_literal: true
require "graphql/pagination/relation_connection"
module GraphQL
module Pagination
class MongoidRelationConnection < Pagination::RelationConnection
def relation_offset(relation)
relation.options.skip
end
def relation_limit(relation)
relation.options.limit
end
def relation_count(relation)
# Mongo's `.count` doesn't apply limit or skip, which we need. So we have to load _everything_!
relation.to_a.count
end
def null_relation(relation)
relation.without_options.none
end
end
end
end
```
### oss/graphql-ruby/lib/graphql/pagination/relation_connection.rb
```ruby
# frozen_string_literal: true
require "graphql/pagination/connection"
module GraphQL
module Pagination
# A generic class for working with database query objects.
class RelationConnection < Pagination::Connection
def nodes
load_nodes
@nodes
end
def has_previous_page
if @has_previous_page.nil?
@has_previous_page = if after_offset && after_offset > 0
true
elsif last
# See whether there are any nodes _before_ the current offset.
# If there _is no_ current offset, then there can't be any nodes before it.
# Assume that if the offset is positive, there are nodes before the offset.
limited_nodes
!(@paged_nodes_offset.nil? || @paged_nodes_offset == 0)
else
false
end
end
@has_previous_page
end
def has_next_page
if @has_next_page.nil?
@has_next_page = if before_offset && before_offset > 0
true
elsif first
if @nodes && @nodes.count < first
false
else
relation_larger_than(sliced_nodes, @sliced_nodes_offset, first)
end
else
false
end
end
@has_next_page
end
def cursor_for(item)
load_nodes
# index in nodes + existing offset + 1 (because it's offset, not index)
offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) - (relation_offset(items) || 0)
encode(offset.to_s)
end
private
# @param relation [Object] A database query object
# @param _initial_offset [Integer] The number of items already excluded from the relation
# @param size [Integer] The value against which we check the relation size
# @return [Boolean] True if the number of items in this relation is larger than `size`
def relation_larger_than(relation, _initial_offset, size)
relation_count(set_limit(relation, size + 1)) == size + 1
end
# @param relation [Object] A database query object
View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment