mcp_authorization 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63bcf798beee003e44d7894f8e2e80177a6ddb1e7357b68507a74464c2665ce6
4
- data.tar.gz: 047560ce4c65b3b960f7c66904ca48c70b513e90510d7714edbd93996c39f919
3
+ metadata.gz: 7998105e5a6828b9673a8b97918503842f5eea467ee3ced5c8fd8298c1000f15
4
+ data.tar.gz: 4a130479e78bdd4ec696ee78623ff233331afebbd968c715eb9b87296163c94b
5
5
  SHA512:
6
- metadata.gz: 41b2c6a57ffeda77b7c74e2d2a849da6653d6678f78e9a76b9dec0cdaef5059b4ea00449063c57254e526c9cf6ebc780a7f43b59bfefeb57fabcc12e2f966842
7
- data.tar.gz: 7dc31d4854025087e495bc9963ee46079a35f3cc6cc6fcbd3a904b00383017e6f68b7718db51a49fb78585657d86158e637fcbd5a182a72b1ad6d09ec83f362e
6
+ metadata.gz: 02ae2178372da419ccbaffe5e7df67bc030e158cfd2b055cfa6682e5cf89d1daa428983c1687479e40de9fc466da01e9f5c2313fa561d5b815a48e884a95b4c0
7
+ data.tar.gz: 14ac5790e1e7f12d36c05331a8193744b2355fd30610416265f7d8f8e0aa730654ff2bdf4bf514d3c3b4d3f677e66916014719fdcda9dfc62b90a7dc4891402a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to this gem are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.2] - 2026-07-01
8
+
9
+ ### Fixed
10
+ - **A record field whose type is an inline string-literal union with a single field-level tag was misclassified as a per-member-tagged union.** `compile_tagged_record` routes a field into `compile_tagged_union` (which gates each `|`-separated member individually, e.g. `stage: a @feature(x) | b @feature(y)`) whenever `tagged_union_field?` sees an `@` anywhere in the type string plus more than one `|`-separated part. A plain literal union with one *field-level* tag trailing the whole thing — `logic: "AND" | "OR" @desc(...)` — matches that same heuristic even though the tag applies to the field, not an individual member. Misrouting sends each bare literal (`"AND"`, `"OR"`) through `resolve_type`, which only resolves *named alias references*; each literal fell back to `{type: "object"}`, producing `{type: "object", oneOf: [{type: "object"}, {type: "object"}]}` instead of `{type: "string", enum: ["AND", "OR"]}`. `tagged_union_field?` and `tagged_array_union_inner` now require at least one *non-final* `|`-separated member to carry a tag before treating a field as per-member-gated — every genuine per-member-tagged union in this codebase tags each gated member individually, so a tag trailing only the last member is never sufficient on its own. Field-level tags on inline literal unions now fall through to the normal RBS-library path (`visit_rbs_union`), which already resolved them correctly.
11
+
12
+ ## [0.6.1] - 2026-07-01
13
+
14
+ ### Fixed
15
+ - **Single-line string-literal-union type aliases (e.g. `type logic = "AND" | "OR"`) lost every member after the first.** Both the `# @rbs type` inline-comment parser and the shared `sig/shared/*.rbs` file parser only scanned *subsequent lines* for `| "value"` continuations, so a union written entirely on one line — the common shape for short enums like `"AND" | "OR"` — resolved to `{type: "string", enum: ["AND"]}` instead of `["AND", "OR"]`. Multi-line unions (`"low"\n| "medium"\n| "high"`) were unaffected. Literal unions written directly inline on a record field (not behind a named alias) were also unaffected, since those go through the RBS-parser union visitor rather than the alias collector.
16
+
7
17
  ## [0.6.0] - 2026-06-25
8
18
 
9
19
  Per-request work is now scoped to what each MCP method needs, and the
@@ -1094,10 +1094,19 @@ module McpAuthorization
1094
1094
  # True when a record field's type is a multi-member union that carries a
1095
1095
  # predicate tag — i.e. per-member gating is intended. A `|` at bracket
1096
1096
  # depth 0 marks a real union (not one inside a nested generic/record).
1097
+ #
1098
+ # A tag trailing only the *final* member (e.g. a plain literal union
1099
+ # with one field-level `@desc(...)`, as in `"AND" | "OR" @desc(...)`)
1100
+ # is NOT per-member gating — every genuine per-member-tagged union in
1101
+ # this codebase tags each gated member individually
1102
+ # (`a @feature(x) | b @feature(y)`), so at least one *non-final*
1103
+ # member must carry a tag before we route into compile_tagged_union.
1104
+ # Otherwise the field falls through to the normal RBS-library path,
1105
+ # which resolves inline literal unions correctly via visit_rbs_union.
1097
1106
  #: (String) -> bool
1098
1107
  def tagged_union_field?(type_str)
1099
1108
  return false unless type_str.include?("@")
1100
- split_at_depth_zero(type_str, "|").size > 1
1109
+ per_member_tagged_union?(type_str, "|")
1101
1110
  end
1102
1111
 
1103
1112
  # If a field type is +Array[<multi-member tagged union>]+, return the inner
@@ -1111,7 +1120,18 @@ module McpAuthorization
1111
1120
  return nil unless m
1112
1121
 
1113
1122
  inner = m[1].to_s
1114
- split_at_depth_zero(inner, "|").size > 1 ? inner : nil
1123
+ per_member_tagged_union?(inner, "|") ? inner : nil
1124
+ end
1125
+
1126
+ # True when a `|`-separated type expression has more than one member
1127
+ # AND at least one *non-final* member carries a predicate tag. See
1128
+ # tagged_union_field? for why the final member alone doesn't count.
1129
+ #: (String, String) -> bool
1130
+ def per_member_tagged_union?(type_str, delimiter)
1131
+ parts = split_at_depth_zero(type_str, delimiter)
1132
+ return false unless parts.size > 1
1133
+
1134
+ parts[0..-2].any? { |part| part.include?("@") }
1115
1135
  end
1116
1136
 
1117
1137
  # Compile a union-style output type (+# @rbs type output = success | admin_detail @requires(:admin)+)
@@ -1364,7 +1384,7 @@ module McpAuthorization
1364
1384
  current_name = $1.to_s
1365
1385
  current_base = nil
1366
1386
  current_body = "{"
1367
- elsif stripped =~ /\Atype (\w+) = "([^"]+)"/
1387
+ elsif stripped =~ /\Atype (\w+) = ("[^"]*"(?:\s*\|\s*"[^"]*")*)/
1368
1388
  aliases[$1.to_s] = parse_rbs_string_union($2.to_s, line, content)
1369
1389
  elsif current_name
1370
1390
  current_body << strip_rbs_comment(stripped)
@@ -1380,15 +1400,24 @@ module McpAuthorization
1380
1400
  aliases
1381
1401
  end
1382
1402
 
1383
- # Parse a multi-line string literal union from an .rbs file:
1403
+ # Parse a string literal union from an .rbs file, either written on
1404
+ # one line:
1405
+ # type logic = "AND" | "OR"
1406
+ # or continued across multiple lines:
1384
1407
  # type priority = "low"
1385
1408
  # | "medium"
1386
1409
  # | "high"
1387
1410
  #
1411
+ # +first_segment+ is everything captured after the +=+ on the
1412
+ # opening line, which may itself already contain the full
1413
+ # +"a" | "b" | "c"+ union — a single-line union has no continuation
1414
+ # lines, so capturing only its first quoted literal (the prior
1415
+ # behavior) silently dropped every member after the first.
1416
+ #
1388
1417
  # @return [Hash] +{type: "string", enum: ["low", "medium", "high"]}+
1389
1418
  #: (String, String, String) -> Hash[Symbol, untyped]
1390
- def parse_rbs_string_union(first_value, line, content)
1391
- values = [first_value]
1419
+ def parse_rbs_string_union(first_segment, line, content)
1420
+ values = first_segment.scan(/"([^"]*)"/).flatten
1392
1421
  content.each_line.drop_while { |l| l != line }.drop(1).each do |next_line|
1393
1422
  if next_line =~ /^\s*\|\s*"([^"]+)"/
1394
1423
  values << $1.to_s
@@ -1491,7 +1520,7 @@ module McpAuthorization
1491
1520
  current_name = nil
1492
1521
  current_body = +""
1493
1522
  end
1494
- elsif line =~ /#\s*@rbs type (\w+)\s*=\s*"([^"]+)"/
1523
+ elsif line =~ /#\s*@rbs type (\w+)\s*=\s*("[^"]*"(?:\s*\|\s*"[^"]*")*)/
1495
1524
  aliases[$1.to_s] = parse_string_union($2.to_s, line, content)
1496
1525
  elsif current_name
1497
1526
  stripped = strip_rbs_comment(line.strip.sub(/^#\s*/, ""))
@@ -1961,15 +1990,24 @@ module McpAuthorization
1961
1990
  str.count("{") == str.count("}")
1962
1991
  end
1963
1992
 
1964
- # Parse a multi-line string literal union from handler source comments:
1993
+ # Parse a string literal union from handler source comments, either
1994
+ # written on one line:
1995
+ # # @rbs type logic = "AND" | "OR"
1996
+ # or continued across multiple lines:
1965
1997
  # # @rbs type priority = "low"
1966
1998
  # # | "medium"
1967
1999
  # # | "high"
1968
2000
  #
2001
+ # +first_segment+ is everything captured after the +=+ on the
2002
+ # opening line, which may itself already contain the full
2003
+ # +"a" | "b" | "c"+ union — a single-line union has no continuation
2004
+ # lines, so capturing only its first quoted literal (the prior
2005
+ # behavior) silently dropped every member after the first.
2006
+ #
1969
2007
  # @return [Hash] +{type: "string", enum: ["low", "medium", "high"]}+
1970
2008
  #: (String, String, String) -> Hash[Symbol, untyped]
1971
- def parse_string_union(first_value, line, content)
1972
- values = [first_value]
2009
+ def parse_string_union(first_segment, line, content)
2010
+ values = first_segment.scan(/"([^"]*)"/).flatten
1973
2011
  content.each_line.drop_while { |l| l != line }.drop(1).each do |next_line|
1974
2012
  if next_line =~ /^\s*#\s*\|\s*"([^"]+)"/
1975
2013
  values << $1.to_s
@@ -1,3 +1,3 @@
1
1
  module McpAuthorization
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp_authorization
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - AndyGauge
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-25 00:00:00.000000000 Z
11
+ date: 2026-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails