Initializing the repository with some basic types.
This commit is contained in:
commit
ea82a5ff44
26 changed files with 1600 additions and 0 deletions
67
.forgejo/workflows/pull_request.yaml
Normal file
67
.forgejo/workflows/pull_request.yaml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
library_snapshot:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: registry.garrity.co:8443/gs/ci-scala:latest
|
||||
name: 'Build and Test Library Snapshot'
|
||||
env:
|
||||
GS_MAVEN_USER: ${{ vars.GS_MAVEN_USER }}
|
||||
GS_MAVEN_TOKEN: ${{ secrets.GS_MAVEN_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
name: 'Checkout Repository'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Pre-Commit'
|
||||
run: |
|
||||
pre-commit install
|
||||
pre-commit run --all-files
|
||||
- name: 'Prepare Versioned Build'
|
||||
run: |
|
||||
latest_git_tag="$(git describe --tags --abbrev=0 || echo 'No Tags')"
|
||||
latest_commit_message="$(git show -s --format=%s HEAD)"
|
||||
if [[ "$latest_commit_message" == *"(major)"* ]]; then
|
||||
export GS_RELEASE_TYPE="major"
|
||||
elif [[ "$latest_commit_message" == *"(minor)"* ]]; then
|
||||
export GS_RELEASE_TYPE="minor"
|
||||
elif [[ "$latest_commit_message" == *"(patch)"* ]]; then
|
||||
export GS_RELEASE_TYPE="patch"
|
||||
elif [[ "$latest_commit_message" == *"(docs)"* ]]; then
|
||||
export GS_RELEASE_TYPE="norelease"
|
||||
elif [[ "$latest_commit_message" == *"(norelease)"* ]]; then
|
||||
export GS_RELEASE_TYPE="norelease"
|
||||
else
|
||||
export GS_RELEASE_TYPE="norelease"
|
||||
fi
|
||||
echo "GS_RELEASE_TYPE=$GS_RELEASE_TYPE" >> $GITHUB_ENV
|
||||
echo "Previous Git Tag: $latest_git_tag"
|
||||
echo "Latest Commit: $latest_commit_message ($GS_RELEASE_TYPE) (SNAPSHOT)"
|
||||
if [ "$GS_RELEASE_TYPE" = "norelease" ]; then
|
||||
sbtn -Dsnapshot=true -Drelease="patch" semVerInfo
|
||||
else
|
||||
sbtn -Dsnapshot=true -Drelease="$GS_RELEASE_TYPE" semVerInfo
|
||||
fi
|
||||
- name: 'Unit Tests and Code Coverage'
|
||||
run: |
|
||||
sbtn clean
|
||||
sbtn coverage
|
||||
sbtn test
|
||||
sbtn coverageReport
|
||||
- name: 'Publish Snapshot'
|
||||
run: |
|
||||
echo "Testing env var propagation = ${{ env.GS_RELEASE_TYPE }}"
|
||||
if [ "${{ env.GS_RELEASE_TYPE }}" = "norelease" ]; then
|
||||
echo "Skipping publish due to GS_RELEASE_TYPE=norelease"
|
||||
else
|
||||
sbtn coverageOff
|
||||
sbtn clean
|
||||
sbtn publish
|
||||
fi
|
||||
84
.forgejo/workflows/release.yaml
Normal file
84
.forgejo/workflows/release.yaml
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
library_release:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: registry.garrity.co:8443/gs/ci-scala:latest
|
||||
name: 'Build and Release Library'
|
||||
env:
|
||||
GS_MAVEN_USER: ${{ vars.GS_MAVEN_USER }}
|
||||
GS_MAVEN_TOKEN: ${{ secrets.GS_MAVEN_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
name: 'Checkout Repository'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Pre-Commit'
|
||||
run: |
|
||||
pre-commit install
|
||||
pre-commit run --all-files
|
||||
- name: 'Prepare Versioned Build'
|
||||
run: |
|
||||
latest_git_tag="$(git describe --tags --abbrev=0 || echo 'No Tags')"
|
||||
latest_commit_message="$(git show -s --format=%s HEAD)"
|
||||
if [[ "$latest_commit_message" == *"(major)"* ]]; then
|
||||
export GS_RELEASE_TYPE="major"
|
||||
elif [[ "$latest_commit_message" == *"(minor)"* ]]; then
|
||||
export GS_RELEASE_TYPE="minor"
|
||||
elif [[ "$latest_commit_message" == *"(patch)"* ]]; then
|
||||
export GS_RELEASE_TYPE="patch"
|
||||
elif [[ "$latest_commit_message" == *"(docs)"* ]]; then
|
||||
export GS_RELEASE_TYPE="norelease"
|
||||
elif [[ "$latest_commit_message" == *"(norelease)"* ]]; then
|
||||
export GS_RELEASE_TYPE="norelease"
|
||||
else
|
||||
export GS_RELEASE_TYPE="norelease"
|
||||
fi
|
||||
|
||||
echo "GS_RELEASE_TYPE=$GS_RELEASE_TYPE" >> $GITHUB_ENV
|
||||
echo "Previous Git Tag: $latest_git_tag"
|
||||
echo "Latest Commit: $latest_commit_message"
|
||||
echo "Selected Release Type: '$GS_RELEASE_TYPE'"
|
||||
|
||||
if [ "$GS_RELEASE_TYPE" = "norelease" ]; then
|
||||
echo "Skipping all versioning for 'norelease' commit."
|
||||
else
|
||||
sbtn -Drelease="$GS_RELEASE_TYPE" semVerInfo
|
||||
fi
|
||||
- name: 'Unit Tests and Code Coverage'
|
||||
run: |
|
||||
if [ "${{ env.GS_RELEASE_TYPE }}" = "norelease" ]; then
|
||||
echo "Skipping build/test for 'norelease' commit."
|
||||
else
|
||||
sbtn clean
|
||||
sbtn coverage
|
||||
sbtn test
|
||||
sbtn coverageReport
|
||||
fi
|
||||
- name: 'Publish Release'
|
||||
run: |
|
||||
if [ "${{ env.GS_RELEASE_TYPE }}" = "norelease" ]; then
|
||||
echo "Skipping publish for 'norelease' commit."
|
||||
else
|
||||
sbtn coverageOff
|
||||
sbtn clean
|
||||
sbtn semVerWriteVersionToFile
|
||||
sbtn publish
|
||||
fi
|
||||
- name: 'Create Git Tag'
|
||||
run: |
|
||||
if [ "${{ env.GS_RELEASE_TYPE }}" = "norelease" ]; then
|
||||
echo "Skipping Git tag for 'norelease' commit."
|
||||
else
|
||||
selected_version="$(cat .version)"
|
||||
git tag "$selected_version"
|
||||
git push origin "$selected_version"
|
||||
fi
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
target/
|
||||
project/target/
|
||||
project/project/
|
||||
modules/core/target/
|
||||
.version
|
||||
.scala-version
|
||||
17
.pre-commit-config.yaml
Normal file
17
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: fix-byte-order-marker
|
||||
- id: mixed-line-ending
|
||||
args: ['--fix=lf']
|
||||
description: Enforces using only 'LF' line endings.
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- repo: https://git.garrity.co/garrity-software/gs-pre-commit-scala
|
||||
rev: v1.0.2
|
||||
hooks:
|
||||
- id: scalafmt
|
||||
71
.scalafmt.conf
Normal file
71
.scalafmt.conf
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// See: https://github.com/scalameta/scalafmt/tags for the latest tags.
|
||||
version = 3.10.7
|
||||
runner.dialect = scala3
|
||||
maxColumn = 80
|
||||
|
||||
rewrite {
|
||||
rules = [RedundantBraces, RedundantParens, Imports, SortModifiers]
|
||||
imports.expand = true
|
||||
imports.sort = scalastyle
|
||||
redundantBraces.ifElseExpressions = true
|
||||
redundantBraces.stringInterpolation = true
|
||||
}
|
||||
|
||||
indent {
|
||||
main = 2
|
||||
callSite = 2
|
||||
defnSite = 2
|
||||
extendSite = 4
|
||||
withSiteRelativeToExtends = 2
|
||||
commaSiteRelativeToExtends = 2
|
||||
}
|
||||
|
||||
align {
|
||||
preset = more
|
||||
openParenCallSite = false
|
||||
openParenDefnSite = false
|
||||
}
|
||||
|
||||
newlines {
|
||||
implicitParamListModifierForce = [before,after]
|
||||
topLevelStatementBlankLines = [
|
||||
{
|
||||
blanks = 1
|
||||
}
|
||||
]
|
||||
afterCurlyLambdaParams = squash
|
||||
}
|
||||
|
||||
danglingParentheses {
|
||||
defnSite = true
|
||||
callSite = true
|
||||
ctrlSite = true
|
||||
exclude = []
|
||||
}
|
||||
|
||||
verticalMultiline {
|
||||
atDefnSite = true
|
||||
arityThreshold = 2
|
||||
newlineAfterOpenParen = true
|
||||
}
|
||||
|
||||
comments {
|
||||
wrap = standalone
|
||||
}
|
||||
|
||||
docstrings {
|
||||
style = "SpaceAsterisk"
|
||||
oneline = unfold
|
||||
wrap = yes
|
||||
forceBlankLineBefore = true
|
||||
}
|
||||
|
||||
project {
|
||||
excludePaths = [
|
||||
"glob:**target/**",
|
||||
"glob:**.metals/**",
|
||||
"glob:**.bloop/**",
|
||||
"glob:**.bsp/**",
|
||||
"glob:**metals.sbt"
|
||||
]
|
||||
}
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
54
README.md
Normal file
54
README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# gs-std
|
||||
|
||||
[GS Open Source](https://garrity.co/oss.html) |
|
||||
[License (Apache 2.0)](./LICENSE)
|
||||
|
||||
Garrity Software standard types and operations. Provides a zero-dependency
|
||||
collection of basic tools.
|
||||
|
||||
- [Usage](#usage)
|
||||
- [Dependency](#dependency)
|
||||
- [Donate](#donate)
|
||||
|
||||
## Usage
|
||||
|
||||
### Dependency
|
||||
|
||||
This artifact is available in the Garrity Software Maven repository.
|
||||
|
||||
```scala
|
||||
externalResolvers +=
|
||||
"Garrity Software Releases" at "https://maven.garrity.co/releases"
|
||||
|
||||
val GsStd: ModuleID =
|
||||
"gs" %% "gs-std-v0" % "$VERSION"
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
- `Nat`
|
||||
- `Size`
|
||||
- `ByteCount`
|
||||
- `Blob`
|
||||
- `CreatedAt`
|
||||
- `UpdatedAt`
|
||||
- `SHA256`
|
||||
- `MD5`
|
||||
- `EncodedString`
|
||||
- `B64`
|
||||
- `B64Url`
|
||||
- `Hex`
|
||||
|
||||
## Tools
|
||||
|
||||
- `Encoder`
|
||||
- `Base64Encoder`
|
||||
- `HexEncoder`
|
||||
- `Decoder`
|
||||
- `Base64Decoder`
|
||||
- `HexDecoder`
|
||||
|
||||
## Donate
|
||||
|
||||
Enjoy this project or want to help me achieve my [goals](https://garrity.co)?
|
||||
Consider [Donating to Pat on Ko-fi](https://ko-fi.com/gspfm).
|
||||
31
build.sbt
Normal file
31
build.sbt
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
val scala3: String = "3.8.3"
|
||||
|
||||
ThisBuild / scalaVersion := scala3
|
||||
ThisBuild / versionScheme := Some("semver-spec")
|
||||
ThisBuild / gsProjectName := "gs-std"
|
||||
|
||||
ThisBuild / licenses := Seq(
|
||||
"Apache 2.0" -> url(
|
||||
"https://git.garrity.co/garrity-software/gs-std/src/branch/main/LICENSE"
|
||||
)
|
||||
)
|
||||
|
||||
val sharedSettings = Seq(
|
||||
scalaVersion := scala3,
|
||||
version := semVerSelected.value,
|
||||
coverageFailOnMinimum := true,
|
||||
coverageMinimumStmtTotal := 100,
|
||||
coverageMinimumBranchTotal := 100
|
||||
)
|
||||
|
||||
lazy val testSettings = Seq(
|
||||
libraryDependencies ++= Seq(
|
||||
"org.scalameta" %% "munit" % "1.3.0" % Test
|
||||
)
|
||||
)
|
||||
|
||||
lazy val `gs-std` = project
|
||||
.in(file("."))
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
|
||||
1
project/build.properties
Normal file
1
project/build.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
sbt.version=1.12.9
|
||||
34
project/plugins.sbt
Normal file
34
project/plugins.sbt
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
def selectCredentials(): Credentials =
|
||||
if ((Path.userHome / ".sbt" / ".credentials").exists())
|
||||
Credentials(Path.userHome / ".sbt" / ".credentials")
|
||||
else
|
||||
Credentials.apply(
|
||||
realm = "Reposilite",
|
||||
host = "maven.garrity.co",
|
||||
userName = sys.env
|
||||
.get("GS_MAVEN_USER")
|
||||
.getOrElse(
|
||||
throw new RuntimeException(
|
||||
"You must either provide ~/.sbt/.credentials or specify the GS_MAVEN_USER environment variable."
|
||||
)
|
||||
),
|
||||
passwd = sys.env
|
||||
.get("GS_MAVEN_TOKEN")
|
||||
.getOrElse(
|
||||
throw new RuntimeException(
|
||||
"You must either provide ~/.sbt/.credentials or specify the GS_MAVEN_TOKEN environment variable."
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
credentials += selectCredentials()
|
||||
|
||||
externalResolvers := Seq(
|
||||
"Garrity Software Mirror" at "https://maven.garrity.co/releases",
|
||||
"Garrity Software Snapshots" at "https://maven.garrity.co/snapshots",
|
||||
"Garrity Software Releases" at "https://maven.garrity.co/gs"
|
||||
)
|
||||
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4")
|
||||
addSbtPlugin("gs" % "sbt-garrity-software" % "0.7.0")
|
||||
addSbtPlugin("gs" % "sbt-gs-semver" % "0.3.0")
|
||||
36
src/main/scala/gs/std/v0/Base64Decoder.scala
Normal file
36
src/main/scala/gs/std/v0/Base64Decoder.scala
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.{util => ju}
|
||||
|
||||
/** Implementation of [[Decoder]] for Base64 strings.
|
||||
*
|
||||
* Supports base64-url decoding as well.
|
||||
*/
|
||||
object Base64Decoder extends Decoder[B64]:
|
||||
private lazy val d: ju.Base64.Decoder = ju.Base64.getDecoder()
|
||||
private lazy val du: ju.Base64.Decoder = ju.Base64.getUrlDecoder()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def decode(input: B64): Array[Byte] =
|
||||
d.decode(input.data)
|
||||
|
||||
/** Decode some arbitrary string data.
|
||||
*
|
||||
* @param input
|
||||
* The data to decode.
|
||||
* @return
|
||||
* The decoded bytes.
|
||||
*/
|
||||
def decodeUnsafe(input: String): Array[Byte] =
|
||||
d.decode(input)
|
||||
|
||||
/** Decode the base64-url encoded input.
|
||||
*
|
||||
* @param input
|
||||
* The base64-url encoded data.
|
||||
* @return
|
||||
* The decoded bytes.
|
||||
*/
|
||||
def decodeUrl(input: B64Url): Array[Byte] =
|
||||
du.decode(input.data)
|
||||
43
src/main/scala/gs/std/v0/Base64Encoder.scala
Normal file
43
src/main/scala/gs/std/v0/Base64Encoder.scala
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Base64
|
||||
|
||||
/** Implementation of [[Encoder]] for Base64.
|
||||
*
|
||||
* Supports base64-url encoding as well.
|
||||
*/
|
||||
object Base64Encoder extends Encoder[B64]:
|
||||
private lazy val e: Base64.Encoder = Base64.getEncoder()
|
||||
private lazy val eu: Base64.Encoder = Base64.getUrlEncoder()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def encode(input: Array[Byte]): B64 =
|
||||
B64(e.encodeToString(input))
|
||||
|
||||
/** Encode the given bytes using base64-url.
|
||||
*
|
||||
* @param input
|
||||
* The input data.
|
||||
* @return
|
||||
* The base64-url-encoded string.
|
||||
*/
|
||||
def encodeUrl(input: Array[Byte]): B64Url =
|
||||
B64Url(eu.encodeToString(input))
|
||||
|
||||
/** Encode the given bytes using base64-url.
|
||||
*
|
||||
* @param input
|
||||
* The input data.
|
||||
* @param charset
|
||||
* The character set of the input data.
|
||||
* @return
|
||||
* The base64-url-encoded string.
|
||||
*/
|
||||
def encodeUrl(
|
||||
input: String,
|
||||
charset: Charset = StandardCharsets.UTF_8
|
||||
): B64Url =
|
||||
encodeUrl(input.getBytes(charset))
|
||||
44
src/main/scala/gs/std/v0/Blob.scala
Normal file
44
src/main/scala/gs/std/v0/Blob.scala
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.util.Base64
|
||||
|
||||
/**
|
||||
* Represents a blob -- some array of bytes.
|
||||
*
|
||||
* @param data The underlying data.
|
||||
*/
|
||||
final class Blob(private val data: Array[Byte]) extends IndexedSeq[Byte]:
|
||||
|
||||
/** @inheritDocs */
|
||||
override def apply(i: Int): Byte = byteAt(i)
|
||||
|
||||
/** @inheritDocs */
|
||||
override def length: Int = data.length
|
||||
|
||||
/**
|
||||
* @return The number of bytes in this blob, expressed as a count.
|
||||
*/
|
||||
def numberOfBytes: ByteCount = ByteCount(data.length)
|
||||
|
||||
/** @inheritDocs */
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: Blob => data.sameElements(other.data)
|
||||
case other: Array[Byte] => data.sameElements(other)
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs */
|
||||
override def hashCode(): Int = data.hashCode()
|
||||
|
||||
/**
|
||||
* Retrieve the byte at the given index.
|
||||
*
|
||||
* @param index The index.
|
||||
* @return The byte stored at the given index.
|
||||
*/
|
||||
def byteAt(index: Int): Byte = data.apply(index)
|
||||
|
||||
/**
|
||||
* @return This byte array, encoded as a base64 string.
|
||||
*/
|
||||
def base64(): String = Base64.getEncoder().encodeToString(data)
|
||||
132
src/main/scala/gs/std/v0/ByteCount.scala
Normal file
132
src/main/scala/gs/std/v0/ByteCount.scala
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package gs.std.v0
|
||||
|
||||
/**
|
||||
* Opaque type for some number of bytes (>= 0).
|
||||
*/
|
||||
opaque type ByteCount = Long
|
||||
|
||||
/**
|
||||
* Opaque type for some number of bytes (>= 0).
|
||||
*/
|
||||
object ByteCount:
|
||||
|
||||
/**
|
||||
* 0 bytes.
|
||||
*/
|
||||
final val Zero: ByteCount = 0
|
||||
|
||||
/**
|
||||
* 1,000 bytes.
|
||||
*/
|
||||
final val OneKilobyte: ByteCount = 1000
|
||||
|
||||
/**
|
||||
* 1,000,000 bytes.
|
||||
*/
|
||||
final val OneMegabyte: ByteCount = 1000000
|
||||
|
||||
/**
|
||||
* 1,000,000,000 bytes.
|
||||
*/
|
||||
final val OneGigabyte: ByteCount = 1000000000
|
||||
|
||||
/**
|
||||
* Express the given number as a byte count. All values are normalized to the
|
||||
* absolute value -- negative values are coerced to positive.
|
||||
*
|
||||
* @param value The input integer.
|
||||
* @return The [[ByteCount]] instance.
|
||||
*/
|
||||
def apply(value: Long): ByteCount = Math.abs(value)
|
||||
|
||||
/**
|
||||
* 1 kilobyte = 1,000 bytes
|
||||
*
|
||||
* @param kb The number of kilobytes.
|
||||
* @return The number of bytes.
|
||||
*/
|
||||
def fromKilobytes(kb: Long): ByteCount =
|
||||
Math.abs(kb) * 1000L
|
||||
|
||||
/**
|
||||
* 1 megabyte = 1,000,000 bytes
|
||||
*
|
||||
* @param mb The number of megabytes.
|
||||
* @return The number of bytes.
|
||||
*/
|
||||
def fromMegabytes(mb: Long): ByteCount =
|
||||
Math.abs(mb) * 1000000L
|
||||
|
||||
/**
|
||||
* 1 gigabyte = 1,000,000,000 bytes
|
||||
*
|
||||
* @param mb The number of gigabytes.
|
||||
* @return The number of bytes.
|
||||
*/
|
||||
def fromGigabytes(gb: Long): ByteCount =
|
||||
Math.abs(gb) * 1000000000
|
||||
|
||||
given CanEqual[ByteCount, ByteCount] = CanEqual.derived
|
||||
|
||||
given Ordering[ByteCount] with
|
||||
/** @inheritDocs */
|
||||
def compare(x: ByteCount, y: ByteCount): Int =
|
||||
if x > y then 1 else if x == y then 0 else -1
|
||||
|
||||
extension (byteCount: ByteCount)
|
||||
/**
|
||||
* @return The underlying `Long`.
|
||||
*/
|
||||
def unwrap(): Long = byteCount
|
||||
|
||||
/**
|
||||
* @return The underlying `Long`.
|
||||
*/
|
||||
def toLong(): Long = byteCount
|
||||
|
||||
/**
|
||||
* 1 kilobyte = 1,000 bytes
|
||||
*
|
||||
* @return The number of kilobytes represented by this count.
|
||||
*/
|
||||
def toKilobytes(): Double = byteCount / 1000.0
|
||||
|
||||
/**
|
||||
* 1 megabyte = 1,000,000 bytes.
|
||||
*
|
||||
* @return The number of megabytes represented by this count.
|
||||
*/
|
||||
def toMegabytes(): Double = byteCount / 1000000.0
|
||||
|
||||
/**
|
||||
* 1 gigabyte = 1,000,000,000 bytes.
|
||||
*
|
||||
* @return The number of gigabytes represented by this count.
|
||||
*/
|
||||
def toGigabytes(): Double = byteCount / 1000000000.0
|
||||
|
||||
/**
|
||||
* Add some count to this one.
|
||||
*
|
||||
* @param that The number to add.
|
||||
* @return The sum of the numbers.
|
||||
*/
|
||||
def +(that: ByteCount): ByteCount = byteCount + that
|
||||
|
||||
/**
|
||||
* Multiply this count by some other count.
|
||||
*
|
||||
* @param that The number to multiply by.
|
||||
* @return The product of the numbers.
|
||||
*/
|
||||
def *(that: ByteCount): ByteCount = byteCount * that
|
||||
|
||||
/**
|
||||
* Check if this value is the same as some number.
|
||||
*
|
||||
* @param value The number to compare against.
|
||||
* @return True if the values are equal, false otherwise.
|
||||
*/
|
||||
def equal(value: Int): Boolean = byteCount == value.toLong
|
||||
|
||||
end ByteCount
|
||||
38
src/main/scala/gs/std/v0/CreatedAt.scala
Normal file
38
src/main/scala/gs/std/v0/CreatedAt.scala
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Opaque type that represents the instant something was created.
|
||||
*/
|
||||
opaque type CreatedAt = Instant
|
||||
|
||||
/**
|
||||
* Opaque type that represents the instant something was created.
|
||||
*/
|
||||
object CreatedAt:
|
||||
|
||||
/**
|
||||
* Instantiate a new [[CreatedAt]] from the given `java.time.Instant`.
|
||||
*
|
||||
* @param value The value to semantically represent.
|
||||
* @return The new [[CreatedAt]].
|
||||
*/
|
||||
def apply(value: Instant): CreatedAt = value
|
||||
|
||||
given CanEqual[CreatedAt, CreatedAt] = CanEqual.derived
|
||||
|
||||
given Ordering[CreatedAt] = Ordering[Instant]
|
||||
|
||||
extension (createdAt: Instant)
|
||||
/**
|
||||
* @return The underlying `java.time.Instant`.
|
||||
*/
|
||||
def unwrap(): Instant = createdAt
|
||||
|
||||
/**
|
||||
* @return The underlying `java.time.Instant`.
|
||||
*/
|
||||
def toInstant(): Instant = createdAt
|
||||
|
||||
end CreatedAt
|
||||
13
src/main/scala/gs/std/v0/Decoder.scala
Normal file
13
src/main/scala/gs/std/v0/Decoder.scala
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package gs.std.v0
|
||||
|
||||
/**
|
||||
* Interface for byte decoding from encoded String formats.
|
||||
*/
|
||||
trait Decoder[-A <: EncodedString]:
|
||||
/**
|
||||
* Decode an input string to an array of bytes.
|
||||
*
|
||||
* @param input The input to decode.
|
||||
* @return The decoded byte array.
|
||||
*/
|
||||
def decode(input: A): Array[Byte]
|
||||
58
src/main/scala/gs/std/v0/Encode.scala
Normal file
58
src/main/scala/gs/std/v0/Encode.scala
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/** Helper functions for encoding data.
|
||||
*/
|
||||
object Encode:
|
||||
|
||||
/** Encode an array of bytes using base64.
|
||||
*
|
||||
* @param input
|
||||
* The bytes to encode.
|
||||
* @return
|
||||
* The base64 string.
|
||||
*/
|
||||
def base64(input: Array[Byte]): B64 = Base64Encoder.encode(input)
|
||||
|
||||
/** Encode a string using base64.
|
||||
*
|
||||
* @param input
|
||||
* The string to encode.
|
||||
* @param charset
|
||||
* The character set of the input string.
|
||||
* @return
|
||||
* The base64 string.
|
||||
*/
|
||||
def base64(
|
||||
input: String,
|
||||
charset: Charset = StandardCharsets.UTF_8
|
||||
): B64 =
|
||||
Base64Encoder.encode(input, charset)
|
||||
|
||||
/** Encode an array of bytes using hexadecimal.
|
||||
*
|
||||
* @param input
|
||||
* The bytes to encode.
|
||||
* @return
|
||||
* The hexadecimal string.
|
||||
*/
|
||||
def hex(input: Array[Byte]): Hex = HexEncoder.encode(input)
|
||||
|
||||
/** Encode a string using hexadecimal.
|
||||
*
|
||||
* @param input
|
||||
* The string to encode.
|
||||
* @param charset
|
||||
* The character set of the input string.
|
||||
* @return
|
||||
* The hexadecimal string.
|
||||
*/
|
||||
def hex(
|
||||
input: String,
|
||||
charset: Charset = StandardCharsets.UTF_8
|
||||
): Hex =
|
||||
HexEncoder.encode(input, charset)
|
||||
|
||||
end Encode
|
||||
162
src/main/scala/gs/std/v0/EncodedString.scala
Normal file
162
src/main/scala/gs/std/v0/EncodedString.scala
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package gs.std.v0
|
||||
|
||||
/** Represents string-encoded data.
|
||||
*
|
||||
* See:
|
||||
* - [[B64]]
|
||||
* - [[Hex]]
|
||||
*/
|
||||
trait EncodedString:
|
||||
/** @return
|
||||
* The encoded data (expressed as a string).
|
||||
*/
|
||||
def data: String
|
||||
|
||||
/** @return
|
||||
* Decode the data to a byte array.
|
||||
*/
|
||||
def decode(): Array[Byte]
|
||||
|
||||
/** Represents Base64-encoded data.
|
||||
*
|
||||
* @param data
|
||||
* The encoded data.
|
||||
*/
|
||||
|
||||
/** Represents Base64-encoded data.
|
||||
*
|
||||
* @param data
|
||||
* The encoded data.
|
||||
*/
|
||||
final class B64(
|
||||
val data: String
|
||||
) extends EncodedString:
|
||||
/** @inheritDocs
|
||||
*/
|
||||
def decode(): Array[Byte] = Base64Decoder.decode(this)
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: B64 => data == other.data
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = data.hashCode()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String = data
|
||||
|
||||
object B64:
|
||||
|
||||
/** Instantiate [[B64]] from the given string. Assumes that the input is
|
||||
* base64-encoded.
|
||||
*
|
||||
* This function does NOT validate the input.
|
||||
*
|
||||
* @param data
|
||||
* The encoded data.
|
||||
* @return
|
||||
* The new [[B64]] instance.
|
||||
*/
|
||||
def apply(
|
||||
data: String
|
||||
): B64 = new B64(data)
|
||||
|
||||
given CanEqual[B64, B64] = CanEqual.derived
|
||||
|
||||
end B64
|
||||
|
||||
/** Represents Base64-url-encoded data.
|
||||
*
|
||||
* @param data
|
||||
* The encoded data.
|
||||
*/
|
||||
final class B64Url(
|
||||
val data: String
|
||||
) extends EncodedString:
|
||||
/** @inheritDocs
|
||||
*/
|
||||
def decode(): Array[Byte] = Base64Decoder.decodeUrl(this)
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: B64Url => data == other.data
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = data.hashCode()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String = data
|
||||
|
||||
object B64Url:
|
||||
|
||||
/** Instantiate [[B64Url]] from the given string. Assumes that the input is
|
||||
* base64-encoded.
|
||||
*
|
||||
* This function does NOT validate the input.
|
||||
*
|
||||
* @param data
|
||||
* The encoded data.
|
||||
* @return
|
||||
* The new [[B64Url]] instance.
|
||||
*/
|
||||
def apply(
|
||||
data: String
|
||||
): B64Url = new B64Url(data)
|
||||
|
||||
given CanEqual[B64Url, B64Url] = CanEqual.derived
|
||||
|
||||
end B64Url
|
||||
|
||||
/** Represents Hex-encoded data.
|
||||
*
|
||||
* @param data
|
||||
* The encoded data.
|
||||
*/
|
||||
final class Hex(
|
||||
val data: String
|
||||
) extends EncodedString:
|
||||
/** @inheritDocs
|
||||
*/
|
||||
def decode(): Array[Byte] = HexDecoder.decode(this)
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: Hex => data == other.data
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = data.hashCode()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String = data
|
||||
|
||||
object Hex:
|
||||
|
||||
/** Instantiate [[Hex]] from the given string. Assumes that the input is
|
||||
* hex-encoded.
|
||||
*
|
||||
* This function does NOT validate the input.
|
||||
*
|
||||
* @param data
|
||||
* The encoded data.
|
||||
* @return
|
||||
* The new [[Hex]] instance.
|
||||
*/
|
||||
def apply(
|
||||
data: String
|
||||
): Hex = new Hex(data)
|
||||
|
||||
given CanEqual[Hex, Hex] = CanEqual.derived
|
||||
|
||||
end Hex
|
||||
44
src/main/scala/gs/std/v0/Encoder.scala
Normal file
44
src/main/scala/gs/std/v0/Encoder.scala
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/** Interface for byte encoding to String formats.
|
||||
*/
|
||||
trait Encoder[+A <: EncodedString]:
|
||||
/** Encode an array of bytes as a string.
|
||||
*
|
||||
* @param input
|
||||
* The bytes to encode.
|
||||
* @return
|
||||
* The encoded string.
|
||||
*/
|
||||
def encode(input: Array[Byte]): A
|
||||
|
||||
/** Encode a string as a string.
|
||||
*
|
||||
* @param input
|
||||
* The string to encode.
|
||||
* @param charset
|
||||
* The character set of the input string.
|
||||
* @return
|
||||
* The encoded string.
|
||||
*/
|
||||
def encode(
|
||||
input: String,
|
||||
charset: Charset = StandardCharsets.UTF_8
|
||||
): A = encode(input.getBytes(charset))
|
||||
|
||||
object Encoder:
|
||||
|
||||
/** @return
|
||||
* The [[Base64Encoder]], typed to `Encoder[Encoded]`.
|
||||
*/
|
||||
def base64(): Encoder[EncodedString] = Base64Encoder
|
||||
|
||||
/** @return
|
||||
* The [[HexEncoder]], typed to `Encoder[Encoded]`.
|
||||
*/
|
||||
def hex(): Encoder[EncodedString] = HexEncoder
|
||||
|
||||
end Encoder
|
||||
23
src/main/scala/gs/std/v0/HexDecoder.scala
Normal file
23
src/main/scala/gs/std/v0/HexDecoder.scala
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.util.HexFormat
|
||||
|
||||
/** Implementation of [[Decoder]] for Hexadecimal strings.
|
||||
*/
|
||||
object HexDecoder extends Decoder[Hex]:
|
||||
private lazy val h: HexFormat = HexFormat.of()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def decode(input: Hex): Array[Byte] =
|
||||
h.parseHex(input.data)
|
||||
|
||||
/** Decode some arbitrary string data.
|
||||
*
|
||||
* @param input
|
||||
* The data to decode.
|
||||
* @return
|
||||
* The decoded bytes.
|
||||
*/
|
||||
def decodeUnsafe(input: String): Array[Byte] =
|
||||
h.parseHex(input)
|
||||
13
src/main/scala/gs/std/v0/HexEncoder.scala
Normal file
13
src/main/scala/gs/std/v0/HexEncoder.scala
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.util.HexFormat
|
||||
|
||||
/** Implementation of [[Encoder]] for Hexadecimal strings.
|
||||
*/
|
||||
object HexEncoder extends Encoder[Hex]:
|
||||
private lazy val h: HexFormat = HexFormat.of()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def encode(input: Array[Byte]): Hex =
|
||||
Hex(h.formatHex(input))
|
||||
105
src/main/scala/gs/std/v0/MD5.scala
Normal file
105
src/main/scala/gs/std/v0/MD5.scala
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Opaque type representing a MD5 hash.
|
||||
*/
|
||||
opaque type MD5 = Array[Byte]
|
||||
|
||||
/**
|
||||
* Opaque type representing a MD5 hash.
|
||||
*/
|
||||
object MD5:
|
||||
|
||||
/**
|
||||
* MD5 hashes are exactly 16 bytes.
|
||||
*/
|
||||
final val NumberOfBytes: ByteCount = ByteCount(16)
|
||||
|
||||
/**
|
||||
* The algorithm name is "MD5".
|
||||
*/
|
||||
final val Algorithm: String = "MD5"
|
||||
|
||||
/**
|
||||
* Instantiate a [[MD5]] from the given byte array. This function does not
|
||||
* know whether the array is actually a calculated hash.
|
||||
*
|
||||
* Typically used for loading pre-validated hashes (e.g. from a database).
|
||||
*
|
||||
* @param bytes The bytes - must contain exactly 16 bytes.
|
||||
* @return The new [[MD5]] instance.
|
||||
*/
|
||||
def fromBytes(bytes: Array[Byte]): MD5 =
|
||||
if NumberOfBytes.equal(bytes.length) then
|
||||
bytes
|
||||
else
|
||||
throw IllegalArgumentException(s"MD5 values must be exactly $NumberOfBytes bytes.")
|
||||
|
||||
/**
|
||||
* Calculate the MD5 hash for the given byte array.
|
||||
*
|
||||
* @param data The byte array.
|
||||
* @return The calculated [[MD5]].
|
||||
*/
|
||||
def calculate(data: Array[Byte]): MD5 =
|
||||
MessageDigest.getInstance(Algorithm).digest(data)
|
||||
|
||||
/*
|
||||
* Calculate the MD5 hash for the given string.
|
||||
*
|
||||
* @param data The string data.
|
||||
* @param charset The character set of the string. Defaults to UTF-8.
|
||||
* @return The calculated [[MD5]].
|
||||
*/
|
||||
def calculate(data: String, charset: Charset = StandardCharsets.UTF_8): MD5 =
|
||||
calculate(data.getBytes(charset))
|
||||
|
||||
extension (md5: MD5)
|
||||
/**
|
||||
* @return The underlying byte array.
|
||||
*/
|
||||
def unwrap(): Array[Byte] = md5
|
||||
|
||||
/**
|
||||
* @return The underlying byte array.
|
||||
*/
|
||||
def toBytes(): Array[Byte] = md5
|
||||
|
||||
/**
|
||||
* Get the byte at the given index (0 to 15).
|
||||
*
|
||||
* Throws an exception if an out-of-bound index is given.
|
||||
*
|
||||
* @param index The byte index (0 to 15).
|
||||
* @return The byte at the specified index.
|
||||
*/
|
||||
def getByte(index: Int): Byte =
|
||||
if index < 0 || index >= NumberOfBytes.unwrap() then
|
||||
throw IndexOutOfBoundsException(s"Index $index out of MD5 bound of $NumberOfBytes bytes.")
|
||||
else
|
||||
md5.apply(index)
|
||||
|
||||
/**
|
||||
* Determine if this hash is the same as some other hash. Compares each byte
|
||||
* in order.
|
||||
*
|
||||
* @param other The [[MD5]] to compare against.
|
||||
* @return True if the hashes are identical, false otherwise.
|
||||
*/
|
||||
def isSame(other: MD5): Boolean = md5.sameElements(other)
|
||||
|
||||
/**
|
||||
* @return This hash encoded to a Base64 string.
|
||||
*/
|
||||
def base64(): EncodedString = Base64Encoder.encode(md5)
|
||||
|
||||
/**
|
||||
* @return This hash encoded to a Hexadecimal string.
|
||||
*/
|
||||
def hex(): EncodedString = HexEncoder.encode(md5)
|
||||
|
||||
end MD5
|
||||
81
src/main/scala/gs/std/v0/Nat.scala
Normal file
81
src/main/scala/gs/std/v0/Nat.scala
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package gs.std.v0
|
||||
|
||||
/**
|
||||
* Opaque type for the natural numbers (including 0).
|
||||
*/
|
||||
opaque type Nat = Int
|
||||
|
||||
/**
|
||||
* Opaque type for the natural numbers (including 0).
|
||||
*/
|
||||
object Nat:
|
||||
|
||||
sealed trait Invalid
|
||||
object Invalid extends Invalid
|
||||
|
||||
/**
|
||||
* The number 0.
|
||||
*/
|
||||
final val Zero: Nat = 0
|
||||
|
||||
/**
|
||||
* The number 1.
|
||||
*/
|
||||
final val One: Nat = 1
|
||||
|
||||
/**
|
||||
* Express the given integer as a natural number.
|
||||
*
|
||||
* Throws an `IllegalArgumentException` if a negative value is given as input.
|
||||
*
|
||||
* @param value The input integer.
|
||||
* @return The [[Nat]] instance.
|
||||
*/
|
||||
def apply(value: Int): Nat =
|
||||
if value >= 0 then
|
||||
value
|
||||
else
|
||||
throw new IllegalArgumentException("Nat values must be 0 or greater.")
|
||||
|
||||
def validate(value: Int): Either[Invalid, Nat] =
|
||||
if value >= 0 then Right(value) else Left(Invalid)
|
||||
|
||||
given CanEqual[Nat, Nat] = CanEqual.derived
|
||||
|
||||
given Ordering[Nat] with
|
||||
/** @inheritDocs */
|
||||
def compare(x: Nat, y: Nat): Int = x - y
|
||||
|
||||
extension (nat: Nat)
|
||||
/**
|
||||
* @return The underlying integer.
|
||||
*/
|
||||
def unwrap(): Int = nat
|
||||
|
||||
/**
|
||||
* @return The next integer.
|
||||
*/
|
||||
def next(): Nat = nat + 1
|
||||
|
||||
/**
|
||||
* @return The next integer.
|
||||
*/
|
||||
def increment(): Nat = nat + 1
|
||||
|
||||
/**
|
||||
* Add some natural number to this one.
|
||||
*
|
||||
* @param that The number to add.
|
||||
* @return The sum of the numbers.
|
||||
*/
|
||||
def +(that: Nat): Nat = nat + that
|
||||
|
||||
/**
|
||||
* Multiply this natural number by some other natural number.
|
||||
*
|
||||
* @param that The number to multiply by.
|
||||
* @return The product of the numbers.
|
||||
*/
|
||||
def *(that: Nat): Nat = nat * that
|
||||
|
||||
end Nat
|
||||
105
src/main/scala/gs/std/v0/SHA256.scala
Normal file
105
src/main/scala/gs/std/v0/SHA256.scala
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Opaque type representing a SHA-256 hash.
|
||||
*/
|
||||
opaque type SHA256 = Array[Byte]
|
||||
|
||||
/**
|
||||
* Opaque type representing a SHA-256 hash.
|
||||
*/
|
||||
object SHA256:
|
||||
|
||||
/**
|
||||
* SHA-256 hashes are exactly 32 bytes.
|
||||
*/
|
||||
final val NumberOfBytes: ByteCount = ByteCount(32)
|
||||
|
||||
/**
|
||||
* The algorithm name is "SHA-256".
|
||||
*/
|
||||
final val Algorithm: String = "SHA-256"
|
||||
|
||||
/**
|
||||
* Instantiate a [[SHA256]] from the given byte array. This function does not
|
||||
* know whether the array is actually a calculated hash.
|
||||
*
|
||||
* Typically used for loading pre-validated hashes (e.g. from a database).
|
||||
*
|
||||
* @param bytes The bytes - must contain exactly 32 bytes.
|
||||
* @return The new [[SHA256]] instance.
|
||||
*/
|
||||
def fromBytes(bytes: Array[Byte]): SHA256 =
|
||||
if NumberOfBytes.equal(bytes.length) then
|
||||
bytes
|
||||
else
|
||||
throw IllegalArgumentException(s"SHA-256 values must be exactly $NumberOfBytes bytes.")
|
||||
|
||||
/**
|
||||
* Calculate the SHA-256 hash for the given byte array.
|
||||
*
|
||||
* @param data The byte array.
|
||||
* @return The calculated [[SHA256]].
|
||||
*/
|
||||
def calculate(data: Array[Byte]): SHA256 =
|
||||
MessageDigest.getInstance(Algorithm).digest(data)
|
||||
|
||||
/**
|
||||
* Calculate the SHA-256 hash for the given string.
|
||||
*
|
||||
* @param data The string data.
|
||||
* @param charset The character set of the string. Defaults to UTF-8.
|
||||
* @return The calculated [[SHA256]].
|
||||
*/
|
||||
def calculate(data: String, charset: Charset = StandardCharsets.UTF_8): SHA256 =
|
||||
calculate(data.getBytes(charset))
|
||||
|
||||
extension (sha: SHA256)
|
||||
/**
|
||||
* @return The underlying byte array.
|
||||
*/
|
||||
def unwrap(): Array[Byte] = sha
|
||||
|
||||
/**
|
||||
* @return The underlying byte array.
|
||||
*/
|
||||
def toBytes(): Array[Byte] = sha
|
||||
|
||||
/**
|
||||
* Get the byte at the given index (0 to 31).
|
||||
*
|
||||
* Throws an exception if an out-of-bound index is given.
|
||||
*
|
||||
* @param index The byte index (0 to 31).
|
||||
* @return The byte at the specified index.
|
||||
*/
|
||||
def getByte(index: Int): Byte =
|
||||
if index < 0 || index >= NumberOfBytes.unwrap() then
|
||||
throw IndexOutOfBoundsException(s"Index $index out of SHA-256 bound of $NumberOfBytes bytes.")
|
||||
else
|
||||
sha.apply(index)
|
||||
|
||||
/**
|
||||
* Determine if this hash is the same as some other hash. Compares each byte
|
||||
* in order.
|
||||
*
|
||||
* @param other The [[SHA256]] to compare against.
|
||||
* @return True if the hashes are identical, false otherwise.
|
||||
*/
|
||||
def isSame(other: SHA256): Boolean = sha.sameElements(other)
|
||||
|
||||
/**
|
||||
* @return This hash encoded to a Base64 string.
|
||||
*/
|
||||
def base64(): EncodedString = Base64Encoder.encode(sha)
|
||||
|
||||
/**
|
||||
* @return This hash encoded to a Hexadecimal string.
|
||||
*/
|
||||
def hex(): EncodedString = HexEncoder.encode(sha)
|
||||
|
||||
end SHA256
|
||||
97
src/main/scala/gs/std/v0/Size.scala
Normal file
97
src/main/scala/gs/std/v0/Size.scala
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package gs.std.v0
|
||||
|
||||
/**
|
||||
* Opaque type for collection sizes. Values are guaranteed to be 0 or greater.
|
||||
*/
|
||||
opaque type Size = Int
|
||||
|
||||
/**
|
||||
* Opaque type for collection sizes. Values are guaranteed to be 0 or greater.
|
||||
*/
|
||||
object Size:
|
||||
|
||||
sealed trait Invalid
|
||||
object Invalid extends Invalid
|
||||
|
||||
/**
|
||||
* The size 0.
|
||||
*/
|
||||
final val Zero: Size = 0
|
||||
|
||||
/**
|
||||
* The size 1.
|
||||
*/
|
||||
final val One: Size = 1
|
||||
|
||||
/**
|
||||
* Express the given integer as a size.
|
||||
*
|
||||
* Throws an `IllegalArgumentException` if a negative value is given as input.
|
||||
*
|
||||
* @param value The input integer.
|
||||
* @return The [[Size]] instance.
|
||||
*/
|
||||
def apply(value: Int): Size =
|
||||
if value >= 0 then
|
||||
value
|
||||
else
|
||||
throw new IllegalArgumentException("Size values must be 0 or greater.")
|
||||
|
||||
def validate(value: Int): Either[Invalid, Size] =
|
||||
if value >= 0 then Right(value) else Left(Invalid)
|
||||
|
||||
/**
|
||||
* Express the size of any collection.
|
||||
*
|
||||
* @param iter The collection.
|
||||
* @return The size of the collection.
|
||||
*/
|
||||
def of(iter: Iterable[?]): Size = iter.size
|
||||
|
||||
/**
|
||||
* Express the size of any array.
|
||||
*
|
||||
* @param arr The array.
|
||||
* @return The size of the array.
|
||||
*/
|
||||
def of(arr: Array[?]): Size = arr.length
|
||||
|
||||
given CanEqual[Size, Size] = CanEqual.derived
|
||||
|
||||
given Ordering[Size] with
|
||||
/** @inheritDocs */
|
||||
def compare(x: Size, y: Size): Int = x - y
|
||||
|
||||
extension (size: Size)
|
||||
/**
|
||||
* @return The underlying integer.
|
||||
*/
|
||||
def unwrap(): Int = size
|
||||
|
||||
/**
|
||||
* @return The next value.
|
||||
*/
|
||||
def next(): Size = size + 1
|
||||
|
||||
/**
|
||||
* @return The next value.
|
||||
*/
|
||||
def increment(): Size = size + 1
|
||||
|
||||
/**
|
||||
* Add some size to this one.
|
||||
*
|
||||
* @param that The number to add.
|
||||
* @return The sum of the numbers.
|
||||
*/
|
||||
def +(that: Size): Size = size + that
|
||||
|
||||
/**
|
||||
* Multiply this size by some other size.
|
||||
*
|
||||
* @param that The number to multiply by.
|
||||
* @return The product of the numbers.
|
||||
*/
|
||||
def *(that: Size): Size = size * that
|
||||
|
||||
end Size
|
||||
39
src/main/scala/gs/std/v0/UpdatedAt.scala
Normal file
39
src/main/scala/gs/std/v0/UpdatedAt.scala
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package gs.std.v0
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Opaque type that represents the instant something was updated.
|
||||
*/
|
||||
opaque type UpdatedAt = Instant
|
||||
|
||||
/**
|
||||
* Opaque type that represents the instant something was updated.
|
||||
*/
|
||||
object UpdatedAt:
|
||||
|
||||
/**
|
||||
* Instantiate a new [[UpdatedAt]] from the given `java.time.Instant`.
|
||||
*
|
||||
* @param value The value to semantically represent.
|
||||
* @return The new [[UpdatedAt]].
|
||||
*/
|
||||
def apply(value: Instant): UpdatedAt = value
|
||||
|
||||
given CanEqual[UpdatedAt, UpdatedAt] = CanEqual.derived
|
||||
|
||||
given Ordering[UpdatedAt] = Ordering[Instant]
|
||||
|
||||
extension (updatedAt: Instant)
|
||||
/**
|
||||
* @return The underlying `java.time.Instant`.
|
||||
*/
|
||||
def unwrap(): Instant = updatedAt
|
||||
|
||||
/**
|
||||
* @return The underlying `java.time.Instant`.
|
||||
*/
|
||||
def toInstant(): Instant = updatedAt
|
||||
|
||||
end UpdatedAt
|
||||
|
||||
Loading…
Add table
Reference in a new issue