Skip to content

Commit 971bfef

Browse files
authored
Merge pull request #25 from mortenpi/mp/state-diagram
Convert state machine diagram to Mermaid
2 parents 21e05a3 + c372700 commit 971bfef

File tree

6 files changed

+377
-24
lines changed

6 files changed

+377
-24
lines changed

bin/Manifest.toml

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# This file is machine-generated - editing it directly is not advised
2+
3+
julia_version = "1.9.0"
4+
manifest_format = "2.0"
5+
project_hash = "c6c7650f18722c9cc0afa199ecf6775e5482794a"
6+
7+
[[deps.ArgTools]]
8+
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
9+
version = "1.1.1"
10+
11+
[[deps.Artifacts]]
12+
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
13+
14+
[[deps.Base64]]
15+
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
16+
17+
[[deps.Dates]]
18+
deps = ["Printf"]
19+
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
20+
21+
[[deps.Downloads]]
22+
deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"]
23+
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
24+
version = "1.6.0"
25+
26+
[[deps.FileWatching]]
27+
uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
28+
29+
[[deps.InteractiveUtils]]
30+
deps = ["Markdown"]
31+
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
32+
33+
[[deps.JSON]]
34+
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
35+
git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a"
36+
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
37+
version = "0.21.4"
38+
39+
[[deps.LibCURL]]
40+
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
41+
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
42+
version = "0.6.3"
43+
44+
[[deps.LibCURL_jll]]
45+
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
46+
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
47+
version = "7.84.0+0"
48+
49+
[[deps.LibGit2]]
50+
deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
51+
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
52+
53+
[[deps.LibSSH2_jll]]
54+
deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
55+
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
56+
version = "1.10.2+0"
57+
58+
[[deps.Libdl]]
59+
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
60+
61+
[[deps.Logging]]
62+
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
63+
64+
[[deps.Markdown]]
65+
deps = ["Base64"]
66+
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
67+
68+
[[deps.MbedTLS_jll]]
69+
deps = ["Artifacts", "Libdl"]
70+
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
71+
version = "2.28.2+0"
72+
73+
[[deps.Mmap]]
74+
uuid = "a63ad114-7e13-5084-954f-fe012c677804"
75+
76+
[[deps.MozillaCACerts_jll]]
77+
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
78+
version = "2022.10.11"
79+
80+
[[deps.NetworkOptions]]
81+
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
82+
version = "1.2.0"
83+
84+
[[deps.Parsers]]
85+
deps = ["Dates", "PrecompileTools", "UUIDs"]
86+
git-tree-sha1 = "a5aef8d4a6e8d81f171b2bd4be5265b01384c74c"
87+
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
88+
version = "2.5.10"
89+
90+
[[deps.Pkg]]
91+
deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
92+
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
93+
version = "1.9.0"
94+
95+
[[deps.PkgAuthentication]]
96+
deps = ["Downloads", "JSON", "Pkg", "Random", "TOML", "Test"]
97+
path = ".."
98+
uuid = "4722fa14-9d28-45f9-a1e2-a38605bd88f0"
99+
version = "2.0.0"
100+
101+
[[deps.PrecompileTools]]
102+
deps = ["Preferences"]
103+
git-tree-sha1 = "259e206946c293698122f63e2b513a7c99a244e8"
104+
uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
105+
version = "1.1.1"
106+
107+
[[deps.Preferences]]
108+
deps = ["TOML"]
109+
git-tree-sha1 = "7eb1686b4f04b82f96ed7a4ea5890a4f0c7a09f1"
110+
uuid = "21216c6a-2e73-6563-6e65-726566657250"
111+
version = "1.4.0"
112+
113+
[[deps.Printf]]
114+
deps = ["Unicode"]
115+
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
116+
117+
[[deps.REPL]]
118+
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
119+
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
120+
121+
[[deps.Random]]
122+
deps = ["SHA", "Serialization"]
123+
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
124+
125+
[[deps.SHA]]
126+
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
127+
version = "0.7.0"
128+
129+
[[deps.Serialization]]
130+
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
131+
132+
[[deps.Sockets]]
133+
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
134+
135+
[[deps.TOML]]
136+
deps = ["Dates"]
137+
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
138+
version = "1.0.3"
139+
140+
[[deps.Tar]]
141+
deps = ["ArgTools", "SHA"]
142+
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
143+
version = "1.10.0"
144+
145+
[[deps.Test]]
146+
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
147+
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
148+
149+
[[deps.TextWrap]]
150+
git-tree-sha1 = "9250ef9b01b66667380cf3275b3f7488d0e25faf"
151+
uuid = "b718987f-49a8-5099-9789-dcd902bef87d"
152+
version = "1.0.1"
153+
154+
[[deps.UUIDs]]
155+
deps = ["Random", "SHA"]
156+
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
157+
158+
[[deps.Unicode]]
159+
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
160+
161+
[[deps.Zlib_jll]]
162+
deps = ["Libdl"]
163+
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
164+
version = "1.2.13+0"
165+
166+
[[deps.nghttp2_jll]]
167+
deps = ["Artifacts", "Libdl"]
168+
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
169+
version = "1.48.0+0"
170+
171+
[[deps.p7zip_jll]]
172+
deps = ["Artifacts", "Libdl"]
173+
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
174+
version = "17.4.0+0"

bin/Project.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[deps]
2+
PkgAuthentication = "4722fa14-9d28-45f9-a1e2-a38605bd88f0"
3+
TextWrap = "b718987f-49a8-5099-9789-dcd902bef87d"

bin/structure.jl

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,87 @@
1-
using LightGraphs, PkgAuthentication, GraphPlot, Cairo, Compose
1+
# This script generates the `docs/internals.md` file, that mainly contains the
2+
# state machine diagram that we can automatically generate from the code.
3+
import PkgAuthentication
4+
import InteractiveUtils, Markdown, TextWrap
25

3-
const bin_dir = @__DIR__
4-
const root_dir = dirname(bin_dir)
5-
const src_dir = joinpath(root_dir, "src")
6-
const docs_dir = joinpath(root_dir, "docs")
7-
const docs_assets_dir = joinpath(docs_dir, "assets")
6+
# Rather than generating the file directly, we'll write the output to a buffer
7+
# first, so that we wouldn't end up with a partial file if there is some error.
8+
buffer = let buffer = IOBuffer(write=true)
9+
write(buffer, """
10+
# Internal implementation notes
811
9-
file = joinpath(src_dir, "PkgAuthentication.jl")
12+
The authentication control flow is implemented as the following state machine, starting from the `NeedAuthentication` state (or `NoAuthentication` if `force=true` is passed to `authenticate`), and finishing in either `Success` or `Failure`.
1013
11-
g = SimpleDiGraph()
12-
lines = readlines(file)
14+
```mermaid
15+
---
16+
title: PkgAuthentication state machine diagram
17+
---
1318
14-
vertices = string.(nameof.(subtypes(PkgAuthentication.State)))
15-
for vertex in vertices
16-
add_vertex!(g)
17-
end
19+
stateDiagram-v2
20+
direction LR
21+
22+
[*] --> NeedAuthentication
23+
[*] --> NoAuthentication
24+
""")
1825

19-
for line in lines
20-
m = match(r"^function step\(state::(.+?)\)::Union{(.+?)}$", line)
21-
if m !== nothing
22-
for target in strip.(split(m[2], ','))
23-
add_edge!(g, findfirst(==(m[1]), vertices), findfirst(==(target), vertices))
26+
all_targets = Dict{String,Vector{String}}()
27+
ignore_errors = (
28+
PkgAuthentication.Failure, PkgAuthentication.Success
29+
)
30+
for line in readlines(pathof(PkgAuthentication))
31+
m = match(r"^function step\(state::(.+?)\)::Union{(.+?)}$", line)
32+
if m !== nothing
33+
all_targets[m[1]] = strip.(split(m[2], ','))
34+
end
35+
end
36+
for state in sort(InteractiveUtils.subtypes(PkgAuthentication.State), by=string)
37+
println(buffer)
38+
state_str = string(nameof(state))
39+
# Generate the connecting arrows between the states
40+
targets = get(all_targets, state_str, String[])
41+
if isempty(targets) && (state ignore_errors)
42+
@warn "Empty targets list for $state"
43+
elseif !isempty(targets)
44+
for target in targets
45+
println(buffer, " $(state_str) --> $(target)")
46+
end
47+
end
48+
# Extract the docstring and put it into a mermaid note
49+
try
50+
docstr::Markdown.MD = Base.Docs.doc(state)
51+
docstr_text = docstr.meta[:results][1].text[1]
52+
println(buffer, " note right of $(state_str)")
53+
TextWrap.print_wrapped(
54+
buffer, docstr_text, width=65,
55+
initial_indent = 8, subsequent_indent = 8,
56+
)
57+
println(buffer)
58+
println(buffer, " end note")
59+
catch e
60+
if state ignore_errors
61+
@error "Invalid docstring for $state" exception = (e, catch_backtrace())
62+
end
2463
end
2564
end
65+
66+
write(buffer, """
67+
Success --> [*]
68+
Failure --> [*]
69+
```
70+
71+
> **Note** This file is automatically generated by the `bin/structure.jl` script.
72+
""")
73+
74+
take!(buffer)
75+
end
76+
77+
# Actually write the diagram to file now that we have successfully managed
78+
# to fully generate it.
79+
let docs_dir = joinpath(dirname(@__DIR__), "docs")
80+
if !isdir(docs_dir)
81+
ispath(docs_dir) && error("$docs_dir exists, but is not a directory")
82+
mkpath(docs_dir)
83+
end
84+
internals_md = joinpath(docs_dir, "internals.md")
85+
isfile(internals_md) && @warn "Overwriting: $(internals_md)"
86+
write(internals_md, buffer)
2687
end
27-
plot = gplot(g, nodelabel=vertices, linetype="curve")
28-
draw(PNG(joinpath(docs_assets_dir, "structure.png"), 16cm, 16cm), plot)

docs/assets/structure.png

-69.4 KB
Binary file not shown.

docs/internals.md

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,89 @@
1-
# Internals
1+
# Internal implementation notes
22

3-
## Implementation
3+
The authentication control flow is implemented as the following state machine, starting from the `NeedAuthentication` state (or `NoAuthentication` if `force=true` is passed to `authenticate`), and finishing in either `Success` or `Failure`.
44

5-
Authentication is implemented with the following state machine:
5+
```mermaid
6+
---
7+
title: PkgAuthentication state machine diagram
8+
---
69
7-
![State machine](assets/structure.png)
10+
stateDiagram-v2
11+
direction LR
12+
13+
[*] --> NeedAuthentication
14+
[*] --> NoAuthentication
15+
16+
ClaimToken --> ClaimToken
17+
ClaimToken --> HasNewToken
18+
ClaimToken --> Failure
19+
note right of ClaimToken
20+
Starts polling the Pkg server's OAuth token claiming
21+
endpoint, returning to ClaimToken while the polling is
22+
happening. Proceeds to HasNewToken if it successfully
23+
acquires a token, or to Failure if the polling times out,
24+
or there is an unexpected error.
25+
end note
26+
27+
28+
HasNewToken --> HasNewToken
29+
HasNewToken --> Success
30+
HasNewToken --> Failure
31+
note right of HasNewToken
32+
Takes the token from the previous step and writes it to
33+
the auth.toml file. In order to handle potential race
34+
conditions with other writes, it will check that the
35+
write was successful, and will try again if it fails. If
36+
the write was successful, it proceeds to Success, or
37+
retries HasNewToken if it was not. May proceed to Failure
38+
if there is an unexpected failure.
39+
end note
40+
41+
HasToken --> NeedRefresh
42+
HasToken --> Success
43+
note right of HasToken
44+
If the token is valid (i.e. not expired, based on the
45+
expiry times in the auth.toml file), proceeds to Success.
46+
Otherwise, proceeds to NeedRefresh.
47+
end note
48+
49+
NeedAuthentication --> HasToken
50+
NeedAuthentication --> NoAuthentication
51+
note right of NeedAuthentication
52+
Checks if a syntactically valid auth.toml token file
53+
exists for the requested server (but does not check
54+
whether it has expired or not). Proceeds to HasToken if
55+
it exists, or NoAuthentication if not.
56+
end note
57+
58+
NeedRefresh --> HasNewToken
59+
NeedRefresh --> NoAuthentication
60+
note right of NeedRefresh
61+
Attempts to acquire a new access token by using the
62+
refresh token in the auth.toml. If the refresh succeeds,
63+
it will proceed to HasNewToken, or to NoAuthentication if
64+
it fails.
65+
end note
66+
67+
NoAuthentication --> RequestLogin
68+
NoAuthentication --> Failure
69+
note right of NoAuthentication
70+
Attempts to acquire an OAuth challenge from the Pkg
71+
server. If successful, proceeds to RequestLogin, or to
72+
Failure otherwise.
73+
end note
74+
75+
RequestLogin --> ClaimToken
76+
RequestLogin --> Failure
77+
note right of RequestLogin
78+
Presents the in-browser step of the OAuth authentication
79+
process to the user (e.g. by opening the Pkg server's
80+
login page in the user's browser). Proceeds to ClaimToken
81+
immediately, or to Failure if there was an unexpected
82+
failure.
83+
end note
84+
85+
Success --> [*]
86+
Failure --> [*]
87+
```
88+
89+
> **Note** This file is automatically generated by the `bin/structure.jl` script.

0 commit comments

Comments
 (0)