diff --git a/ConsumePlugin/ConsumePlugin.fsproj b/ConsumePlugin/ConsumePlugin.fsproj
index b7f8230..7b45ed4 100644
--- a/ConsumePlugin/ConsumePlugin.fsproj
+++ b/ConsumePlugin/ConsumePlugin.fsproj
@@ -17,6 +17,10 @@
JsonRecord.fs
+
+
+
+ PureGymDto.fs
runmyriad.sh
diff --git a/ConsumePlugin/GeneratedPureGymDto.fs b/ConsumePlugin/GeneratedPureGymDto.fs
new file mode 100644
index 0000000..192b7dd
--- /dev/null
+++ b/ConsumePlugin/GeneratedPureGymDto.fs
@@ -0,0 +1,350 @@
+//------------------------------------------------------------------------------
+// This code was generated by myriad.
+// Changes to this file will be lost when the code is regenerated.
+//------------------------------------------------------------------------------
+
+namespace PureGym
+
+/// Module containing JSON parsing methods for the GymOpeningHours type
+[]
+[]
+module GymOpeningHours =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymOpeningHours =
+ let OpeningHours =
+ node.["openingHours"].AsArray ()
+ |> Seq.map (fun elt -> elt.AsValue().GetValue ())
+ |> List.ofSeq
+
+ let IsAlwaysOpen = node.["isAlwaysOpen"].AsValue().GetValue ()
+
+ {
+ IsAlwaysOpen = IsAlwaysOpen
+ OpeningHours = OpeningHours
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the GymAccessOptions type
+[]
+[]
+module GymAccessOptions =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymAccessOptions =
+ let QrCodeAccess = node.["qrCodeAccess"].AsValue().GetValue ()
+ let PinAccess = node.["pinAccess"].AsValue().GetValue ()
+
+ {
+ PinAccess = PinAccess
+ QrCodeAccess = QrCodeAccess
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the GymLocation type
+[]
+[]
+module GymLocation =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymLocation =
+ let Latitude =
+ try
+ node.["latitude"].AsValue().GetValue ()
+ with :? System.InvalidOperationException as exc ->
+ if exc.Message.Contains "cannot be converted to" then
+ if
+ System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString
+ then
+ node.["latitude"].AsValue().GetValue () |> System.Double.Parse
+ else
+ reraise ()
+ else
+ reraise ()
+
+ let Longitude =
+ try
+ node.["longitude"].AsValue().GetValue ()
+ with :? System.InvalidOperationException as exc ->
+ if exc.Message.Contains "cannot be converted to" then
+ if
+ System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString
+ then
+ node.["longitude"].AsValue().GetValue () |> System.Double.Parse
+ else
+ reraise ()
+ else
+ reraise ()
+
+ {
+ Longitude = Longitude
+ Latitude = Latitude
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the GymAddress type
+[]
+[]
+module GymAddress =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymAddress =
+ let Postcode = node.["postcode"].AsValue().GetValue ()
+
+ let County =
+ match node.["county"] with
+ | null -> None
+ | v -> v.AsValue().GetValue () |> Some
+
+ let Town = node.["town"].AsValue().GetValue ()
+
+ let AddressLine3 =
+ match node.["addressLine3"] with
+ | null -> None
+ | v -> v.AsValue().GetValue () |> Some
+
+ let AddressLine2 =
+ match node.["addressLine2"] with
+ | null -> None
+ | v -> v.AsValue().GetValue () |> Some
+
+ let AddressLine1 = node.["addressLine1"].AsValue().GetValue ()
+
+ {
+ AddressLine1 = AddressLine1
+ AddressLine2 = AddressLine2
+ AddressLine3 = AddressLine3
+ Town = Town
+ County = County
+ Postcode = Postcode
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the Gym type
+[]
+[]
+module Gym =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : Gym =
+ let ReopenDate = node.["reopenDate"].AsValue().GetValue ()
+ let TimeZone = node.["timeZone"].AsValue().GetValue ()
+ let Location = GymLocation.jsonParse node.["location"]
+ let AccessOptions = GymAccessOptions.jsonParse node.["accessOptions"]
+ let GymOpeningHours = GymOpeningHours.jsonParse node.["gymOpeningHours"]
+ let EmailAddress = node.["emailAddress"].AsValue().GetValue ()
+ let PhoneNumber = node.["phoneNumber"].AsValue().GetValue ()
+ let Address = GymAddress.jsonParse node.["address"]
+ let Status = node.["status"].AsValue().GetValue ()
+ let Id = node.["id"].AsValue().GetValue ()
+ let Name = node.["name"].AsValue().GetValue ()
+
+ {
+ Name = Name
+ Id = Id
+ Status = Status
+ Address = Address
+ PhoneNumber = PhoneNumber
+ EmailAddress = EmailAddress
+ GymOpeningHours = GymOpeningHours
+ AccessOptions = AccessOptions
+ Location = Location
+ TimeZone = TimeZone
+ ReopenDate = ReopenDate
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the Member type
+[]
+[]
+module Member =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : Member =
+ let MemberStatus = node.["memberStatus"].AsValue().GetValue ()
+ let SuspendedReason = node.["suspendedReason"].AsValue().GetValue ()
+ let MembershipLevel = node.["membershipLevel"].AsValue().GetValue ()
+ let MembershipName = node.["membershipName"].AsValue().GetValue ()
+ let Postcode = node.["postCode"].AsValue().GetValue ()
+ let MobileNumber = node.["mobileNumber"].AsValue().GetValue ()
+
+ let DateOfBirth =
+ node.["dateofBirth"].AsValue().GetValue () |> System.DateOnly.Parse
+
+ let GymAccessPin = node.["gymAccessPin"].AsValue().GetValue ()
+ let EmailAddress = node.["emailAddress"].AsValue().GetValue ()
+ let HomeGymName = node.["homeGymName"].AsValue().GetValue ()
+ let HomeGymId = node.["homeGymId"].AsValue().GetValue ()
+ let LastName = node.["lastName"].AsValue().GetValue ()
+ let FirstName = node.["firstName"].AsValue().GetValue ()
+ let CompoundMemberId = node.["compoundMemberId"].AsValue().GetValue ()
+ let Id = node.["id"].AsValue().GetValue ()
+
+ {
+ Id = Id
+ CompoundMemberId = CompoundMemberId
+ FirstName = FirstName
+ LastName = LastName
+ HomeGymId = HomeGymId
+ HomeGymName = HomeGymName
+ EmailAddress = EmailAddress
+ GymAccessPin = GymAccessPin
+ DateOfBirth = DateOfBirth
+ MobileNumber = MobileNumber
+ Postcode = Postcode
+ MembershipName = MembershipName
+ MembershipLevel = MembershipLevel
+ SuspendedReason = SuspendedReason
+ MemberStatus = MemberStatus
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the GymAttendance type
+[]
+[]
+module GymAttendance =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymAttendance =
+ let MaximumCapacity = node.["maximumCapacity"].AsValue().GetValue ()
+
+ let LastRefreshedPeopleInClasses =
+ node.["lastRefreshedPeopleInClasses"].AsValue().GetValue ()
+ |> System.DateTime.Parse
+
+ let LastRefreshed =
+ node.["lastRefreshed"].AsValue().GetValue () |> System.DateTime.Parse
+
+ let AttendanceTime =
+ node.["attendanceTime"].AsValue().GetValue () |> System.DateTime.Parse
+
+ let IsApproximate = node.["isApproximate"].AsValue().GetValue ()
+
+ let TotalPeopleSuffix =
+ match node.["totalPeopleSuffix"] with
+ | null -> None
+ | v -> v.AsValue().GetValue () |> Some
+
+ let TotalPeopleInClasses = node.["totalPeopleInClasses"].AsValue().GetValue ()
+ let TotalPeopleInGym = node.["totalPeopleInGym"].AsValue().GetValue ()
+ let Description = node.["description"].AsValue().GetValue ()
+
+ {
+ Description = Description
+ TotalPeopleInGym = TotalPeopleInGym
+ TotalPeopleInClasses = TotalPeopleInClasses
+ TotalPeopleSuffix = TotalPeopleSuffix
+ IsApproximate = IsApproximate
+ AttendanceTime = AttendanceTime
+ LastRefreshed = LastRefreshed
+ LastRefreshedPeopleInClasses = LastRefreshedPeopleInClasses
+ MaximumCapacity = MaximumCapacity
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the MemberActivityDto type
+[]
+[]
+module MemberActivityDto =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : MemberActivityDto =
+ let LastRefreshed =
+ node.["lastRefreshed"].AsValue().GetValue () |> System.DateTime.Parse
+
+ let IsEstimated = node.["isEstimated"].AsValue().GetValue ()
+ let TotalClasses = node.["totalClasses"].AsValue().GetValue ()
+ let TotalVisits = node.["totalVisits"].AsValue().GetValue ()
+ let AverageDuration = node.["averageDuration"].AsValue().GetValue ()
+ let TotalDuration = node.["totalDuration"].AsValue().GetValue ()
+
+ {
+ TotalDuration = TotalDuration
+ AverageDuration = AverageDuration
+ TotalVisits = TotalVisits
+ TotalClasses = TotalClasses
+ IsEstimated = IsEstimated
+ LastRefreshed = LastRefreshed
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the SessionsAggregate type
+[]
+[]
+module SessionsAggregate =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : SessionsAggregate =
+ let Duration = node.["duration"].AsValue().GetValue ()
+ let Visits = node.["visits"].AsValue().GetValue ()
+ let Activities = node.["activities"].AsValue().GetValue ()
+
+ {
+ Activities = Activities
+ Visits = Visits
+ Duration = Duration
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the VisitGym type
+[]
+[]
+module VisitGym =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : VisitGym =
+ let Status = node.["status"].AsValue().GetValue ()
+ let Name = node.["name"].AsValue().GetValue ()
+ let Id = node.["id"].AsValue().GetValue ()
+
+ {
+ Id = Id
+ Name = Name
+ Status = Status
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the Visit type
+[]
+[]
+module Visit =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : Visit =
+ let Gym = VisitGym.jsonParse node.["gym"]
+ let Duration = node.["duration"].AsValue().GetValue ()
+
+ let StartTime =
+ node.["startTime"].AsValue().GetValue () |> System.DateTime.Parse
+
+ let IsDurationEstimated = node.["isDurationEstimated"].AsValue().GetValue ()
+
+ {
+ IsDurationEstimated = IsDurationEstimated
+ StartTime = StartTime
+ Duration = Duration
+ Gym = Gym
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the SessionsSummary type
+[]
+[]
+module SessionsSummary =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : SessionsSummary =
+ let ThisWeek = SessionsAggregate.jsonParse node.["thisWeek"]
+ let Total = SessionsAggregate.jsonParse node.["total"]
+
+ {
+ Total = Total
+ ThisWeek = ThisWeek
+ }
+namespace PureGym
+
+/// Module containing JSON parsing methods for the Sessions type
+[]
+[]
+module Sessions =
+ /// Parse from a JSON node.
+ let jsonParse (node : System.Text.Json.Nodes.JsonNode) : Sessions =
+ let Visits =
+ node.["visits"].AsArray ()
+ |> Seq.map (fun elt -> Visit.jsonParse elt)
+ |> List.ofSeq
+
+ let Summary = SessionsSummary.jsonParse node.["summary"]
+
+ {
+ Summary = Summary
+ Visits = Visits
+ }
diff --git a/ConsumePlugin/PureGymDto.fs b/ConsumePlugin/PureGymDto.fs
new file mode 100644
index 0000000..79bad17
--- /dev/null
+++ b/ConsumePlugin/PureGymDto.fs
@@ -0,0 +1,317 @@
+// Copied from https://gitea.patrickstevens.co.uk/patrick/puregym-unofficial-dotnet/src/commit/2741c5e36cf0bdb203b12b78a8062e25af9d89c7/PureGym/Api.fs
+
+namespace PureGym
+
+open System
+open System.Text.Json.Serialization
+
+/// 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
+ }
+
+/// 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"
+ []
+ AddressLine1 : string
+ /// E.g. "Units 4, 4A, 5 And 5A"
+ AddressLine2 : string option
+ /// E.g. "Kennington Park"
+ AddressLine3 : string option
+ /// E.g. "LONDON"
+ []
+ Town : string
+ County : string option
+ /// E.g. "SW9 6DE"
+ []
+ Postcode : string
+ }
+
+ /// Human-readable statement of the address
+ override this.ToString () =
+ [
+ yield Some this.AddressLine1
+ yield this.AddressLine2
+ yield this.AddressLine3
+ match this.County with
+ | None -> yield Some $"%s{this.Town} %s{this.Postcode}"
+ | Some 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"
+ []
+ Name : string
+ /// This gym's ID in the PureGym system, e.g. 19
+ []
+ Id : int
+ /// I don't know what this status is. Please tell me if you know!
+ []
+ Status : int
+ /// Postal address of this gym
+ []
+ Address : GymAddress
+ /// Phone number of this gym, e.g. "+44 1234 567890"
+ []
+ PhoneNumber : string
+ /// Contact email address for this gym's staff
+ []
+ EmailAddress : string
+ /// When this gym is open
+ []
+ GymOpeningHours : GymOpeningHours
+ /// How a human can physically authenticate when they physically enter this gym
+ []
+ AccessOptions : GymAccessOptions
+ /// Where this gym is physically located
+ []
+ Location : GymLocation
+ /// The IANA time zone this gym observes, e.g. "Europe/London"
+ []
+ 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 this.GymOpeningHours}
+%s{string 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 =
+ {
+ /// This appears always to be just equal to TotalPeopleInGym, but a string.
+ []
+ Description : string
+ /// How many people are in the gym as of this statistics snapshot
+ []
+ TotalPeopleInGym : int
+ /// How many people are in classes at the gym as of this statistics snapshot
+ []
+ TotalPeopleInClasses : int
+ /// E.g. " or fewer"
+ TotalPeopleSuffix : string option
+ []
+ 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
+ }
+
+/// The visit statistics for a particular human to a particular gym.
+/// The semantics of this class are basically unknown.
+type MemberActivityThisMonth =
+ {
+ /// How many minutes, including classes, have been logged so far this month
+ TotalDurationMinutes : int
+ /// How long, in minutes, each visit has been on average this month
+ AverageDurationMinutes : int
+ /// How many visits have been made this month, excluding classes
+ TotalVisits : int
+ /// How many classes have been attended this month
+ TotalClasses : int
+ /// Whether this block of statistics is estimated rather than exact
+ IsEstimated : bool
+ /// When this data was constructed
+ LastRefreshed : DateTime
+ }
+
+/// Don't use this type. It's public because System.Text.Json can't do private types.
+[]
+type MemberActivityDto =
+ {
+ []
+ TotalDuration : int
+ []
+ AverageDuration : int
+ []
+ TotalVisits : int
+ []
+ TotalClasses : int
+ []
+ IsEstimated : bool
+ []
+ LastRefreshed : DateTime
+ }
+
+ member this.ToMemberActivity () =
+ {
+ TotalDurationMinutes = this.TotalDuration
+ AverageDurationMinutes = this.AverageDuration
+ TotalVisits = this.TotalVisits
+ TotalClasses = this.TotalClasses
+ IsEstimated = this.IsEstimated
+ LastRefreshed = this.LastRefreshed
+ }
+
+[]
+type SessionsAggregate =
+ {
+ /// Number of gym "activities" within some query-defined time period; presumably this is like classes?
+ /// It's always 0 for me.
+ Activities : int
+ /// Number of visits to the gym within some query-defined time period.
+ Visits : int
+ /// In minutes: total time spent in gym during the query-defined time period.
+ Duration : int
+ }
+
+/// The DTO for gym info returned from the Sessions endpoint.
+[]
+type VisitGym =
+ {
+ // Omitting Location, GymAccess, ContactInfo, TimeZone because these were all null for me
+ /// The PureGym ID of this gym, e.g. 19
+ Id : int
+ /// E.g. "London Oval", the canonical name of this gym
+ Name : string
+ /// For some reason this always seems to be "Blocked"
+ Status : string
+ }
+
+/// Summary of a single visit to a gym.
+[]
+type Visit =
+ {
+ // Omitted Name because it always was null for me
+ /// Whether the Duration field is estimated.
+ IsDurationEstimated : bool
+ /// When the visit began.
+ StartTime : DateTime
+ /// In minutes.
+ Duration : int
+ /// Which gym was visited
+ Gym : VisitGym
+ }
+
+ /// Human-readable non-round-trip representation.
+ override this.ToString () =
+ let startTime = this.StartTime.ToString "yyyy-MM-dd HH:mm"
+ $"%s{this.Gym.Name}: %s{startTime} (%i{this.Duration} minutes)"
+
+/// Aggregate statistics for gym visits across a time period.
+[]
+type SessionsSummary =
+ {
+ /// Aggregate stats for gym visits within the query-dependent time period.
+ Total : SessionsAggregate
+ /// Aggregate stats for gym visits "this week", whatever that means to PureGym.
+ ThisWeek : SessionsAggregate
+ }
+
+ /// Human-readable non-round-trip representation.
+ override this.ToString () =
+ $"%i{this.Total.Visits} visits, totalling %i{this.Total.Duration} minutes"
+
+[]
+type Sessions =
+ {
+ Summary : SessionsSummary
+ Visits : Visit list
+ }
+
+ /// Human-readable non-round-trip representation.
+ override this.ToString () =
+ let summary = string this.Summary
+ let visits = this.Visits |> Seq.map string |> String.concat "\n"
+
+ $"%s{summary}\n%s{visits}"
diff --git a/MyriadPlugin.Test/MyriadPlugin.Test.fsproj b/MyriadPlugin.Test/MyriadPlugin.Test.fsproj
index 0679a37..6a6e9c1 100644
--- a/MyriadPlugin.Test/MyriadPlugin.Test.fsproj
+++ b/MyriadPlugin.Test/MyriadPlugin.Test.fsproj
@@ -8,6 +8,7 @@
+
diff --git a/MyriadPlugin.Test/TestPureGymJson.fs b/MyriadPlugin.Test/TestPureGymJson.fs
new file mode 100644
index 0000000..ce1f949
--- /dev/null
+++ b/MyriadPlugin.Test/TestPureGymJson.fs
@@ -0,0 +1,312 @@
+namespace PureGym.Test
+
+open System
+open System.Text.Json.Nodes
+open NUnit.Framework
+open FsUnitTyped
+open PureGym
+
+[]
+module TestPureGymJson =
+
+ let gymOpeningHoursCases =
+ [
+ """{"openingHours": [], "isAlwaysOpen": false}""",
+ {
+ GymOpeningHours.OpeningHours = []
+ IsAlwaysOpen = false
+ }
+ """{"openingHours": ["something"], "isAlwaysOpen": false}""",
+ {
+ GymOpeningHours.OpeningHours = [ "something" ]
+ IsAlwaysOpen = false
+ }
+ ]
+ |> List.map TestCaseData
+
+ []
+ let ``GymOpeningHours JSON parse`` (json : string, expected : GymOpeningHours) =
+ JsonNode.Parse json |> GymOpeningHours.jsonParse |> shouldEqual expected
+
+ let gymAccessOptionsCases =
+ List.allPairs [ true ; false ] [ true ; false ]
+ |> List.map (fun (a, b) ->
+ let s = sprintf """{"pinAccess": %b, "qrCodeAccess": %b}""" a b
+
+ s,
+ {
+ GymAccessOptions.PinAccess = a
+ QrCodeAccess = b
+ }
+ )
+ |> List.map TestCaseData
+
+ []
+ let ``GymAccessOptions JSON parse`` (json : string, expected : GymAccessOptions) =
+ JsonNode.Parse json |> GymAccessOptions.jsonParse |> shouldEqual expected
+
+ let gymLocationCases =
+ [
+ """{"latitude": 1.0, "longitude": 3.0}""",
+ {
+ GymLocation.Latitude = 1.0
+ Longitude = 3.0
+ }
+ ]
+ |> List.map TestCaseData
+
+ []
+ let ``GymLocation JSON parse`` (json : string, expected : GymLocation) =
+ JsonNode.Parse json |> GymLocation.jsonParse |> shouldEqual expected
+
+ let gymAddressCases =
+ [
+ """{"addressLine1": "", "postCode": "hi", "town": ""}""",
+ {
+ GymAddress.AddressLine1 = ""
+ AddressLine2 = None
+ AddressLine3 = None
+ County = None
+ Postcode = "hi"
+ Town = ""
+ }
+ """{"addressLine1": "", "addressLine2": null, "postCode": "hi", "town": ""}""",
+ {
+ GymAddress.AddressLine1 = ""
+ AddressLine2 = None
+ AddressLine3 = None
+ County = None
+ Postcode = "hi"
+ Town = ""
+ }
+ ]
+ |> List.map TestCaseData
+
+ []
+ let ``GymAddress JSON parse`` (json : string, expected : GymAddress) =
+ JsonNode.Parse (json, Nullable (JsonNodeOptions (PropertyNameCaseInsensitive = true)))
+ |> GymAddress.jsonParse
+ |> shouldEqual expected
+
+ let gymCases =
+ let ovalJson =
+ """{"name":"London Oval","id":19,"status":2,"address":{"addressLine1":"Canterbury Court","addressLine2":"Units 4, 4A, 5 And 5A","addressLine3":"Kennington Park","town":"LONDON","county":null,"postcode":"SW9 6DE"},"phoneNumber":"+44 3444770005","emailAddress":"info.londonoval@puregym.com","staffMembers":null,"gymOpeningHours":{"isAlwaysOpen":true,"openingHours":[]},"reasonsToJoin":null,"accessOptions":{"pinAccess":true,"qrCodeAccess":true},"virtualTourUrl":null,"personalTrainersUrl":null,"webViewUrl":null,"floorPlanUrl":null,"location":{"longitude":"-0.110252","latitude":"51.480401"},"timeZone":"Europe/London","reopenDate":"2021-04-12T00:00:00+01 Europe/London"}"""
+
+ let oval =
+ {
+ Gym.Name = "London Oval"
+ Id = 19
+ Status = 2
+ Address =
+ {
+ AddressLine1 = "Canterbury Court"
+ AddressLine2 = Some "Units 4, 4A, 5 And 5A"
+ AddressLine3 = Some "Kennington Park"
+ Town = "LONDON"
+ County = None
+ Postcode = "SW9 6DE"
+ }
+ PhoneNumber = "+44 3444770005"
+ EmailAddress = "info.londonoval@puregym.com"
+ GymOpeningHours =
+ {
+ IsAlwaysOpen = true
+ OpeningHours = []
+ }
+ AccessOptions =
+ {
+ PinAccess = true
+ QrCodeAccess = true
+ }
+ Location =
+ {
+ Longitude = -0.110252
+ Latitude = 51.480401
+ }
+ TimeZone = "Europe/London"
+ ReopenDate = "2021-04-12T00:00:00+01 Europe/London"
+ }
+
+ [ ovalJson, oval ] |> List.map TestCaseData
+
+ []
+ let ``Gym JSON parse`` (json : string, expected : Gym) =
+ JsonNode.Parse json |> Gym.jsonParse |> shouldEqual expected
+
+ let memberCases =
+ let me =
+ {
+ Id = 1234567
+ CompoundMemberId = "12A123456"
+ FirstName = "Patrick"
+ LastName = "Stevens"
+ HomeGymId = 19
+ HomeGymName = "London Oval"
+ EmailAddress = "someone@somewhere"
+ GymAccessPin = "00000000"
+ DateOfBirth = DateOnly (1994, 01, 02)
+ MobileNumber = "+44 1234567"
+ Postcode = "W1A 1AA"
+ MembershipName = "Corporate"
+ MembershipLevel = 12
+ SuspendedReason = 0
+ MemberStatus = 2
+ }
+
+ let meJson =
+ """{
+ "id": 1234567,
+ "compoundMemberId": "12A123456",
+ "firstName": "Patrick",
+ "lastName": "Stevens",
+ "homeGymId": 19,
+ "homeGymName": "London Oval",
+ "emailAddress": "someone@somewhere",
+ "gymAccessPin": "00000000",
+ "dateofBirth": "1994-01-02",
+ "mobileNumber": "+44 1234567",
+ "postCode": "W1A 1AA",
+ "membershipName": "Corporate",
+ "membershipLevel": 12,
+ "suspendedReason": 0,
+ "memberStatus": 2
+}"""
+
+ [ meJson, me ] |> List.map TestCaseData
+
+ []
+ let ``Member JSON parse`` (json : string, expected : Member) =
+ json |> JsonNode.Parse |> Member.jsonParse |> shouldEqual expected
+
+ let gymAttendanceCases =
+ let json =
+ """{
+ "description": "65",
+ "totalPeopleInGym": 65,
+ "totalPeopleInClasses": 2,
+ "totalPeopleSuffix": null,
+ "isApproximate": false,
+ "attendanceTime": "2023-12-27T18:54:09.5101697",
+ "lastRefreshed": "2023-12-27T18:54:09.5101697Z",
+ "lastRefreshedPeopleInClasses": "2023-12-27T18:50:26.0782286Z",
+ "maximumCapacity": 0
+}"""
+
+ let expected =
+ {
+ Description = "65"
+ TotalPeopleInGym = 65
+ TotalPeopleInClasses = 2
+ TotalPeopleSuffix = None
+ IsApproximate = false
+ AttendanceTime =
+ DateTime (2023, 12, 27, 18, 54, 09, 510, 169, DateTimeKind.Utc)
+ + TimeSpan.FromTicks 7L
+ LastRefreshed =
+ DateTime (2023, 12, 27, 18, 54, 09, 510, 169, DateTimeKind.Utc)
+ + TimeSpan.FromTicks 7L
+ LastRefreshedPeopleInClasses =
+ DateTime (2023, 12, 27, 18, 50, 26, 078, 228, DateTimeKind.Utc)
+ + TimeSpan.FromTicks 6L
+ MaximumCapacity = 0
+ }
+
+ [ json, expected ] |> List.map TestCaseData
+
+ []
+ let ``GymAttendance JSON parse`` (json : string, expected : GymAttendance) =
+ json |> JsonNode.Parse |> GymAttendance.jsonParse |> shouldEqual expected
+
+ let memberActivityDtoCases =
+ let json =
+ """{"totalDuration":2217,"averageDuration":48,"totalVisits":46,"totalClasses":0,"isEstimated":false,"lastRefreshed":"2023-12-27T19:00:56.0309892Z"}"""
+
+ let value =
+ {
+ TotalDuration = 2217
+ AverageDuration = 48
+ TotalVisits = 46
+ TotalClasses = 0
+ IsEstimated = false
+ LastRefreshed =
+ DateTime (2023, 12, 27, 19, 00, 56, 030, 989, DateTimeKind.Utc)
+ + TimeSpan.FromTicks 2L
+ }
+
+ [ json, value ] |> List.map TestCaseData
+
+ []
+ let ``MemberActivityDto JSON parse`` (json : string, expected : MemberActivityDto) =
+ json |> JsonNode.Parse |> MemberActivityDto.jsonParse |> shouldEqual expected
+
+ let sessionsCases =
+ let json =
+ """{
+ "Summary":{"Total":{"Activities":0,"Visits":10,"Duration":445},"ThisWeek":{"Activities":0,"Visits":0,"Duration":0}},
+ "Visits":[
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-21T10:12:00","Duration":50,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-20T12:05:00","Duration":80,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-17T19:37:00","Duration":46,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-16T12:19:00","Duration":37,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-15T11:14:00","Duration":47,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-13T10:30:00","Duration":36,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-10T16:18:00","Duration":32,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-05T22:36:00","Duration":40,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-03T17:59:00","Duration":48,"Name":null},
+ {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-01T21:41:00","Duration":29,"Name":null}],
+ "Activities":[]}
+"""
+
+ let singleVisit startTime duration =
+ {
+ IsDurationEstimated = false
+ Gym =
+ {
+ Id = 19
+ Name = "London Oval"
+ Status = "Blocked"
+ }
+ StartTime = startTime
+ Duration = duration
+ }
+
+ let expected =
+ {
+ Summary =
+ {
+ Total =
+ {
+ Activities = 0
+ Visits = 10
+ Duration = 445
+ }
+ ThisWeek =
+ {
+ Activities = 0
+ Visits = 0
+ Duration = 0
+ }
+ }
+ Visits =
+ [
+ singleVisit (DateTime (2023, 12, 21, 10, 12, 00)) 50
+ singleVisit (DateTime (2023, 12, 20, 12, 05, 00)) 80
+ singleVisit (DateTime (2023, 12, 17, 19, 37, 00)) 46
+ singleVisit (DateTime (2023, 12, 16, 12, 19, 00)) 37
+ singleVisit (DateTime (2023, 12, 15, 11, 14, 00)) 47
+ singleVisit (DateTime (2023, 12, 13, 10, 30, 00)) 36
+ singleVisit (DateTime (2023, 12, 10, 16, 18, 00)) 32
+ singleVisit (DateTime (2023, 12, 05, 22, 36, 00)) 40
+ singleVisit (DateTime (2023, 12, 03, 17, 59, 00)) 48
+ singleVisit (DateTime (2023, 12, 01, 21, 41, 00)) 29
+ ]
+ }
+
+ [ json, expected ] |> List.map TestCaseData
+
+ []
+ let ``Sessions JSON parse`` (json : string, expected : Sessions) =
+ json
+ |> fun o -> JsonNode.Parse (o, Nullable (JsonNodeOptions (PropertyNameCaseInsensitive = true)))
+ |> Sessions.jsonParse
+ |> shouldEqual expected