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