mirror of
https://github.com/Smaug123/WoofWare.Myriad
synced 2025-10-05 20:18:43 +00:00
NuGet Pack (#15)
This commit is contained in:
@@ -7,6 +7,12 @@
|
|||||||
"commands": [
|
"commands": [
|
||||||
"fantomas"
|
"fantomas"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"fsharp-analyzers": {
|
||||||
|
"version": "0.22.0",
|
||||||
|
"commands": [
|
||||||
|
"fsharp-analyzers"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
58
.github/workflows/dotnet.yaml
vendored
58
.github/workflows/dotnet.yaml
vendored
@@ -39,6 +39,25 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}}
|
run: nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}}
|
||||||
|
|
||||||
|
analyzers:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v24
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Prepare analyzers
|
||||||
|
run: nix develop --command dotnet restore analyzers/analyzers.fsproj
|
||||||
|
- name: Run analyzers
|
||||||
|
run: nix run .#fsharp-analyzers -- --project ./WoofWare.Myriad.Plugins/WoofWare.Myriad.Plugins.fsproj --analyzers-path ./.analyzerpackages/g-research.fsharp.analyzers/0.6.0/ --verbosity detailed --report ./analysis.sarif --treat-as-error GRA-STRING-001 GRA-STRING-002 GRA-STRING-003 GRA-UNIONCASE-001 GRA-INTERPOLATED-001 GRA-TYPE-ANNOTATE-001 GRA-VIRTUALCALL-001 GRA-IMMUTABLECOLLECTIONEQUALITY-001 GRA-JSONOPTS-001 GRA-LOGARGFUNCFULLAPP-001
|
||||||
|
- name: Upload SARIF file
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
with:
|
||||||
|
sarif_file: analysis.sarif
|
||||||
|
|
||||||
build-nix:
|
build-nix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -89,7 +108,7 @@ jobs:
|
|||||||
extra_nix_config: |
|
extra_nix_config: |
|
||||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Run link checker
|
- name: Run link checker
|
||||||
run: nix develop --command markdown-link-check README.md
|
run: nix develop --command markdown-link-check README.md CONTRIBUTING.md
|
||||||
|
|
||||||
flake-check:
|
flake-check:
|
||||||
name: Check flake
|
name: Check flake
|
||||||
@@ -104,8 +123,43 @@ jobs:
|
|||||||
- name: Flake check
|
- name: Flake check
|
||||||
run: nix flake check
|
run: nix flake check
|
||||||
|
|
||||||
|
nuget-pack:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v24
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: nix develop --command dotnet restore
|
||||||
|
- name: Build
|
||||||
|
run: nix develop --command dotnet build --no-restore --configuration Release
|
||||||
|
- name: Pack
|
||||||
|
run: nix develop --command dotnet pack --configuration Release
|
||||||
|
- name: Upload NuGet artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nuget-package
|
||||||
|
path: WoofWare.Myriad.Plugins/bin/Release/WoofWare.Myriad.Plugins.*.nupkg
|
||||||
|
|
||||||
|
expected-pack:
|
||||||
|
needs: [nuget-pack]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download NuGet artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nuget-package
|
||||||
|
- name: Check NuGet contents
|
||||||
|
# Verify that there is exactly one nupkg in the artifact that would be NuGet published
|
||||||
|
run: if [[ $(find . -maxdepth 1 -name 'WoofWare.Myriad.Plugins.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
|
||||||
|
|
||||||
all-required-checks-complete:
|
all-required-checks-complete:
|
||||||
needs: [check-dotnet-format, check-nix-format, build, build-nix, linkcheck, flake-check]
|
needs: [check-dotnet-format, check-nix-format, build, build-nix, linkcheck, flake-check, analyzers, nuget-pack, expected-pack]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo "All required checks complete."
|
- run: echo "All required checks complete."
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ riderModule.iml
|
|||||||
*.sln.DotSettings.user
|
*.sln.DotSettings.user
|
||||||
.DS_Store
|
.DS_Store
|
||||||
result
|
result
|
||||||
|
.analyzerpackages/
|
||||||
|
analysis.sarif
|
||||||
|
55
CONTRIBUTING.md
Normal file
55
CONTRIBUTING.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
The main project fork lives [on GitHub](https://github.com/Smaug123/WoofWare.Myriad).
|
||||||
|
|
||||||
|
Contributions are welcome, but I am generally very opinionated about both style and content.
|
||||||
|
I also can't commit to looking at anything in a particularly timely manner (or at all, though I expect I will try).
|
||||||
|
|
||||||
|
In general my aesthetics lead me to accept correctness fixes much more readily than other changes.
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
Please raise bug reports and feature requests as Issues on [the main GitHub project](https://github.com/Smaug123/WoofWare.Myriad/issues).
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
Before embarking on a large change, I strongly recommend checking via a GitHub Issue first that I'm likely to accept it.
|
||||||
|
|
||||||
|
You may find that the following guidelines will help you produce a change that I accept:
|
||||||
|
|
||||||
|
* Keep your change as small and focused as is practical.
|
||||||
|
* Ensure that your change is thoroughly tested.
|
||||||
|
* Document any choices you make which are not immediately obvious.
|
||||||
|
* Explain why your change is necessary or desirable.
|
||||||
|
|
||||||
|
## On your first checkout
|
||||||
|
|
||||||
|
There are pull request checks on this repo, enforcing [Fantomas](https://github.com/fsprojects/fantomas/)-compliant formatting according to the [G-Research style guidelines](https://github.com/G-Research/fsharp-formatting-conventions/).
|
||||||
|
After checking out the repo, you may wish to add a pre-push hook to ensure locally that formatting is complete, rather than having to wait for the CI checks to tell you that you haven't formatted your code.
|
||||||
|
Consider performing the following command to set this up in the repo:
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath hooks/
|
||||||
|
```
|
||||||
|
Before your first push (but only once), you will need to install the [.NET local tools](https://docs.microsoft.com/en-us/dotnet/core/tools/local-tools-how-to-use) which form part of the pre-push hook:
|
||||||
|
```bash
|
||||||
|
dotnet tool restore
|
||||||
|
```
|
||||||
|
|
||||||
|
In future, some commits (such as big-bang formatting commits) may be recorded for convenience in `.git-blame-ignore-revs`.
|
||||||
|
Consider performing the following command to have `git blame` ignore these commits, when we ever create any:
|
||||||
|
```bash
|
||||||
|
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
I try to keep this repository's dependencies as few as possible, because (for example) any consumer of the source generator will also consume this project via the attributes.
|
||||||
|
When adding dependencies, you will need to `nix run .#fetchDeps` to obtain a new copy of [the dependency lockfile](./nix/deps.nix).
|
||||||
|
|
||||||
|
## Branch strategy
|
||||||
|
|
||||||
|
Releases are made from the `main` branch.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed with the MIT license, a copy of which you can find at the repository root.
|
@@ -5,217 +5,109 @@ namespace PureGym
|
|||||||
open System
|
open System
|
||||||
open System.Text.Json.Serialization
|
open System.Text.Json.Serialization
|
||||||
|
|
||||||
/// Describes the opening hours of a given gym.
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type GymOpeningHours =
|
type GymOpeningHours =
|
||||||
{
|
{
|
||||||
/// If this is true, there should be no OpeningHours (but nothing enforces that).
|
|
||||||
IsAlwaysOpen : bool
|
IsAlwaysOpen : bool
|
||||||
/// This is a pretty unstructured list, which is in general not really parseable: it's human-readable only.
|
|
||||||
OpeningHours : string list
|
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
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type GymAccessOptions =
|
type GymAccessOptions =
|
||||||
{
|
{
|
||||||
/// This gym has PIN entry pads
|
|
||||||
PinAccess : bool
|
PinAccess : bool
|
||||||
/// This gym has a QR code scanner. QR codes can be generated with the PureGym app.
|
|
||||||
QrCodeAccess : bool
|
QrCodeAccess : bool
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a gym is on the Earth
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type GymLocation =
|
type GymLocation =
|
||||||
{
|
{
|
||||||
/// Measured in degrees
|
|
||||||
[<JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)>]
|
[<JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)>]
|
||||||
Longitude : float
|
Longitude : float
|
||||||
/// Measured in degrees
|
|
||||||
[<JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)>]
|
[<JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)>]
|
||||||
Latitude : float
|
Latitude : float
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The postal address of a gym
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type GymAddress =
|
type GymAddress =
|
||||||
{
|
{
|
||||||
/// E.g. "Canterbury Court"
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
AddressLine1 : string
|
AddressLine1 : string
|
||||||
/// E.g. "Units 4, 4A, 5 And 5A"
|
|
||||||
AddressLine2 : string option
|
AddressLine2 : string option
|
||||||
/// E.g. "Kennington Park"
|
|
||||||
AddressLine3 : string option
|
AddressLine3 : string option
|
||||||
/// E.g. "LONDON"
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
Town : string
|
Town : string
|
||||||
County : string option
|
County : string option
|
||||||
/// E.g. "SW9 6DE"
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
Postcode : string
|
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
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type 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>]
|
[<JsonRequired>]
|
||||||
Name : string
|
Name : string
|
||||||
/// This gym's ID in the PureGym system, e.g. 19
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
Id : int
|
Id : int
|
||||||
/// I don't know what this status is. Please tell me if you know!
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
Status : int
|
Status : int
|
||||||
/// Postal address of this gym
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
Address : GymAddress
|
Address : GymAddress
|
||||||
/// Phone number of this gym, e.g. "+44 1234 567890"
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
PhoneNumber : string
|
PhoneNumber : string
|
||||||
/// Contact email address for this gym's staff
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
EmailAddress : string
|
EmailAddress : string
|
||||||
/// When this gym is open
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
GymOpeningHours : GymOpeningHours
|
GymOpeningHours : GymOpeningHours
|
||||||
/// How a human can physically authenticate when they physically enter this gym
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
AccessOptions : GymAccessOptions
|
AccessOptions : GymAccessOptions
|
||||||
/// Where this gym is physically located
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
Location : GymLocation
|
Location : GymLocation
|
||||||
/// The IANA time zone this gym observes, e.g. "Europe/London"
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
TimeZone : string
|
TimeZone : string
|
||||||
/// This is a date-time in the format yyyy-MM-ddTHH:mm:ss+01 Europe/London
|
|
||||||
ReopenDate : string
|
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
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type Member =
|
type Member =
|
||||||
{
|
{
|
||||||
/// This member's ID. This is a fairly large number.
|
|
||||||
Id : int
|
Id : int
|
||||||
/// No idea what this is - please tell me if you know!
|
|
||||||
CompoundMemberId : string
|
CompoundMemberId : string
|
||||||
/// First name, e.g. "Patrick"
|
|
||||||
FirstName : string
|
FirstName : string
|
||||||
/// Last name, e.g. "Stevens"
|
|
||||||
LastName : string
|
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
|
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
|
HomeGymName : string
|
||||||
/// This user's email address
|
|
||||||
EmailAddress : string
|
EmailAddress : string
|
||||||
/// This user's gym access pin, probably 8 digits
|
|
||||||
GymAccessPin : string
|
GymAccessPin : string
|
||||||
/// This user's recorded date of birth
|
|
||||||
[<JsonPropertyName "dateofBirth">]
|
[<JsonPropertyName "dateofBirth">]
|
||||||
DateOfBirth : DateOnly
|
DateOfBirth : DateOnly
|
||||||
/// This user's phone number, human-readable
|
|
||||||
MobileNumber : string
|
MobileNumber : string
|
||||||
/// This user's registered home postcode
|
|
||||||
[<JsonPropertyName "postCode">]
|
[<JsonPropertyName "postCode">]
|
||||||
Postcode : string
|
Postcode : string
|
||||||
/// E.g. "Corporate"
|
|
||||||
MembershipName : string
|
MembershipName : string
|
||||||
MembershipLevel : int
|
MembershipLevel : int
|
||||||
SuspendedReason : int
|
SuspendedReason : int
|
||||||
MemberStatus : int
|
MemberStatus : int
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Statistics for how many people are currently at a gym
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type GymAttendance =
|
type GymAttendance =
|
||||||
{
|
{
|
||||||
/// This appears always to be just equal to TotalPeopleInGym, but a string.
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
Description : string
|
Description : string
|
||||||
/// How many people are in the gym as of this statistics snapshot
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
TotalPeopleInGym : int
|
TotalPeopleInGym : int
|
||||||
/// How many people are in classes at the gym as of this statistics snapshot
|
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
TotalPeopleInClasses : int
|
TotalPeopleInClasses : int
|
||||||
/// E.g. " or fewer"
|
|
||||||
TotalPeopleSuffix : string option
|
TotalPeopleSuffix : string option
|
||||||
[<JsonRequired>]
|
[<JsonRequired>]
|
||||||
IsApproximate : bool
|
IsApproximate : bool
|
||||||
/// When the query was received (I think)
|
|
||||||
AttendanceTime : DateTime
|
AttendanceTime : DateTime
|
||||||
/// When the "total people in gym" snapshot was taken that is reported here
|
|
||||||
LastRefreshed : DateTime
|
LastRefreshed : DateTime
|
||||||
/// When the "number of people in classes" snapshot was taken that is reported here
|
|
||||||
LastRefreshedPeopleInClasses : DateTime
|
LastRefreshedPeopleInClasses : DateTime
|
||||||
/// Maximum capacity of the gym, or 0 if no listed capacity
|
|
||||||
MaximumCapacity : int
|
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.
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type MemberActivityDto =
|
type MemberActivityDto =
|
||||||
{
|
{
|
||||||
@@ -233,85 +125,41 @@ type MemberActivityDto =
|
|||||||
LastRefreshed : DateTime
|
LastRefreshed : DateTime
|
||||||
}
|
}
|
||||||
|
|
||||||
member this.ToMemberActivity () =
|
|
||||||
{
|
|
||||||
TotalDurationMinutes = this.TotalDuration
|
|
||||||
AverageDurationMinutes = this.AverageDuration
|
|
||||||
TotalVisits = this.TotalVisits
|
|
||||||
TotalClasses = this.TotalClasses
|
|
||||||
IsEstimated = this.IsEstimated
|
|
||||||
LastRefreshed = this.LastRefreshed
|
|
||||||
}
|
|
||||||
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type SessionsAggregate =
|
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
|
Activities : int
|
||||||
/// Number of visits to the gym within some query-defined time period.
|
|
||||||
Visits : int
|
Visits : int
|
||||||
/// In minutes: total time spent in gym during the query-defined time period.
|
|
||||||
Duration : int
|
Duration : int
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The DTO for gym info returned from the Sessions endpoint.
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type VisitGym =
|
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
|
Id : int
|
||||||
/// E.g. "London Oval", the canonical name of this gym
|
|
||||||
Name : string
|
Name : string
|
||||||
/// For some reason this always seems to be "Blocked"
|
|
||||||
Status : string
|
Status : string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Summary of a single visit to a gym.
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type Visit =
|
type Visit =
|
||||||
{
|
{
|
||||||
// Omitted Name because it always was null for me
|
|
||||||
/// Whether the Duration field is estimated.
|
|
||||||
IsDurationEstimated : bool
|
IsDurationEstimated : bool
|
||||||
/// When the visit began.
|
|
||||||
StartTime : DateTime
|
StartTime : DateTime
|
||||||
/// In minutes.
|
|
||||||
Duration : int
|
Duration : int
|
||||||
/// Which gym was visited
|
|
||||||
Gym : VisitGym
|
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.
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type SessionsSummary =
|
type SessionsSummary =
|
||||||
{
|
{
|
||||||
/// Aggregate stats for gym visits within the query-dependent time period.
|
|
||||||
Total : SessionsAggregate
|
Total : SessionsAggregate
|
||||||
/// Aggregate stats for gym visits "this week", whatever that means to PureGym.
|
|
||||||
ThisWeek : SessionsAggregate
|
ThisWeek : SessionsAggregate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Human-readable non-round-trip representation.
|
|
||||||
override this.ToString () =
|
|
||||||
$"%i{this.Total.Visits} visits, totalling %i{this.Total.Duration} minutes"
|
|
||||||
|
|
||||||
[<WoofWare.Myriad.Plugins.JsonParse>]
|
[<WoofWare.Myriad.Plugins.JsonParse>]
|
||||||
type Sessions =
|
type Sessions =
|
||||||
{
|
{
|
||||||
Summary : SessionsSummary
|
Summary : SessionsSummary
|
||||||
Visits : Visit list
|
Visits : Visit list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Human-readable non-round-trip representation.
|
|
||||||
override this.ToString () =
|
|
||||||
let summary = string<SessionsSummary> this.Summary
|
|
||||||
let visits = this.Visits |> Seq.map string<Visit> |> String.concat "\n"
|
|
||||||
|
|
||||||
$"%s{summary}\n%s{visits}"
|
|
||||||
|
@@ -7,10 +7,22 @@
|
|||||||
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
|
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<WarnOn>FS3559</WarnOn>
|
<WarnOn>FS3559</WarnOn>
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.128" PrivateAssets="all" />
|
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.128" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
<SourceLinkGitHubHost Include="github.com" ContentUrl="https://raw.githubusercontent.com" />
|
<SourceLinkGitHubHost Include="github.com" ContentUrl="https://raw.githubusercontent.com" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<!--
|
||||||
|
SourceLink doesn't support F# deterministic builds out of the box,
|
||||||
|
so tell SourceLink that our source root is going to be remapped.
|
||||||
|
-->
|
||||||
|
<Target Name="MapSourceRoot" BeforeTargets="_GenerateSourceLinkFile" Condition="'$(SourceRootMappedPathsFeatureSupported)' != 'true'">
|
||||||
|
<ItemGroup>
|
||||||
|
<SourceRoot Update="@(SourceRoot)">
|
||||||
|
<MappedPath>Z:\CheckoutRoot\WoofWare.Myriad\</MappedPath>
|
||||||
|
</SourceRoot>
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
98
README.md
98
README.md
@@ -1,34 +1,13 @@
|
|||||||
# fsharp-arguments
|
# WoofWare.Myriad.Plugins
|
||||||
|
|
||||||
Some helpers in [Myriad](https://github.com/MoiraeSoftware/myriad/) which might be useful for someone writing an argument parser.
|
Some helpers in [Myriad](https://github.com/MoiraeSoftware/myriad/) which might be useful.
|
||||||
|
|
||||||
## `RemoveOptions`
|
These are currently somewhat experimental, and I personally am their primary customer.
|
||||||
|
The `RemoveOptions` generator in particular is extremely half-baked.
|
||||||
|
|
||||||
Takes a record like this:
|
Currently implemented:
|
||||||
|
* `JsonParse` (to stamp out `jsonParse : JsonNode -> 'T` methods);
|
||||||
```fsharp
|
* `RemoveOptions` (to strip `option` modifiers from a type).
|
||||||
type Foo =
|
|
||||||
{
|
|
||||||
A : int option
|
|
||||||
B : string
|
|
||||||
C : float list
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
and stamps out a record like this:
|
|
||||||
|
|
||||||
```fsharp
|
|
||||||
[<RequireQualifiedAccess>]
|
|
||||||
module Foo =
|
|
||||||
type Short =
|
|
||||||
{
|
|
||||||
A : int
|
|
||||||
B : string
|
|
||||||
C : float list
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
(This is a proof of concept. It would be better to somehow disambiguate the module name.)
|
|
||||||
|
|
||||||
## `JsonParse`
|
## `JsonParse`
|
||||||
|
|
||||||
@@ -86,3 +65,66 @@ module JsonRecordType =
|
|||||||
let A = node.["a"].AsValue().GetValue<int>()
|
let A = node.["a"].AsValue().GetValue<int>()
|
||||||
{ A = A; B = B; C = C; D = D }
|
{ A = A; B = B; C = C; D = D }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### What's the point?
|
||||||
|
|
||||||
|
`System.Text.Json`, in a `PublishAot` context, relies on C# source generators.
|
||||||
|
The default reflection-heavy implementations have the necessary code trimmed away, and result in a runtime exception.
|
||||||
|
But C# source generators [are entirely unsupported in F#](https://github.com/dotnet/fsharp/issues/14300).
|
||||||
|
|
||||||
|
This Myriad generator expects you to use `System.Text.Json` to construct a `JsonNode`, and then the generator takes over to construct a strongly-typed object.
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
This source generator is enough for what I first wanted to use it for.
|
||||||
|
However, there is *far* more that could be done.
|
||||||
|
|
||||||
|
* Make it possible to give an exact format and cultural info in date and time parsing.
|
||||||
|
* Make it possible to reject parsing if extra fields are present.
|
||||||
|
* Rather than just throwing `NullReferenceException`, print out the field name that failed.
|
||||||
|
* Generally support all the `System.Text.Json` attributes.
|
||||||
|
|
||||||
|
## `RemoveOptions`
|
||||||
|
|
||||||
|
Takes a record like this:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
type Foo =
|
||||||
|
{
|
||||||
|
A : int option
|
||||||
|
B : string
|
||||||
|
C : float list
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and stamps out a record like this:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Foo =
|
||||||
|
type Short =
|
||||||
|
{
|
||||||
|
A : int
|
||||||
|
B : string
|
||||||
|
C : float list
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### What's the point?
|
||||||
|
|
||||||
|
The motivating example is argument parsing.
|
||||||
|
An argument parser naturally wants to express "the user did not supply this, so I will provide a default".
|
||||||
|
But it's not a very ergonomic experience for the programmer to deal with all these options,
|
||||||
|
so this Myriad generator stamps out a type *without* any options, and also stamps out an appropriate constructor function.
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
This generator is *far* from where I want it, because I haven't really spent any time on it.
|
||||||
|
* It really wants to be able to recurse into the types within the record, to strip options from them.
|
||||||
|
* It needs some sort of attribute to mark a field as *not* receiving this treatment.
|
||||||
|
* What do we do about discriminated unions?
|
||||||
|
|
||||||
|
# Detailed examples
|
||||||
|
|
||||||
|
See the tests.
|
||||||
|
For example, [PureGymDto.fs](./ConsumePlugin/PureGymDto.fs) is a real-world set of DTOs.
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
<Copyright>Copyright (c) Patrick Stevens 2023</Copyright>
|
<Copyright>Copyright (c) Patrick Stevens 2023</Copyright>
|
||||||
<Description>Provides some Myriad compile-time code generation plugins.</Description>
|
<Description>Provides some Myriad compile-time code generation plugins.</Description>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<RepositoryUrl>https://github.com/Smaug123/WoofWare.Myriad</RepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageTags>myriad;fsharp;source-generator;source-gen;json</PackageTags>
|
<PackageTags>myriad;fsharp;source-generator;source-gen;json</PackageTags>
|
||||||
@@ -27,6 +28,10 @@
|
|||||||
<Compile Include="JsonParseGenerator.fs" />
|
<Compile Include="JsonParseGenerator.fs" />
|
||||||
<None Include="version.json" />
|
<None Include="version.json" />
|
||||||
<EmbeddedResource Include="SurfaceBaseline.txt" />
|
<EmbeddedResource Include="SurfaceBaseline.txt" />
|
||||||
|
<None Include="..\README.md">
|
||||||
|
<Pack>True</Pack>
|
||||||
|
<PackagePath>\</PackagePath>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
16
analyzers/analyzers.fsproj
Normal file
16
analyzers/analyzers.fsproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.Build.NoTargets/1.0.80"> <!-- This is not a project we want to build. -->
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsPublishable>false</IsPublishable>
|
||||||
|
<RestorePackagesPath>../.analyzerpackages/</RestorePackagesPath>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
|
||||||
|
<AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages> <!-- We don't want to build this project, so we do not need the reference assemblies for the framework we chose.-->
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.6.0]" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
18
flake.nix
18
flake.nix
@@ -14,11 +14,11 @@
|
|||||||
}:
|
}:
|
||||||
flake-utils.lib.eachDefaultSystem (system: let
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
pname = "fsharp-arguments";
|
pname = "WoofWare.Myriad.Plugins";
|
||||||
dotnet-sdk = pkgs.dotnet-sdk_8;
|
dotnet-sdk = pkgs.dotnet-sdk_8;
|
||||||
dotnet-runtime = pkgs.dotnetCorePackages.runtime_8_0;
|
dotnet-runtime = pkgs.dotnetCorePackages.runtime_8_0;
|
||||||
version = "0.1";
|
version = "0.1";
|
||||||
dotnetTool = toolName: toolVersion: sha256:
|
dotnetTool = dllOverride: toolName: toolVersion: sha256:
|
||||||
pkgs.stdenvNoCC.mkDerivation rec {
|
pkgs.stdenvNoCC.mkDerivation rec {
|
||||||
name = toolName;
|
name = toolName;
|
||||||
version = toolVersion;
|
version = toolVersion;
|
||||||
@@ -29,17 +29,23 @@
|
|||||||
sha256 = sha256;
|
sha256 = sha256;
|
||||||
installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
|
installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
|
||||||
};
|
};
|
||||||
installPhase = ''
|
installPhase = let
|
||||||
|
dll =
|
||||||
|
if isNull dllOverride
|
||||||
|
then name
|
||||||
|
else dllOverride;
|
||||||
|
in ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
mkdir -p "$out/lib"
|
mkdir -p "$out/lib"
|
||||||
cp -r ./bin/* "$out/lib"
|
cp -r ./bin/* "$out/lib"
|
||||||
makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${name}.dll"
|
makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${dll}.dll"
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
packages = {
|
packages = {
|
||||||
fantomas = dotnetTool "fantomas" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fantomas.version "sha256-Jmo7s8JMdQ8SxvNvPnryfE7n24mIgKi5cbgNwcQw3yU=";
|
fantomas = dotnetTool null "fantomas" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fantomas.version "sha256-Jmo7s8JMdQ8SxvNvPnryfE7n24mIgKi5cbgNwcQw3yU=";
|
||||||
|
fsharp-analyzers = dotnetTool "FSharp.Analyzers.Cli" "fsharp-analyzers" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fsharp-analyzers.version "sha256-wDS7aE4VI718iwU8xUm0aCOYIcFpMuqWu9+H5d+8XAA=";
|
||||||
fetchDeps = let
|
fetchDeps = let
|
||||||
flags = [];
|
flags = [];
|
||||||
runtimeIds = ["win-x64"] ++ map (system: pkgs.dotnetCorePackages.systemToDotnetRid system) dotnet-sdk.meta.platforms;
|
runtimeIds = ["win-x64"] ++ map (system: pkgs.dotnetCorePackages.systemToDotnetRid system) dotnet-sdk.meta.platforms;
|
||||||
@@ -60,7 +66,7 @@
|
|||||||
}));
|
}));
|
||||||
default = pkgs.buildDotnetModule {
|
default = pkgs.buildDotnetModule {
|
||||||
pname = pname;
|
pname = pname;
|
||||||
name = "argument-helpers";
|
name = "WoofWare.Myriad.Plugins";
|
||||||
version = version;
|
version = version;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
projectFile = "./WoofWare.Myriad.Plugins/WoofWare.Myriad.Plugins.fsproj";
|
projectFile = "./WoofWare.Myriad.Plugins/WoofWare.Myriad.Plugins.fsproj";
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
# This file was automatically generated by passthru.fetch-deps.
|
# This file was automatically generated by passthru.fetch-deps.
|
||||||
# Please don't edit it manually, your changes might get overwritten!
|
# Please don't edit it manually, your changes might get overwritten!
|
||||||
{fetchNuGet}: [
|
{fetchNuGet}: [
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "fsharp-analyzers";
|
||||||
|
version = "0.22.0";
|
||||||
|
sha256 = "sha256-wDS7aE4VI718iwU8xUm0aCOYIcFpMuqWu9+H5d+8XAA=";
|
||||||
|
})
|
||||||
(fetchNuGet {
|
(fetchNuGet {
|
||||||
pname = "fantomas";
|
pname = "fantomas";
|
||||||
version = "6.3.0-alpha-005";
|
version = "6.3.0-alpha-005";
|
||||||
|
Reference in New Issue
Block a user