Initial commit
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/all-checks-complete Pipeline was successful

This commit is contained in:
Smaug123
2023-10-11 00:17:48 +01:00
commit 42eb1f7726
32 changed files with 1911 additions and 0 deletions

12
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fantomas": {
"version": "6.2.0",
"commands": [
"fantomas"
]
}
}
}

41
.editorconfig Normal file
View File

@@ -0,0 +1,41 @@
root=true
[*]
charset=utf-8
end_of_line=crlf
trim_trailing_whitespace=true
insert_final_newline=true
indent_style=space
indent_size=4
# ReSharper properties
resharper_xml_indent_size=2
resharper_xml_max_line_length=100
resharper_xml_tab_width=2
[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
indent_style=space
indent_size=2
[*.{fs,fsi}]
fsharp_bar_before_discriminated_union_declaration=true
fsharp_space_before_uppercase_invocation=true
fsharp_space_before_class_constructor=true
fsharp_space_before_member=true
fsharp_space_before_colon=true
fsharp_space_before_semicolon=true
fsharp_multiline_bracket_style=aligned
fsharp_newline_between_type_definition_and_members=true
fsharp_align_function_signature_to_indentation=true
fsharp_alternative_long_member_definitions=true
fsharp_multi_line_lambda_closing_newline=true
fsharp_experimental_keep_indent_in_branch=true
fsharp_max_value_binding_width=80
fsharp_max_record_width=0
max_line_length=120
end_of_line=lf
[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
indent_style=space
indent_size=2
tab_width=2

4
.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
* eol=auto
*.sh text eol=lf
*.nix text eol=lf
hooks/pre-push text eol=lf

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea/
*.user
*.DotSettings
.DS_Store
result
.profile*
node_modules/
package.json
package-lock.json

View File

@@ -0,0 +1,10 @@
steps:
echo:
image: alpine
commands:
- echo "All required checks complete"
depends_on:
- build
skip_clone: true

22
.woodpecker/.build.yml Normal file
View File

@@ -0,0 +1,22 @@
steps:
build:
image: nixos/nix
commands:
- echo 'experimental-features = flakes nix-command' >> /etc/nix/nix.conf
# Lint
- "nix flake check"
# Test
- nix build
- nix run . -- --help
- nix run . -- auth --help
- nix run . -- lookup-gym --help
- nix run . -- fullness --help
- nix run . -- activity --help
- nix develop --command markdown-link-check README.md
- nix develop --command dotnet test
- nix develop --command dotnet test --configuration Release
when:
- event: "push"
evaluate: 'CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH'
- event: "pull_request"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Patrick Stevens
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
PureGym.App/ArgsCrate.fs Normal file
View File

@@ -0,0 +1,21 @@
namespace PureGym.App
open System.Threading.Tasks
open Argu
type ArgsEvaluator<'ret> =
abstract Eval<'a, 'b when 'b :> IArgParserTemplate> :
(ParseResults<'b> -> Result<'a, ArguParseException>) -> ('a -> Task<int>) -> 'ret
type ArgsCrate =
abstract Apply<'ret> : ArgsEvaluator<'ret> -> 'ret
[<RequireQualifiedAccess>]
module ArgsCrate =
let make<'a, 'b when 'b :> IArgParserTemplate>
(ofResult : ParseResults<'b> -> Result<'a, ArguParseException>)
(run : 'a -> Task<int>)
=
{ new ArgsCrate with
member _.Apply e = e.Eval ofResult run
}

41
PureGym.App/AuthArg.fs Normal file
View File

@@ -0,0 +1,41 @@
namespace PureGym.App
open Argu
open PureGym
type AuthArg =
| [<Unique ; CustomAppSettings "PUREGYM_BEARER_TOKEN">] Bearer_Token of string
| [<Unique>] User_Email of string
| [<Unique>] Pin of string
| [<GatherUnrecognized>] Others of string
interface IArgParserTemplate with
member s.Usage =
match s with
| AuthArg.Bearer_Token _ -> "A bearer token for the PureGym API"
| AuthArg.User_Email _ -> "PureGym user's email address"
| AuthArg.Pin _ -> "Eight-digit PureGym user's PIN"
| AuthArg.Others _ -> "<specific args for command>"
static member Parse (args : ParseResults<AuthArg>) : Result<Auth * string[], ArguParseException> =
let unmatchedArgs = args.GetResults AuthArg.Others |> List.toArray
match
args.TryGetResult AuthArg.User_Email, args.TryGetResult AuthArg.Pin, args.TryGetResult AuthArg.Bearer_Token
with
| Some email, Some pin, _ ->
let auth =
Auth.User
{
Username = email
Pin = pin
}
Ok (auth, unmatchedArgs)
| Some _email, None, _ -> failwith "Supplied --user-email but no --pin; either both or neither are required."
| None, Some _pin, _ -> failwith "Supplied --pin but no --user-email; either both or neither are required."
| None, None, None ->
failwith "No creds given: expected at least one of `--bearer-token` or `--user-email --pin`"
| None, None, Some token ->
let auth = Auth.Token (AuthToken.ofBearerToken token)
Ok (auth, unmatchedArgs)

View File

@@ -0,0 +1,40 @@
namespace PureGym.App
open Argu
open System
open PureGym
type GetTokenArg =
| [<ExactlyOnce>] User_Email of string
| [<ExactlyOnce>] Pin of string
interface IArgParserTemplate with
member s.Usage =
match s with
| GetTokenArg.Pin _ -> "Eight-digit PureGym user's PIN"
| GetTokenArg.User_Email _ -> "PureGym user's email address"
static member Parse (args : ParseResults<GetTokenArg>) : Result<UsernamePin, ArguParseException> =
try
{
Username = args.GetResult GetTokenArg.User_Email
Pin = args.GetResult GetTokenArg.Pin
}
|> Ok
with :? ArguParseException as e ->
Error e
[<RequireQualifiedAccess>]
module Authenticate =
let run (creds : UsernamePin) =
task {
let! cred = AuthToken.get creds
Console.WriteLine cred.AccessToken
match cred.ExpiryTime with
| None -> ()
| Some expiry -> Console.Error.WriteLine $"Expires at {expiry}"
return 0
}

11
PureGym.App/Exception.fs Normal file
View File

@@ -0,0 +1,11 @@
namespace PureGym.App
open System.Runtime.ExceptionServices
[<RequireQualifiedAccess>]
module Exception =
let reraiseWithOriginalStackTrace<'a> (e : exn) : 'a =
let edi = ExceptionDispatchInfo.Capture e
edi.Throw ()
failwith "unreachable"

61
PureGym.App/Fullness.fs Normal file
View File

@@ -0,0 +1,61 @@
namespace PureGym.App
open Argu
open PureGym
type FullnessArgsFragment =
| [<Unique>] Gym_Id of int
| [<Unique>] Gym_Name of string
| [<Unique>] Terse of bool
interface IArgParserTemplate with
member s.Usage =
match s with
| FullnessArgsFragment.Gym_Id _ -> "ID of the gym to look up, according to PureGym's internal mapping"
| FullnessArgsFragment.Gym_Name _ -> "Name of a gym (best-guess fuzzy matching)"
| FullnessArgsFragment.Terse _ -> "If true, output only the single number 'how many people are in the gym'."
type FullnessArgs =
{
Creds : Auth
Gym : GymSelector
Terse : bool
}
static member Parse
(auth : Auth)
(args : FullnessArgsFragment ParseResults)
: Result<FullnessArgs, ArguParseException>
=
let gym =
match args.TryGetResult FullnessArgsFragment.Gym_Id, args.TryGetResult FullnessArgsFragment.Gym_Name with
| Some _, Some _ -> failwith "--gym-id and --gym-name are mutually exclusive"
| None, None ->
System.Console.Error.WriteLine "No gym ID given and no gym named; assuming the user's home gym"
GymSelector.Home
| Some id, None -> GymSelector.Id id
| None, Some name -> GymSelector.Name name
{
Creds = auth
Gym = gym
Terse = args.TryGetResult FullnessArgsFragment.Terse |> Option.defaultValue false
}
|> Ok
[<RequireQualifiedAccess>]
module Fullness =
let run (args : FullnessArgs) =
task {
let! client = Api.make args.Creds
let! id = GymSelector.canonicalId client args.Gym
let! attendance = client.GetGymAttendance id
if args.Terse then
System.Console.WriteLine attendance.TotalPeopleInGym
else
System.Console.WriteLine (string<GymAttendance> attendance)
return 0
}

51
PureGym.App/LookupGym.fs Normal file
View File

@@ -0,0 +1,51 @@
namespace PureGym.App
open Argu
open PureGym
type LookupGymArgsFragment =
| [<Unique>] Gym_Id of int
| [<Unique>] Gym_Name of string
interface IArgParserTemplate with
member s.Usage =
match s with
| LookupGymArgsFragment.Gym_Id _ -> "ID of the gym to look up, according to PureGym's internal mapping"
| LookupGymArgsFragment.Gym_Name _ -> "Name of a gym (best-guess fuzzy matching)"
type LookupGymArgs =
{
Creds : Auth
Gym : GymSelector
}
static member Parse
(auth : Auth)
(args : LookupGymArgsFragment ParseResults)
: Result<LookupGymArgs, ArguParseException>
=
let gym =
match args.TryGetResult LookupGymArgsFragment.Gym_Id, args.TryGetResult LookupGymArgsFragment.Gym_Name with
| Some _, Some _ -> failwith "--gym-id and --gym-name are mutually exclusive"
| None, None ->
System.Console.Error.WriteLine "No gym ID given and no gym named; assuming the user's home gym"
GymSelector.Home
| Some id, None -> GymSelector.Id id
| None, Some name -> GymSelector.Name name
{
Creds = auth
Gym = gym
}
|> Ok
[<RequireQualifiedAccess>]
module LookupGym =
let run (args : LookupGymArgs) =
task {
let! client = Api.make args.Creds
let! s = client.GetGym 19
System.Console.WriteLine (string<Gym> s)
return 0
}

View File

@@ -0,0 +1,38 @@
namespace PureGym.App
open Argu
open PureGym
type MemberActivityArgsFragment =
| MemberActivityArgsFragment of bool
interface IArgParserTemplate with
member s.Usage =
match s with
| MemberActivityArgsFragment _ -> "dummy argument: this subcommand has no args"
type MemberActivityArgs =
{
Creds : Auth
}
static member Parse
(auth : Auth)
(_ : MemberActivityArgsFragment ParseResults)
: Result<MemberActivityArgs, ArguParseException>
=
{
Creds = auth
}
|> Ok
[<RequireQualifiedAccess>]
module MemberActivity =
let run (args : MemberActivityArgs) =
task {
let! client = Api.make args.Creds
let! activity = client.GetMemberActivity ()
System.Console.WriteLine (string<MemberActivity> activity)
return 0
}

118
PureGym.App/Program.fs Normal file
View File

@@ -0,0 +1,118 @@
namespace PureGym.App
open System
open Argu
type Subcommand =
| RequiresAuth of (PureGym.Auth -> ArgsCrate)
| NoAuth of ArgsCrate
module Program =
let subcommands =
[|
"auth", ("Get an authentication token", NoAuth (ArgsCrate.make GetTokenArg.Parse Authenticate.run))
"lookup-gym",
("Get information about the physical instantiation of a gym",
RequiresAuth (fun auth -> ArgsCrate.make (LookupGymArgs.Parse auth) LookupGym.run))
"fullness",
("Determine how full a gym is",
RequiresAuth (fun auth -> ArgsCrate.make (FullnessArgs.Parse auth) Fullness.run))
"activity",
("Get information about your gym usage",
RequiresAuth (fun auth -> ArgsCrate.make (MemberActivityArgs.Parse auth) MemberActivity.run))
|]
|> Map.ofArray
[<EntryPoint>]
let main argv =
// It looks like Argu doesn't really support the combination of subcommands and read-from-env-vars, so we just
// roll our own.
match Array.tryHead argv with
| None
| Some "--help" ->
subcommands.Keys
|> String.concat ","
|> eprintfn "Subcommands (try each with `--help`): %s"
0
| Some commandName ->
match Map.tryFind commandName subcommands with
| None ->
subcommands.Keys
|> String.concat ","
|> eprintfn "Unrecognised command '%s'. Subcommands (try each with `--help`): %s" commandName
127
| Some (_help, command) ->
let argv = Array.tail argv
let config = ConfigurationReader.FromEnvironmentVariables ()
let argv, command =
match command with
| RequiresAuth command ->
let authParser = ArgumentParser.Create<AuthArg> ()
let extractedAuthParse, helpRequested =
try
authParser.Parse (argv, config, raiseOnUsage = true, ignoreUnrecognized = true), false
with :? ArguParseException as e ->
if e.Message.StartsWith ("USAGE:", StringComparison.OrdinalIgnoreCase) then
authParser.Parse (argv, config, raiseOnUsage = false, ignoreUnrecognized = true), true
else
reraise ()
let authArgs, argv =
if helpRequested then
let subcommandArgs =
("--help" :: extractedAuthParse.GetResults AuthArg.Others) |> List.toArray
Unchecked.defaultof<_>, subcommandArgs
else
match AuthArg.Parse extractedAuthParse with
| Ok a -> a
| Error e -> Exception.reraiseWithOriginalStackTrace e
argv, command authArgs
| NoAuth command -> argv, command
{ new ArgsEvaluator<_> with
member _.Eval<'a, 'b when 'b :> IArgParserTemplate> (ofResult : ParseResults<'b> -> Result<'a, _>) run =
let parser = ArgumentParser.Create<'b> ()
let parsed =
try
parser.Parse (argv, config, raiseOnUsage = true) |> Ok
with :? ArguParseException as e ->
e.Message.Replace ("PureGym.App ", $"PureGym.App %s{commandName} ")
|> Console.Error.WriteLine
if e.Message.StartsWith ("USAGE:", StringComparison.OrdinalIgnoreCase) then
Error true
else
Error false
match parsed with
| Error false -> Error 127
| Error true -> Error 0
| Ok parsed ->
match ofResult parsed with
| Error e ->
e.Message.Replace ("PureGym.App ", $"PureGym.App %s{commandName} ")
|> Console.Error.WriteLine
Error 127
| Ok args ->
run args |> Ok
}
|> command.Apply
|> Result.cata (fun t -> t.Result) id

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Result.fs" />
<Compile Include="Exception.fs" />
<Compile Include="ArgsCrate.fs" />
<Compile Include="AuthArg.fs" />
<Compile Include="Authenticate.fs" />
<Compile Include="Fullness.fs" />
<Compile Include="LookupGym.fs" />
<Compile Include="MemberActivity.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PureGym\PureGym.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Argu" Version="6.1.1" />
</ItemGroup>
</Project>

9
PureGym.App/Result.fs Normal file
View File

@@ -0,0 +1,9 @@
namespace PureGym.App
[<RequireQualifiedAccess>]
module Result =
let cata<'ok, 'err, 'result> onOk onError (r : Result<'ok, 'err>) : 'result =
match r with
| Ok ok -> onOk ok
| Error e -> onError e

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Compile Include="TestSurface.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ApiSurface" Version="4.0.12" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2"/>
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
<PackageReference Include="coverlet.collector" Version="3.2.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PureGym\PureGym.fsproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
namespace PureGym.Test
open PureGym
open NUnit.Framework
open ApiSurface
[<TestFixture>]
module TestSurface =
let assembly = typeof<Gym>.Assembly
[<Test>]
let ``Ensure API surface has not been modified`` () = ApiSurface.assertIdentical assembly
[<Test ; Explicit>]
let ``Update API surface`` () =
ApiSurface.writeAssemblyBaseline assembly
[<Test ; Explicit "This isn't done yet">]
let ``Ensure public API is fully documented`` () =
DocCoverage.assertFullyDocumented assembly

28
PureGym.sln Normal file
View File

@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PureGym", "PureGym\PureGym.fsproj", "{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PureGym.App", "PureGym.App\PureGym.App.fsproj", "{B4848668-4CB2-4919-987D-8C95F865C81E}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PureGym.Test", "PureGym.Test\PureGym.Test.fsproj", "{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}.Release|Any CPU.Build.0 = Release|Any CPU
{B4848668-4CB2-4919-987D-8C95F865C81E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4848668-4CB2-4919-987D-8C95F865C81E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4848668-4CB2-4919-987D-8C95F865C81E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4848668-4CB2-4919-987D-8C95F865C81E}.Release|Any CPU.Build.0 = Release|Any CPU
{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

272
PureGym/Api.fs Normal file
View File

@@ -0,0 +1,272 @@
namespace PureGym
open System
open System.Net.Http
open System.Text.Json.Serialization
open System.Threading.Tasks
open RestEase
/// Describes the opening hours of a given gym.
type GymOpeningHours =
{
/// If this is true, there should be no OpeningHours (but nothing enforces that).
IsAlwaysOpen : bool
/// This is a pretty unstructured list, which is in general not really parseable: it's human-readable only.
OpeningHours : string list
}
/// Human-readable representation
override this.ToString () =
if this.IsAlwaysOpen then
"always open"
else
this.OpeningHours |> String.concat ", "
/// How a human can authenticate with a gym when they physically try to enter it
type GymAccessOptions =
{
/// This gym has PIN entry pads
PinAccess : bool
/// This gym has a QR code scanner. QR codes can be generated with the PureGym app.
QrCodeAccess : bool
}
/// Human-readable representation
override this.ToString () =
$"Pin access: %c{Char.emoji this.PinAccess}; QR code access: %c{Char.emoji this.QrCodeAccess}"
/// Where a gym is on the Earth
type GymLocation =
{
/// Measured in degrees
Longitude : float
/// Measured in degrees
Latitude : float
}
/// The postal address of a gym
type GymAddress =
{
/// E.g. "Canterbury Court"
[<JsonRequired>]
AddressLine1 : string
/// E.g. "Units 4, 4A, 5 And 5A"
AddressLine2 : string
/// E.g. "Kennington Park"
AddressLine3 : string
/// E.g. "LONDON"
[<JsonRequired>]
Town : string
County : string
/// E.g. "SW9 6DE"
[<JsonRequired>]
Postcode : string
}
/// Human-readable statement of the address
override this.ToString () =
[
yield Some this.AddressLine1
yield this.AddressLine2 |> Option.ofObj
yield this.AddressLine3 |> Option.ofObj
match this.County with
| null -> yield Some $"%s{this.Town} %s{this.Postcode}"
| county ->
yield Some this.Town
yield Some $"%s{county} %s{this.Postcode}"
]
|> Seq.choose id
|> String.concat "\n"
/// Metadata about a physical gym
type Gym =
{
// The following fields are returned but are always null
// ReasonsToJoin : string
// VirtualTourUrl : Uri
// PersonalTrainersUrl : Uri
// WebViewUrl : Uri
// FloorPlanUrl : Uri
// StaffMembers : string
/// The name of this gym, e.g. "London Oval"
[<JsonRequired>]
Name : string
/// This gym's ID in the PureGym system, e.g. 19
[<JsonRequired>]
Id : int
/// I don't know what this status is. Please tell me if you know!
[<JsonRequired>]
Status : int
/// Postal address of this gym
[<JsonRequired>]
Address : GymAddress
/// Phone number of this gym, e.g. "+44 1234 567890"
[<JsonRequired>]
PhoneNumber : string
/// Contact email address for this gym's staff
[<JsonRequired>]
EmailAddress : string
/// When this gym is open
[<JsonRequired>]
GymOpeningHours : GymOpeningHours
/// How a human can physically authenticate when they physically enter this gym
[<JsonRequired>]
AccessOptions : GymAccessOptions
/// Where this gym is physically located
[<JsonRequired>]
Location : GymLocation
/// The IANA time zone this gym observes, e.g. "Europe/London"
[<JsonRequired>]
TimeZone : string
/// This is a date-time in the format yyyy-MM-ddTHH:mm:ss+01 Europe/London
ReopenDate : string
}
/// Human-readable representation of the most important information about this gym
override this.ToString () =
$"""%s{this.Name} (%i{this.Id})
{this.Address}
%s{this.EmailAddress} %s{this.PhoneNumber}
Opening hours: %s{string<GymOpeningHours> this.GymOpeningHours}
%s{string<GymAccessOptions> this.AccessOptions}
"""
/// A human member of PureGym
type Member =
{
/// This member's ID. This is a fairly large number.
Id : int
/// No idea what this is - please tell me if you know!
CompoundMemberId : string
/// First name, e.g. "Patrick"
FirstName : string
/// Last name, e.g. "Stevens"
LastName : string
/// ID of the gym designated as this user's home gym. This is also the "Id" field of the appropriate Gym object.
HomeGymId : int
/// The name of the gym designated as this user's home gym. This is also the "Name" field of the appropriate
/// Gym object.
HomeGymName : string
/// This user's email address
EmailAddress : string
/// This user's gym access pin, probably 8 digits
GymAccessPin : string
/// This user's recorded date of birth
DateOfBirth : DateOnly
/// This user's phone number, human-readable
MobileNumber : string
/// This user's registered home postcode
Postcode : string
/// E.g. "Corporate"
MembershipName : string
MembershipLevel : int
SuspendedReason : int
MemberStatus : int
}
/// Statistics for how many people are currently at a gym
type GymAttendance =
{
[<JsonRequired>]
Description : string
/// How many people are in the gym as of this statistics snapshot
[<JsonRequired>]
TotalPeopleInGym : int
/// How many people are in classes at the gym as of this statistics snapshot
[<JsonRequired>]
TotalPeopleInClasses : int
TotalPeopleSuffix : string
[<JsonRequired>]
IsApproximate : bool
/// When the query was received (I think)
AttendanceTime : DateTime
/// When the "total people in gym" snapshot was taken that is reported here
LastRefreshed : DateTime
/// When the "number of people in classes" snapshot was taken that is reported here
LastRefreshedPeopleInClasses : DateTime
/// Maximum capacity of the gym, or 0 if no listed capacity
MaximumCapacity : int
}
/// Human-readable representation
override this.ToString () =
if not (Object.ReferenceEquals (this.TotalPeopleSuffix, null)) then
failwith $"Unexpectedly got Total People Suffix: %s{this.TotalPeopleSuffix}"
let capacity =
if this.MaximumCapacity = 0 then
""
else
$" out of %i{this.MaximumCapacity} maximum"
let classes =
if this.TotalPeopleInClasses = 0 then
""
else
$"\n%i{this.TotalPeopleInClasses} in classes"
$"""%i{this.TotalPeopleInGym} in gym%s{capacity} (is exact: %c{Char.emoji (not this.IsApproximate)})%s{classes}
Query made at %s{this.AttendanceTime.ToString ("s")}%s{this.AttendanceTime.ToString ("zzz")}
Snapshot correct as of %s{this.LastRefreshed.ToString ("s")}%s{this.LastRefreshed.ToString ("zzz")}
Classes info correct as of %s{this.LastRefreshedPeopleInClasses.ToString ("s")}%s{this.LastRefreshedPeopleInClasses.ToString ("zzz")}"""
/// The visit statistics for a particular human to a particular gym.
/// The semantics of this class are basically unknown.
type MemberActivity =
{
/// ??? semantics unknown; this was 2852 for me
[<JsonPropertyName "totalDuration">]
TotalDurationMinutes : int
[<JsonPropertyName "averageDuration">]
AverageDurationMinutes : int
TotalVisits : int
TotalClasses : int
IsEstimated : bool
LastRefreshed : DateTime
}
/// The PureGym REST API. You probably want to instantiate one of these with `Api.make`.
[<Header("User-Agent", "PureGym/1523 CFNetwork/1312 Darwin/21.0.0")>]
type IPureGymApi =
/// Get the complete list of all gyms known to PureGym.
[<Get "v1/gyms/">]
abstract GetGyms : unit -> Task<Gym list>
/// Get information about the PureGym human whose credentials this client is authenticated with.
[<Get "v1/member">]
abstract GetMember : unit -> Task<Member>
/// Get information about how full the given gym currently is. The gym ID can be found from `GetGyms`.
[<Get "v1/gyms/{gym_id}/attendance">]
abstract GetGymAttendance : [<Path "gym_id">] gymId : int -> Task<GymAttendance>
/// Get information about a specific gym.
[<Get "v1/gyms/{gym_id}">]
abstract GetGym : [<Path "gym_id">] gymId : int -> Task<Gym>
/// Get information about the activities logged against the currently authenticated PureGym human.
[<Get "v1/member/activity">]
abstract GetMemberActivity : unit -> Task<MemberActivity>
/// Methods for interacting with the PureGym REST API.
[<RequireQualifiedAccess>]
module Api =
/// Create a REST client, authenticated as the specified user.
let make (auth : Auth) : IPureGymApi Task =
task {
let! token =
match auth with
| Auth.Token t -> Task.FromResult<_> t
| Auth.User cred -> AuthToken.get cred
let client = new HttpClient ()
client.BaseAddress <- Uri "https://capi.puregym.com/api"
client.DefaultRequestHeaders.Authorization <-
Headers.AuthenticationHeaderValue ("Bearer", token.AccessToken)
client.DefaultRequestHeaders.Add ("User-Agent", "PureGym/1523 CFNetwork/1312 Darwin/21.0.0")
return RestClient.For<IPureGymApi> client
}

97
PureGym/Auth.fs Normal file
View File

@@ -0,0 +1,97 @@
namespace PureGym
open System
open System.Collections.Generic
open System.Net.Http
open System.Text.Json
open System.Text.Json.Serialization
open System.Threading.Tasks
// System.Text.Json does not support internal F# records as of .NET 8, presumably because it can't find the constructor.
// So we end up with this gruesome soup of C#.
// TODO(net8): make this internal
type AuthResponseRaw [<JsonConstructor>] (access_token : string, expires_in : int, token_type : string, scope : string)
=
member _.access_token = access_token
member _.expires_in = expires_in
member _.token_type = token_type
member _.scope = scope
/// A token which can be used to authenticate with the PureGym API.
type AuthToken =
{
/// A string which can be passed as the "Bearer" in an Authorization header
AccessToken : string
/// Time that this token expires, if known
ExpiryTime : DateTime option
}
static member internal Parse (response : AuthResponseRaw) : AuthToken =
if response.scope <> "pgcapi" then
failwithf $"unexpected scope: %s{response.scope}"
if response.token_type <> "Bearer" then
failwithf $"unexpected type: %s{response.token_type}"
{
AccessToken = response.access_token
ExpiryTime = Some (DateTime.Now + TimeSpan.FromSeconds response.expires_in)
}
/// Authentication credentials as known to a human user
type UsernamePin =
{
/// The user's email address
Username : string
/// Eight-digit PIN
Pin : string
}
/// Any way to authenticate with the PureGym API.
type Auth =
/// Authenticate with credentials which are known to the user
| User of UsernamePin
/// An AuthToken (that is, a bearer token)
| Token of AuthToken
/// Methods for constructing PureGym authentication tokens.
[<RequireQualifiedAccess>]
module AuthToken =
/// Construct an AuthToken given that you already have a bearer token.
let ofBearerToken (token : string) : AuthToken =
{
AccessToken = token
ExpiryTime = None
}
let private options = JsonSerializerOptions (IncludeFields = true)
/// Get an AuthToken for the given user email address with the given eight-digit PureGym PIN.
let get (creds : UsernamePin) : Task<AuthToken> =
task {
use client = new HttpClient ()
client.BaseAddress <- Uri "https://auth.puregym.com"
client.DefaultRequestHeaders.Add ("User-Agent", "PureGym/1523 CFNetwork/1312 Darwin/21.0.0")
let request =
[
"grant_type", "password"
"username", creds.Username
"password", creds.Pin
"scope", "pgcapi"
"client_id", "ro.client"
]
|> List.map KeyValuePair
use content = new FormUrlEncodedContent (request)
let! response = client.PostAsync (Uri "https://auth.puregym.com/connect/token", content)
if response.IsSuccessStatusCode then
let! content = response.Content.ReadAsStreamAsync ()
let! response = JsonSerializer.DeserializeAsync<AuthResponseRaw> (content, options)
// let! response = JsonSerializer.DeserializeAsync<AuthResponseRaw> (content, options)
return AuthToken.Parse response
else
let! content = response.Content.ReadAsStringAsync ()
return failwithf $"bad status code: %+A{response.StatusCode}\n%s{content}"
}

48
PureGym/GymSelector.fs Normal file
View File

@@ -0,0 +1,48 @@
namespace PureGym
open System.Threading.Tasks
open Fastenshtein
/// Identifies a gym, possibly non-uniquely and possibly ambiguously.
[<RequireQualifiedAccess>]
type GymSelector =
/// The ID of this gym, according to the PureGym internal mapping.
| Id of int
/// The user-specified name of this gym.
| Name of string
/// The home gym of the authenticated user.
| Home
/// Methods for manipulating GymSelector.
[<RequireQualifiedAccess>]
module GymSelector =
/// Get the canonical PureGym ID for this user-specified gym.
let canonicalId (client : IPureGymApi) (gym : GymSelector) : int Task =
match gym with
| GymSelector.Home ->
task {
let! self = client.GetMember ()
return self.HomeGymId
}
| GymSelector.Id i -> Task.FromResult<_> i
| GymSelector.Name name ->
task {
let! allGyms = client.GetGyms ()
if allGyms.IsEmpty then
return failwith "PureGym API returned no gyms!"
else
let distance = Levenshtein (name.ToLowerInvariant ())
let bestDistance, bestGym =
allGyms
|> Seq.map (fun gym -> distance.DistanceFrom (gym.Name.ToLowerInvariant ()), gym)
|> Seq.sortBy fst
|> Seq.head
if bestDistance <> 0 then
System.Console.Error.WriteLine $"Autocorrected gym from %s{name} to %s{bestGym.Name}"
return bestGym.Id
}

24
PureGym/PureGym.fsproj Normal file
View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="String.fs" />
<Compile Include="Auth.fs" />
<Compile Include="Api.fs" />
<Compile Include="GymSelector.fs" />
<EmbeddedResource Include="SurfaceBaseline.txt" />
<EmbeddedResource Include="version.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="RestEase" Version="1.6.4" />
<PackageReference Update="FSharp.Core" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="7.0.3" />
<PackageReference Include="Fastenshtein" Version="1.0.0.8" />
</ItemGroup>
</Project>

5
PureGym/String.fs Normal file
View File

@@ -0,0 +1,5 @@
namespace PureGym
[<RequireQualifiedAccess>]
module internal Char =
let emoji bool = if bool then '✅' else '❌'

198
PureGym/SurfaceBaseline.txt Normal file
View File

@@ -0,0 +1,198 @@
PureGym.Api inherit obj
PureGym.Api.make [static method]: PureGym.Auth -> PureGym.IPureGymApi System.Threading.Tasks.Task
PureGym.Auth inherit obj, implements PureGym.Auth System.IEquatable, System.Collections.IStructuralEquatable, PureGym.Auth System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 2 cases
PureGym.Auth+Tags inherit obj
PureGym.Auth+Tags.Token [static field]: int = 1
PureGym.Auth+Tags.User [static field]: int = 0
PureGym.Auth+Token inherit PureGym.Auth
PureGym.Auth+Token.get_Item [method]: unit -> PureGym.AuthToken
PureGym.Auth+Token.Item [property]: [read-only] PureGym.AuthToken
PureGym.Auth+User inherit PureGym.Auth
PureGym.Auth+User.get_Item [method]: unit -> PureGym.UsernamePin
PureGym.Auth+User.Item [property]: [read-only] PureGym.UsernamePin
PureGym.Auth.get_IsToken [method]: unit -> bool
PureGym.Auth.get_IsUser [method]: unit -> bool
PureGym.Auth.get_Tag [method]: unit -> int
PureGym.Auth.IsToken [property]: [read-only] bool
PureGym.Auth.IsUser [property]: [read-only] bool
PureGym.Auth.NewToken [static method]: PureGym.AuthToken -> PureGym.Auth
PureGym.Auth.NewUser [static method]: PureGym.UsernamePin -> PureGym.Auth
PureGym.Auth.Tag [property]: [read-only] int
PureGym.AuthResponseRaw inherit obj
PureGym.AuthResponseRaw..ctor [constructor]: (string, int, string, string)
PureGym.AuthResponseRaw.access_token [property]: [read-only] string
PureGym.AuthResponseRaw.expires_in [property]: [read-only] int
PureGym.AuthResponseRaw.get_access_token [method]: unit -> string
PureGym.AuthResponseRaw.get_expires_in [method]: unit -> int
PureGym.AuthResponseRaw.get_scope [method]: unit -> string
PureGym.AuthResponseRaw.get_token_type [method]: unit -> string
PureGym.AuthResponseRaw.scope [property]: [read-only] string
PureGym.AuthResponseRaw.token_type [property]: [read-only] string
PureGym.AuthToken inherit obj, implements PureGym.AuthToken System.IEquatable, System.Collections.IStructuralEquatable, PureGym.AuthToken System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.AuthToken..ctor [constructor]: (string, System.DateTime option)
PureGym.AuthToken.AccessToken [property]: [read-only] string
PureGym.AuthToken.ExpiryTime [property]: [read-only] System.DateTime option
PureGym.AuthToken.get_AccessToken [method]: unit -> string
PureGym.AuthToken.get_ExpiryTime [method]: unit -> System.DateTime option
PureGym.AuthTokenModule inherit obj
PureGym.AuthTokenModule.get [static method]: PureGym.UsernamePin -> PureGym.AuthToken System.Threading.Tasks.Task
PureGym.AuthTokenModule.ofBearerToken [static method]: string -> PureGym.AuthToken
PureGym.Gym inherit obj, implements PureGym.Gym System.IEquatable, System.Collections.IStructuralEquatable, PureGym.Gym System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.Gym..ctor [constructor]: (string, int, int, PureGym.GymAddress, string, string, PureGym.GymOpeningHours, PureGym.GymAccessOptions, PureGym.GymLocation, string, string)
PureGym.Gym.AccessOptions [property]: [read-only] PureGym.GymAccessOptions
PureGym.Gym.Address [property]: [read-only] PureGym.GymAddress
PureGym.Gym.EmailAddress [property]: [read-only] string
PureGym.Gym.get_AccessOptions [method]: unit -> PureGym.GymAccessOptions
PureGym.Gym.get_Address [method]: unit -> PureGym.GymAddress
PureGym.Gym.get_EmailAddress [method]: unit -> string
PureGym.Gym.get_GymOpeningHours [method]: unit -> PureGym.GymOpeningHours
PureGym.Gym.get_Id [method]: unit -> int
PureGym.Gym.get_Location [method]: unit -> PureGym.GymLocation
PureGym.Gym.get_Name [method]: unit -> string
PureGym.Gym.get_PhoneNumber [method]: unit -> string
PureGym.Gym.get_ReopenDate [method]: unit -> string
PureGym.Gym.get_Status [method]: unit -> int
PureGym.Gym.get_TimeZone [method]: unit -> string
PureGym.Gym.GymOpeningHours [property]: [read-only] PureGym.GymOpeningHours
PureGym.Gym.Id [property]: [read-only] int
PureGym.Gym.Location [property]: [read-only] PureGym.GymLocation
PureGym.Gym.Name [property]: [read-only] string
PureGym.Gym.PhoneNumber [property]: [read-only] string
PureGym.Gym.ReopenDate [property]: [read-only] string
PureGym.Gym.Status [property]: [read-only] int
PureGym.Gym.TimeZone [property]: [read-only] string
PureGym.GymAccessOptions inherit obj, implements PureGym.GymAccessOptions System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymAccessOptions System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.GymAccessOptions..ctor [constructor]: (bool, bool)
PureGym.GymAccessOptions.get_PinAccess [method]: unit -> bool
PureGym.GymAccessOptions.get_QrCodeAccess [method]: unit -> bool
PureGym.GymAccessOptions.PinAccess [property]: [read-only] bool
PureGym.GymAccessOptions.QrCodeAccess [property]: [read-only] bool
PureGym.GymAddress inherit obj, implements PureGym.GymAddress System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymAddress System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.GymAddress..ctor [constructor]: (string, string, string, string, string, string)
PureGym.GymAddress.AddressLine1 [property]: [read-only] string
PureGym.GymAddress.AddressLine2 [property]: [read-only] string
PureGym.GymAddress.AddressLine3 [property]: [read-only] string
PureGym.GymAddress.County [property]: [read-only] string
PureGym.GymAddress.get_AddressLine1 [method]: unit -> string
PureGym.GymAddress.get_AddressLine2 [method]: unit -> string
PureGym.GymAddress.get_AddressLine3 [method]: unit -> string
PureGym.GymAddress.get_County [method]: unit -> string
PureGym.GymAddress.get_Postcode [method]: unit -> string
PureGym.GymAddress.get_Town [method]: unit -> string
PureGym.GymAddress.Postcode [property]: [read-only] string
PureGym.GymAddress.Town [property]: [read-only] string
PureGym.GymAttendance inherit obj, implements PureGym.GymAttendance System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymAttendance System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.GymAttendance..ctor [constructor]: (string, int, int, string, bool, System.DateTime, System.DateTime, System.DateTime, int)
PureGym.GymAttendance.AttendanceTime [property]: [read-only] System.DateTime
PureGym.GymAttendance.Description [property]: [read-only] string
PureGym.GymAttendance.get_AttendanceTime [method]: unit -> System.DateTime
PureGym.GymAttendance.get_Description [method]: unit -> string
PureGym.GymAttendance.get_IsApproximate [method]: unit -> bool
PureGym.GymAttendance.get_LastRefreshed [method]: unit -> System.DateTime
PureGym.GymAttendance.get_LastRefreshedPeopleInClasses [method]: unit -> System.DateTime
PureGym.GymAttendance.get_MaximumCapacity [method]: unit -> int
PureGym.GymAttendance.get_TotalPeopleInClasses [method]: unit -> int
PureGym.GymAttendance.get_TotalPeopleInGym [method]: unit -> int
PureGym.GymAttendance.get_TotalPeopleSuffix [method]: unit -> string
PureGym.GymAttendance.IsApproximate [property]: [read-only] bool
PureGym.GymAttendance.LastRefreshed [property]: [read-only] System.DateTime
PureGym.GymAttendance.LastRefreshedPeopleInClasses [property]: [read-only] System.DateTime
PureGym.GymAttendance.MaximumCapacity [property]: [read-only] int
PureGym.GymAttendance.TotalPeopleInClasses [property]: [read-only] int
PureGym.GymAttendance.TotalPeopleInGym [property]: [read-only] int
PureGym.GymAttendance.TotalPeopleSuffix [property]: [read-only] string
PureGym.GymLocation inherit obj, implements PureGym.GymLocation System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymLocation System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.GymLocation..ctor [constructor]: (float, float)
PureGym.GymLocation.get_Latitude [method]: unit -> float
PureGym.GymLocation.get_Longitude [method]: unit -> float
PureGym.GymLocation.Latitude [property]: [read-only] float
PureGym.GymLocation.Longitude [property]: [read-only] float
PureGym.GymOpeningHours inherit obj, implements PureGym.GymOpeningHours System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymOpeningHours System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.GymOpeningHours..ctor [constructor]: (bool, string list)
PureGym.GymOpeningHours.get_IsAlwaysOpen [method]: unit -> bool
PureGym.GymOpeningHours.get_OpeningHours [method]: unit -> string list
PureGym.GymOpeningHours.IsAlwaysOpen [property]: [read-only] bool
PureGym.GymOpeningHours.OpeningHours [property]: [read-only] string list
PureGym.GymSelector inherit obj, implements PureGym.GymSelector System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymSelector System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 3 cases
PureGym.GymSelector+Id inherit PureGym.GymSelector
PureGym.GymSelector+Id.get_Item [method]: unit -> int
PureGym.GymSelector+Id.Item [property]: [read-only] int
PureGym.GymSelector+Name inherit PureGym.GymSelector
PureGym.GymSelector+Name.get_Item [method]: unit -> string
PureGym.GymSelector+Name.Item [property]: [read-only] string
PureGym.GymSelector+Tags inherit obj
PureGym.GymSelector+Tags.Home [static field]: int = 2
PureGym.GymSelector+Tags.Id [static field]: int = 0
PureGym.GymSelector+Tags.Name [static field]: int = 1
PureGym.GymSelector.get_Home [static method]: unit -> PureGym.GymSelector
PureGym.GymSelector.get_IsHome [method]: unit -> bool
PureGym.GymSelector.get_IsId [method]: unit -> bool
PureGym.GymSelector.get_IsName [method]: unit -> bool
PureGym.GymSelector.get_Tag [method]: unit -> int
PureGym.GymSelector.Home [static property]: [read-only] PureGym.GymSelector
PureGym.GymSelector.IsHome [property]: [read-only] bool
PureGym.GymSelector.IsId [property]: [read-only] bool
PureGym.GymSelector.IsName [property]: [read-only] bool
PureGym.GymSelector.NewId [static method]: int -> PureGym.GymSelector
PureGym.GymSelector.NewName [static method]: string -> PureGym.GymSelector
PureGym.GymSelector.Tag [property]: [read-only] int
PureGym.GymSelectorModule inherit obj
PureGym.GymSelectorModule.canonicalId [static method]: PureGym.IPureGymApi -> PureGym.GymSelector -> int System.Threading.Tasks.Task
PureGym.IPureGymApi - interface with 5 member(s)
PureGym.IPureGymApi.GetGym [method]: int -> PureGym.Gym System.Threading.Tasks.Task
PureGym.IPureGymApi.GetGymAttendance [method]: int -> PureGym.GymAttendance System.Threading.Tasks.Task
PureGym.IPureGymApi.GetGyms [method]: unit -> PureGym.Gym list System.Threading.Tasks.Task
PureGym.IPureGymApi.GetMember [method]: unit -> PureGym.Member System.Threading.Tasks.Task
PureGym.IPureGymApi.GetMemberActivity [method]: unit -> PureGym.MemberActivity System.Threading.Tasks.Task
PureGym.Member inherit obj, implements PureGym.Member System.IEquatable, System.Collections.IStructuralEquatable, PureGym.Member System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.Member..ctor [constructor]: (int, string, string, string, int, string, string, string, System.DateOnly, string, string, string, int, int, int)
PureGym.Member.CompoundMemberId [property]: [read-only] string
PureGym.Member.DateOfBirth [property]: [read-only] System.DateOnly
PureGym.Member.EmailAddress [property]: [read-only] string
PureGym.Member.FirstName [property]: [read-only] string
PureGym.Member.get_CompoundMemberId [method]: unit -> string
PureGym.Member.get_DateOfBirth [method]: unit -> System.DateOnly
PureGym.Member.get_EmailAddress [method]: unit -> string
PureGym.Member.get_FirstName [method]: unit -> string
PureGym.Member.get_GymAccessPin [method]: unit -> string
PureGym.Member.get_HomeGymId [method]: unit -> int
PureGym.Member.get_HomeGymName [method]: unit -> string
PureGym.Member.get_Id [method]: unit -> int
PureGym.Member.get_LastName [method]: unit -> string
PureGym.Member.get_MembershipLevel [method]: unit -> int
PureGym.Member.get_MembershipName [method]: unit -> string
PureGym.Member.get_MemberStatus [method]: unit -> int
PureGym.Member.get_MobileNumber [method]: unit -> string
PureGym.Member.get_Postcode [method]: unit -> string
PureGym.Member.get_SuspendedReason [method]: unit -> int
PureGym.Member.GymAccessPin [property]: [read-only] string
PureGym.Member.HomeGymId [property]: [read-only] int
PureGym.Member.HomeGymName [property]: [read-only] string
PureGym.Member.Id [property]: [read-only] int
PureGym.Member.LastName [property]: [read-only] string
PureGym.Member.MembershipLevel [property]: [read-only] int
PureGym.Member.MembershipName [property]: [read-only] string
PureGym.Member.MemberStatus [property]: [read-only] int
PureGym.Member.MobileNumber [property]: [read-only] string
PureGym.Member.Postcode [property]: [read-only] string
PureGym.Member.SuspendedReason [property]: [read-only] int
PureGym.MemberActivity inherit obj, implements PureGym.MemberActivity System.IEquatable, System.Collections.IStructuralEquatable, PureGym.MemberActivity System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.MemberActivity..ctor [constructor]: (int, int, int, int, bool, System.DateTime)
PureGym.MemberActivity.AverageDurationMinutes [property]: [read-only] int
PureGym.MemberActivity.get_AverageDurationMinutes [method]: unit -> int
PureGym.MemberActivity.get_IsEstimated [method]: unit -> bool
PureGym.MemberActivity.get_LastRefreshed [method]: unit -> System.DateTime
PureGym.MemberActivity.get_TotalClasses [method]: unit -> int
PureGym.MemberActivity.get_TotalDurationMinutes [method]: unit -> int
PureGym.MemberActivity.get_TotalVisits [method]: unit -> int
PureGym.MemberActivity.IsEstimated [property]: [read-only] bool
PureGym.MemberActivity.LastRefreshed [property]: [read-only] System.DateTime
PureGym.MemberActivity.TotalClasses [property]: [read-only] int
PureGym.MemberActivity.TotalDurationMinutes [property]: [read-only] int
PureGym.MemberActivity.TotalVisits [property]: [read-only] int
PureGym.UsernamePin inherit obj, implements PureGym.UsernamePin System.IEquatable, System.Collections.IStructuralEquatable, PureGym.UsernamePin System.IComparable, System.IComparable, System.Collections.IStructuralComparable
PureGym.UsernamePin..ctor [constructor]: (string, string)
PureGym.UsernamePin.get_Pin [method]: unit -> string
PureGym.UsernamePin.get_Username [method]: unit -> string
PureGym.UsernamePin.Pin [property]: [read-only] string
PureGym.UsernamePin.Username [property]: [read-only] string

7
PureGym/version.json Normal file
View File

@@ -0,0 +1,7 @@
{
"version": "1.0",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
"pathFilters": null
}

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# Unofficial PureGym client
![Status](https://woodpecker.patrickstevens.co.uk/api/badges/44/status.svg)
*With thanks to [Tom Hollingsworth](https://github.com/2t6h/puregym-attendance/blob/64dcd830bd874dc0150c7767f5cc6c75ed0b9dad/puregym.py).*
## Usage
* With Nix: `nix run` (you can refer to this flake).
* Manually: `git clone` and then `dotnet run --project PureGym.App/PureGym.App.fsproj`.
(Something is up on Darwin: `nix run` currently produces an executable which dies instantly.
Workaround: `nix build` and then `nix develop --command dotnet exec ./result/lib/puregym/PureGym.App.dll`.)
The available subcommands can be viewed in the `subcommands` map defined in [Program.fs](./PureGym.App/Program.fs).
As of this writing, the following are implemented:
* `activity`: get the logged-in user's activity stats. I have no idea what the semantics of these numbers are!
* `fullness`: determine how full a given gym is right now.
* `lookup-gym`: give information about the gym's physical instantiation (e.g. its address).
### Authentication
You can authenticate with your PureGym email address and PIN combination, or (probably better) you can call `PureGym.App auth` to obtain a token.
Use this token in subsequent commands by setting the `PUREGYM_BEARER_TOKEN` environment variable or supplying it as `--bearer-token`.
## Structure
* The REST client is at [Api.fs](./PureGym/Api.fs).
* The standalone application is at [PureGym.App](./PureGym.App). It uses a ghastly mix of hand-rolled argument parsing and [Argu](https://fsprojects.github.io/Argu/), because Argu does not *quite* want to do what I want an argument parser to do.

60
flake.lock generated Normal file
View File

@@ -0,0 +1,60 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1696981262,
"narHash": "sha256-YaCOjdqhbjBeyMjxlgFWt4XD/b9pGKWURgS3uEwNLtc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a2b87a4f66f309d2f4b789fd0457f5fc5db0a9a6",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

114
flake.nix Normal file
View File

@@ -0,0 +1,114 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
flake-utils = {
url = "github:numtide/flake-utils";
};
};
outputs = inputs @ {
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
projectFile = "./PureGym.App/PureGym.App.fsproj";
testProjectFile = "./PureGym.Test/PureGym.Test.fsproj";
pname = "puregym";
dotnet-sdk = pkgs.dotnet-sdk_7;
dotnet-runtime = pkgs.dotnetCorePackages.runtime_7_0;
version = "0.1";
dotnetTool = toolName: toolVersion: sha256:
pkgs.stdenvNoCC.mkDerivation rec {
name = toolName;
version = toolVersion;
nativeBuildInputs = [pkgs.makeWrapper];
src = pkgs.fetchNuGet {
pname = name;
version = version;
sha256 = sha256;
installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
};
installPhase = ''
runHook preInstall
mkdir -p "$out/lib"
cp -r ./bin/* "$out/lib"
makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${name}.dll"
runHook postInstall
'';
};
fantomas = dotnetTool "fantomas" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fantomas.version "sha256-83RodORaC3rkYfbFMHsYLEtl0+8+akZXcKoSJdgwuUo=";
in {
packages = {
fantomas = fantomas;
fetchDeps = let
flags = [];
runtimeIds = ["win-x64"] ++ map (system: pkgs.dotnetCorePackages.systemToDotnetRid system) dotnet-sdk.meta.platforms;
in
pkgs.writeShellScriptBin "fetch-${pname}-deps" (builtins.readFile (pkgs.substituteAll {
src = ./nix/fetchDeps.sh;
pname = pname;
binPath = pkgs.lib.makeBinPath [pkgs.coreutils dotnet-sdk (pkgs.nuget-to-nix.override {inherit dotnet-sdk;})];
projectFiles = toString (pkgs.lib.toList projectFile);
testProjectFiles = toString (pkgs.lib.toList testProjectFile);
rids = pkgs.lib.concatStringsSep "\" \"" runtimeIds;
packages = dotnet-sdk.packages;
storeSrc = pkgs.srcOnly {
src = ./.;
pname = pname;
version = version;
};
}));
default = pkgs.buildDotnetModule {
pname = pname;
name = "puregym";
version = version;
src = ./.;
projectFile = projectFile;
nugetDeps = ./nix/deps.nix;
doCheck = true;
dotnet-sdk = dotnet-sdk;
dotnet-runtime = dotnet-runtime;
};
};
apps = {
default = {
type = "app";
program = "${self.packages.${system}.default}/bin/PureGym.App";
};
};
devShells.default = pkgs.mkShell {
buildInputs =
[pkgs.alejandra pkgs.dotnet-sdk_7 pkgs.python3 pkgs.nodePackages.markdown-link-check]
++ (
if pkgs.stdenv.isDarwin
then [pkgs.darwin.apple_sdk.frameworks.CoreServices]
else []
);
};
checks = {
alejandra = pkgs.stdenvNoCC.mkDerivation {
name = "alejandra-check";
src = ./.;
checkPhase = ''
${pkgs.alejandra}/bin/alejandra --check .
'';
installPhase = "mkdir $out";
dontBuild = true;
doCheck = true;
};
fantomas = pkgs.stdenvNoCC.mkDerivation {
name = "fantomas-check";
src = ./.;
checkPhase = ''
${fantomas}/bin/fantomas --check .
'';
installPhase = "mkdir $out";
dontBuild = true;
doCheck = true;
};
};
});
}

364
nix/deps.nix Normal file
View File

@@ -0,0 +1,364 @@
# This file was automatically generated by passthru.fetch-deps.
# Please don't edit it manually, your changes might get overwritten!
{fetchNuGet}: [
(fetchNuGet {
pname = "Fastenshtein";
version = "1.0.0.8";
sha256 = "1rvw27rz7qb2n68i0jvvcr224fcpy5yzzxaj1bp89jw41cpdabp2";
})
(fetchNuGet {
pname = "Argu";
version = "6.1.1";
sha256 = "1v996g0760qhiys2ahdpnvkldaxr2jn5f1falf789glnk4a6f3xl";
})
(fetchNuGet {
pname = "System.Configuration.ConfigurationManager";
version = "4.4.0";
sha256 = "1hjgmz47v5229cbzd2pwz2h0dkq78lb2wp9grx8qr72pb5i0dk7v";
})
(fetchNuGet {
pname = "fantomas";
version = "6.2.0";
sha256 = "sha256-83RodORaC3rkYfbFMHsYLEtl0+8+akZXcKoSJdgwuUo=";
})
(fetchNuGet {
pname = "ApiSurface";
version = "4.0.12";
sha256 = "0v56sv4cz8bgrfqjjg0q96619qs9dvvi0a6lp7hzz2mi82i1inmq";
})
(fetchNuGet {
pname = "coverlet.collector";
version = "3.2.0";
sha256 = "1qxpv8v10p5wn162lzdm193gdl6c5f81zadj8h889dprlnj3g8yr";
})
(fetchNuGet {
pname = "FSharp.Core";
version = "6.0.0";
sha256 = "1hjhvr39c1vpgrdmf8xln5q86424fqkvy9nirkr29vl2461d2039";
})
(fetchNuGet {
pname = "FSharp.Core";
version = "7.0.400";
sha256 = "1pl6iqqcpm9djfn7f6ms5j1xbcyz00nb808qd6pmsjrnylflalgp";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Ref";
version = "6.0.22";
sha256 = "0fqpl1fr213b4fb3c6xw3fy6669yxqcp1bzcnayw80yrskw8lpxs";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.linux-arm64";
version = "6.0.22";
sha256 = "1xvqqc7bzj764g3scp0saqxlfiv866crgi8chz57vhjp9sgd61jw";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.linux-arm64";
version = "7.0.11";
sha256 = "0hmsqy4yc3023mcp5rg0h59yv3f8cnjhxw1g4i8md67vm5y04lfv";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.linux-x64";
version = "6.0.22";
sha256 = "1gcv99y295fnhy12fyx8wqvbhbj6mz8p5bm66ppwdxb3zykjg2l8";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.linux-x64";
version = "7.0.11";
sha256 = "18sk9wka8z5354ca77q43hi0615yjssdjbyi0hqq92w6zmg43vgc";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.osx-arm64";
version = "6.0.22";
sha256 = "1ib0x1w33wqy7lgzjf14dvgx981xpjffjqd800d7wgxisgmakrmr";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.osx-arm64";
version = "7.0.11";
sha256 = "1j0zbd4rmmd3ylgixsvyj145g2r6px6b9d9k4yxxg6d61x90c165";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.osx-x64";
version = "6.0.22";
sha256 = "026r38a7by7wdfd3virjdaah3y2sjjmnabgf5l25vdnwpwc7c31d";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.osx-x64";
version = "7.0.11";
sha256 = "0wxw7vgygg6hqzq479n0pfjizr69wq7ja03a0qh8bma8b9q2mn6f";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.win-x64";
version = "6.0.22";
sha256 = "0ygdqsd312kqpykwb0k2942n45q1w3yn1nia6m1ahf7b74926qb5";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.win-x64";
version = "7.0.11";
sha256 = "05ywwfn5lzx6y999f7gwmablkxi2zvska4sg20ihmjzp3xakcmk0";
})
(fetchNuGet {
pname = "Microsoft.CodeCoverage";
version = "17.6.0";
sha256 = "02s98d8nwz5mg4mymcr86qdamy71a29g2091xg452czmd3s3x2di";
})
(fetchNuGet {
pname = "Microsoft.NET.Test.Sdk";
version = "17.6.0";
sha256 = "1bnwpwg7k72z06027ip4yi222863r8sv14ck9nj8h64ckiw2r256";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.linux-arm64";
version = "6.0.22";
sha256 = "0gri1gqznm5c8fsb6spqb3j88a3b0br0iy50y66fh4hz9wc4fwzm";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.linux-arm64";
version = "7.0.11";
sha256 = "03nkxjn4wq30rw0163rqi8sngfxmcvwgm0wg7sgyb1cdh0q1ai68";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.linux-x64";
version = "6.0.22";
sha256 = "0k1i74wn6j7nq0bd8m6jrpl65wda6qc9pglppvz4ybk0n2ab1rbi";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.linux-x64";
version = "7.0.11";
sha256 = "12hh69sr4wf8sjcw3q71vky51sn854ffahbq6rgz3njzvbvc0dbj";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.osx-arm64";
version = "6.0.22";
sha256 = "0166gwarhhnary19lf80ff33bkx00mkm24f17bc8j6v7g3a7zvq6";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.osx-x64";
version = "6.0.22";
sha256 = "038bjwk201p2kzs3jflrkhlnszf7cwalafq0nvs2v8bp7jlnx5ib";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.osx-x64";
version = "7.0.11";
sha256 = "1j1k735gkwba93n5yck87wppfpsbny979hppcygwrk81myf3fv03";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.win-x64";
version = "6.0.22";
sha256 = "1bjy3zmrmaq97xp0f3nzs3ax330ji632avrfpg8xz4vc5p8s1xpc";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.win-x64";
version = "7.0.11";
sha256 = "0ifshdx19bgnbgynbk6iy6gybnxmp63nylrn7068x66hvcavh7kh";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Ref";
version = "6.0.22";
sha256 = "0km8184kma8kgz7iyl3j6apj1n7vskzdhzmq3myy3y36ysqrb4wf";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.linux-arm64";
version = "6.0.22";
sha256 = "01gbl9dgky4h7ijxryz3527l39v23lkcvk4fs4w91ra4pris2n8p";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.linux-arm64";
version = "7.0.11";
sha256 = "1gzwc96fs222ddia0k1924cn7gxm2a4anqgcxhmavx56x76wsy6f";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.linux-x64";
version = "6.0.22";
sha256 = "09gfqdxbh36bjx20fw9k94b9qa9bwffhrq0ldwn834mx31bgrfs8";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.linux-x64";
version = "7.0.11";
sha256 = "0vxza49wwiia0d3m887yiaprp3xnax2bgzhj5bf080b4ayapzkf9";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.osx-arm64";
version = "6.0.22";
sha256 = "1x7wclv93q8wp7rip5nwnsxbqcami92yilvzbp0yn42ddkw177ds";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.osx-arm64";
version = "7.0.11";
sha256 = "15b62hxrpfy19xvyxlyligixxpa9sysfgi47xi4imx5055fhwphh";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.osx-x64";
version = "6.0.22";
sha256 = "1sq1ygsrpv2sl85wrs8382wgkjic0zylaj1y8kcvhczcmkpk3wr5";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.osx-x64";
version = "7.0.11";
sha256 = "018qf23b0jixfh3fm74zqaakk01qx6yq21gk2mdn68b0xhnvlzma";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.win-x64";
version = "6.0.22";
sha256 = "1nn254xv1hi5c4rg38fbfkln3031vv545lv9f4df31i8c1yfzz24";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.win-x64";
version = "7.0.11";
sha256 = "12xmw2kcpf5rh8sv4y0mqzp917f7q8g4mfh5navqw4jmnxyb26qq";
})
(fetchNuGet {
pname = "Microsoft.NETCore.Platforms";
version = "1.1.0";
sha256 = "08vh1r12g6ykjygq5d3vq09zylgb84l63k49jc4v8faw9g93iqqm";
})
(fetchNuGet {
pname = "Microsoft.NETCore.Platforms";
version = "2.0.0";
sha256 = "1fk2fk2639i7nzy58m9dvpdnzql4vb8yl8vr19r2fp8lmj9w2jr0";
})
(fetchNuGet {
pname = "Microsoft.TestPlatform.ObjectModel";
version = "17.6.0";
sha256 = "1rz22chnis11dwjrqrcvvmfw80fi2a7756a7ahwy6jlnr250zr61";
})
(fetchNuGet {
pname = "Microsoft.TestPlatform.TestHost";
version = "17.6.0";
sha256 = "16vpicp4q2kbpgr3qwpsxg7srabxqszx23x6smjvvrvz7qmr5v8i";
})
(fetchNuGet {
pname = "NETStandard.Library";
version = "2.0.0";
sha256 = "1bc4ba8ahgk15m8k4nd7x406nhi0kwqzbgjk2dmw52ss553xz7iy";
})
(fetchNuGet {
pname = "Newtonsoft.Json";
version = "13.0.1";
sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb";
})
(fetchNuGet {
pname = "NuGet.Common";
version = "6.6.1";
sha256 = "1q7k5rqwchxgs5pnrn22d1rkdb7l2qblvsb9hy046ll69i71vv45";
})
(fetchNuGet {
pname = "NuGet.Configuration";
version = "6.6.1";
sha256 = "0pw4ikd8784iya920wxigacqn5g2v0zlpwxjlswyq5mnj2ha7gpk";
})
(fetchNuGet {
pname = "NuGet.Frameworks";
version = "5.11.0";
sha256 = "0wv26gq39hfqw9md32amr5771s73f5zn1z9vs4y77cgynxr73s4z";
})
(fetchNuGet {
pname = "NuGet.Frameworks";
version = "6.6.1";
sha256 = "1zq79mklzq7qyiyhcv3w8pznw6rq1ddcl8fvy7j1c6n8qh3mglhx";
})
(fetchNuGet {
pname = "NuGet.Packaging";
version = "6.6.1";
sha256 = "1lmx8kgpg220q8kic4wm8skccj53cbkdqggirq9js34gnxxi9b88";
})
(fetchNuGet {
pname = "NuGet.Protocol";
version = "6.6.1";
sha256 = "01n8cw114npvzfk3m3803lb8plk0wm1zg496gpq9az8hw20nmd8g";
})
(fetchNuGet {
pname = "NuGet.Versioning";
version = "6.6.1";
sha256 = "0n2p05y8ciw6jc5s238rlnx6q4dgxvm14v06pcd84ji5j1iirc30";
})
(fetchNuGet {
pname = "NUnit";
version = "3.13.3";
sha256 = "0wdzfkygqnr73s6lpxg5b1pwaqz9f414fxpvpdmf72bvh4jaqzv6";
})
(fetchNuGet {
pname = "NUnit.Analyzers";
version = "3.6.1";
sha256 = "16dw5375k2wyhiw9x387y7pjgq6zms30y036qb8z7idx4lxw9yi9";
})
(fetchNuGet {
pname = "NUnit3TestAdapter";
version = "4.4.2";
sha256 = "1n2jlc16vjdd81cb1by4qbp75sq73zsjz5w3zc61ssmbdci1q2ri";
})
(fetchNuGet {
pname = "RestEase";
version = "1.6.4";
sha256 = "1mvi3nbrr450g3fgd1y4wg3bwl9k1agyjfd9wdkqk12714bsln8l";
})
(fetchNuGet {
pname = "System.Formats.Asn1";
version = "5.0.0";
sha256 = "1axc8z0839yvqi2cb63l73l6d9j6wd20lsbdymwddz9hvrsgfwpn";
})
(fetchNuGet {
pname = "System.IO.Abstractions";
version = "4.2.13";
sha256 = "0s784iphsmj4vhkrzq9q3w39vsn76w44zclx3hsygsw458zbyh4y";
})
(fetchNuGet {
pname = "System.IO.FileSystem.AccessControl";
version = "4.5.0";
sha256 = "1gq4s8w7ds1sp8f9wqzf8nrzal40q5cd2w4pkf4fscrl2ih3hkkj";
})
(fetchNuGet {
pname = "System.Reflection.Metadata";
version = "1.6.0";
sha256 = "1wdbavrrkajy7qbdblpbpbalbdl48q3h34cchz24gvdgyrlf15r4";
})
(fetchNuGet {
pname = "System.Runtime.CompilerServices.Unsafe";
version = "6.0.0";
sha256 = "0qm741kh4rh57wky16sq4m0v05fxmkjjr87krycf5vp9f0zbahbc";
})
(fetchNuGet {
pname = "System.Security.AccessControl";
version = "4.5.0";
sha256 = "1wvwanz33fzzbnd2jalar0p0z3x0ba53vzx1kazlskp7pwyhlnq0";
})
(fetchNuGet {
pname = "System.Security.Cryptography.Cng";
version = "5.0.0";
sha256 = "06hkx2za8jifpslkh491dfwzm5dxrsyxzj5lsc0achb6yzg4zqlw";
})
(fetchNuGet {
pname = "System.Security.Cryptography.Pkcs";
version = "5.0.0";
sha256 = "0hb2mndac3xrw3786bsjxjfh19bwnr991qib54k6wsqjhjyyvbwj";
})
(fetchNuGet {
pname = "System.Security.Cryptography.ProtectedData";
version = "4.4.0";
sha256 = "1q8ljvqhasyynp94a1d7jknk946m20lkwy2c3wa8zw2pc517fbj6";
})
(fetchNuGet {
pname = "System.Security.Principal.Windows";
version = "4.5.0";
sha256 = "0rmj89wsl5yzwh0kqjgx45vzf694v9p92r4x4q6yxldk1cv1hi86";
})
(fetchNuGet {
pname = "System.Text.Encodings.Web";
version = "6.0.0";
sha256 = "06n9ql3fmhpjl32g3492sj181zjml5dlcc5l76xq2h38c4f87sai";
})
(fetchNuGet {
pname = "System.Text.Encodings.Web";
version = "7.0.0";
sha256 = "1151hbyrcf8kyg1jz8k9awpbic98lwz9x129rg7zk1wrs6vjlpxl";
})
(fetchNuGet {
pname = "System.Text.Json";
version = "6.0.0";
sha256 = "1si2my1g0q0qv1hiqnji4xh9wd05qavxnzj9dwgs23iqvgjky0gl";
})
(fetchNuGet {
pname = "System.Text.Json";
version = "7.0.3";
sha256 = "0zjrnc9lshagm6kdb9bdh45dmlnkpwcpyssa896sda93ngbmj8k9";
})
]

73
nix/fetchDeps.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/bin/bash
# This file was adapted from
# https://github.com/NixOS/nixpkgs/blob/b981d811453ab84fb3ea593a9b33b960f1ab9147/pkgs/build-support/dotnet/build-dotnet-module/default.nix#L173
set -euo pipefail
export PATH="@binPath@"
for arg in "$@"; do
case "$arg" in
--keep-sources|-k)
keepSources=1
shift
;;
--help|-h)
echo "usage: $0 [--keep-sources] [--help] <output path>"
echo " <output path> The path to write the lockfile to. A temporary file is used if this is not set"
echo " --keep-sources Don't remove temporary directories upon exit, useful for debugging"
echo " --help Show this help message"
exit
;;
esac
done
tmp=$(mktemp -td "@pname@-tmp-XXXXXX")
export tmp
HOME=$tmp/home
exitTrap() {
test -n "${ranTrap-}" && return
ranTrap=1
if test -n "${keepSources-}"; then
echo -e "Path to the source: $tmp/src\nPath to the fake home: $tmp/home"
else
rm -rf "$tmp"
fi
# Since mktemp is used this will be empty if the script didnt succesfully complete
if ! test -s "$depsFile"; then
rm -rf "$depsFile"
fi
}
trap exitTrap EXIT INT TERM
dotnetRestore() {
local -r project="${1-}"
local -r rid="$2"
dotnet restore "${project-}" \
-p:ContinuousIntegrationBuild=true \
-p:Deterministic=true \
--packages "$tmp/nuget_pkgs" \
--runtime "$rid" \
--no-cache \
--force
}
declare -a projectFiles=( @projectFiles@ )
declare -a testProjectFiles=( @testProjectFiles@ )
export DOTNET_NOLOGO=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1
depsFile=$(realpath "${1:-$(mktemp -t "@pname@-deps-XXXXXX.nix")}")
mkdir -p "$tmp/nuget_pkgs"
storeSrc="@storeSrc@"
src="$tmp/src"
cp -rT "$storeSrc" "$src"
chmod -R +w "$src"
cd "$src"
echo "Restoring project..."
rids=("@rids@")
for rid in "${rids[@]}"; do
(( ${#projectFiles[@]} == 0 )) && dotnetRestore "" "$rid"
for project in "${projectFiles[@]-}" "${testProjectFiles[@]-}"; do
dotnetRestore "$project" "$rid"
done
done
echo "Successfully restored project"
echo "Writing lockfile..."
echo -e "# This file was automatically generated by passthru.fetch-deps.\n# Please don't edit it manually, your changes might get overwritten!\n" > "$depsFile"
nuget-to-nix "$tmp/nuget_pkgs" "@packages@" >> "$depsFile"
echo "Successfully wrote lockfile to $depsFile"