-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from nationalarchives/TDR-3378_add_basic_structure
Add required build, Dependencies and github workflows
- Loading branch information
Showing
15 changed files
with
782 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
name: Deploy if not a version bump PR | ||
on: | ||
pull_request: | ||
types: | ||
- closed | ||
jobs: | ||
deploy: | ||
runs-on: ubuntu-latest | ||
if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'Version bump') }} | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- run: gh workflow run deploy.yml | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_PAT }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
name: TDR Deploy Metadata Validation | ||
on: | ||
workflow_dispatch: | ||
jobs: | ||
deploy: | ||
uses: nationalarchives/tdr-github-actions/.github/workflows/sbt_release.yml@main | ||
with: | ||
library-name: "Metadata validation" | ||
secrets: | ||
WORKFLOW_PAT: ${{ secrets.WORKFLOW_PAT }} | ||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | ||
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} | ||
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} | ||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | ||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
name: TDR Run Tests | ||
on: | ||
pull_request: | ||
push: | ||
branches-ignore: | ||
- master | ||
- release-* | ||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: nationalarchives/tdr-github-actions/.github/actions/run-git-secrets@main | ||
- uses: nationalarchives/tdr-github-actions/.github/actions/slack-send@main | ||
if: failure() | ||
with: | ||
message: ":warning: Secrets found in repository ${{ inputs.repo-name }}" | ||
slack-url: ${{ secrets.SLACK_WEBHOOK }} | ||
- name: Run tests | ||
run: sbt scalafmtCheckAll test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
version = 3.7.11 | ||
preset = default | ||
runner.dialect = scala213 | ||
maxColumn = 180 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import Dependencies._ | ||
import sbt.url | ||
import sbtrelease.ReleaseStateTransformations._ | ||
|
||
ThisBuild / organization := "uk.gov.nationalarchives" | ||
ThisBuild / organizationName := "National Archives" | ||
|
||
scalaVersion := "2.13.11" | ||
version := version.value | ||
|
||
|
||
ThisBuild / scmInfo := Some( | ||
ScmInfo( | ||
url("https://github.com/nationalarchives/tdr-metadata-validation"), | ||
"[email protected]:nationalarchives/tdr-metadata-validation.git" | ||
) | ||
) | ||
|
||
developers := List( | ||
Developer( | ||
id = "tna-digital-archiving-jenkins", | ||
name = "TNA Digital Archiving", | ||
email = "[email protected]", | ||
url = url("https://github.com/nationalarchives/tdr-metadata-validation") | ||
) | ||
) | ||
|
||
ThisBuild / description := "A library to validate input metadata for Transfer Digital Records" | ||
ThisBuild / licenses := List("MIT" -> new URL("https://choosealicense.com/licenses/mit/")) | ||
ThisBuild / homepage := Some(url("https://github.com/nationalarchives/tdr-metadata-validation")) | ||
|
||
useGpgPinentry := true | ||
publishTo := sonatypePublishToBundle.value | ||
publishMavenStyle := true | ||
|
||
releaseProcess := Seq[ReleaseStep]( | ||
checkSnapshotDependencies, | ||
inquireVersions, | ||
runClean, | ||
runTest, | ||
setReleaseVersion, | ||
commitReleaseVersion, | ||
tagRelease, | ||
releaseStepCommand("publishSigned"), | ||
releaseStepCommand("sonatypeBundleRelease"), | ||
setNextVersion, | ||
commitNextVersion, | ||
pushChanges | ||
) | ||
|
||
resolvers += | ||
"Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" | ||
|
||
lazy val root = (project in file(".")) | ||
.settings( | ||
name := "tdr-metadata-validation", | ||
libraryDependencies ++= Seq( | ||
commonsLang3, | ||
scalaTest % Test, | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import sbt._ | ||
|
||
object Dependencies { | ||
lazy val commonsLang3 = "org.apache.commons" % "commons-lang3" % "3.13.0" | ||
lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.2.12" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sbt.version=1.9.4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") | ||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") | ||
resolvers += Resolver.jcenterRepo | ||
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21") | ||
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") |
161 changes: 161 additions & 0 deletions
161
src/main/scala/uk/gov/nationalarchives/tdr/validation/DataType.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package uk.gov.nationalarchives.tdr.validation | ||
|
||
import uk.gov.nationalarchives.tdr.validation.ErrorCode._ | ||
|
||
import java.time.{LocalDateTime, Year} | ||
import scala.util.control.Exception.allCatch | ||
|
||
sealed trait DataType | ||
|
||
case object Integer extends DataType with Product with Serializable { | ||
def checkValue(value: String, criteria: MetadataCriteria): Option[String] = { | ||
value match { | ||
case "" if criteria.required => Some(EMPTY_VALUE_ERROR) | ||
case t if allCatch.opt(t.toInt).isEmpty => Some(NUMBER_ONLY_ERROR) | ||
case t if t.toInt < 0 => Some(NEGATIVE_NUMBER_ERROR) | ||
case _ => None | ||
} | ||
} | ||
} | ||
|
||
case object DateTime extends DataType with Product with Serializable { | ||
def checkValue(value: String, criteria: MetadataCriteria): Option[String] = { | ||
value match { | ||
case "" if criteria.required => Some(EMPTY_VALUE_ERROR) | ||
case "" if !criteria.required => None | ||
case v => | ||
val date = v.replace("T", "-").split("[-:]") | ||
if (date.length < 6) { | ||
Some(INVALID_DATE_FORMAT_ERROR) | ||
} else { | ||
validate(date(2), date(1), date(0), criteria) | ||
} | ||
} | ||
} | ||
|
||
val isInvalidDay: Int => Boolean = (day: Int) => day < 1 || day > 31 | ||
val isInvalidMonth: Int => Boolean = (month: Int) => month < 1 || month > 12 | ||
val isInvalidYear: Int => Boolean = (year: Int) => year.toString.length != 4 | ||
val isALeapYear: Int => Boolean = (year: Int) => Year.of(year).isLeap | ||
|
||
lazy val monthsWithLessThan31Days: Map[Int, String] = Map( | ||
2 -> "February", | ||
4 -> "April", | ||
6 -> "June", | ||
9 -> "September", | ||
11 -> "November" | ||
) | ||
|
||
private def validate(day: String, month: String, year: String, criteria: MetadataCriteria): Option[String] = { | ||
val emptyDate: Boolean = day.isEmpty && month.isEmpty && year.isEmpty | ||
|
||
emptyDate match { | ||
case false => validateDateValues(day, month, year, criteria) | ||
case true if criteria.required => Some(EMPTY_VALUE_ERROR) | ||
case _ => None | ||
} | ||
} | ||
|
||
private def validateDateValues(day: String, month: String, year: String, criteria: MetadataCriteria): Option[String] = { | ||
val dayError = validateDay(day) | ||
val monthError = if (dayError.isEmpty) validateMonth(month) else dayError | ||
val yearError = if (monthError.isEmpty) validateYear(year) else monthError | ||
val dayForMonthError = if (yearError.isEmpty) checkDayForTheMonthAndYear(day.toInt, month.toInt, year.toInt) else yearError | ||
if (dayForMonthError.isEmpty) checkIfFutureDateIsAllowed(day.toInt, month.toInt, year.toInt, criteria) else dayForMonthError | ||
} | ||
|
||
private def validateDay(day: String): Option[String] = { | ||
day match { | ||
case v if v.isEmpty => Some(EMPTY_VALUE_ERROR_FOR_DAY) | ||
case v if allCatch.opt(v.toInt).isEmpty => Some(NUMBER_ERROR_FOR_DAY) | ||
case v if v.toInt < 0 => Some(NEGATIVE_NUMBER_ERROR_FOR_DAY) | ||
case v if isInvalidDay(v.toInt) => Some(INVALID_NUMBER_ERROR_FOR_DAY) | ||
case _ => None | ||
} | ||
} | ||
|
||
private def validateMonth(month: String): Option[String] = { | ||
month match { | ||
case v if v.isEmpty => Some(EMPTY_VALUE_ERROR_FOR_MONTH) | ||
case v if allCatch.opt(v.toInt).isEmpty => Some(NUMBER_ERROR_FOR_MONTH) | ||
case v if v.toInt < 0 => Some(NEGATIVE_NUMBER_ERROR_FOR_MONTH) | ||
case v if isInvalidMonth(v.toInt) => Some(INVALID_NUMBER_ERROR_FOR_MONTH) | ||
case _ => None | ||
} | ||
} | ||
|
||
private def validateYear(year: String): Option[String] = { | ||
year match { | ||
case v if v.isEmpty => Some(EMPTY_VALUE_ERROR_FOR_YEAR) | ||
case v if allCatch.opt(v.toInt).isEmpty => Some(NUMBER_ERROR_FOR_YEAR) | ||
case v if v.toInt < 0 => Some(NEGATIVE_NUMBER_ERROR_FOR_YEAR) | ||
case v if isInvalidYear(v.toInt) => Some(INVALID_NUMBER_ERROR_FOR_YEAR) | ||
case _ => None | ||
} | ||
} | ||
|
||
private def checkDayForTheMonthAndYear(dayNumber: Int, monthNumber: Int, yearNumber: Int): Option[String] = { | ||
val monthHasLessThan31Days = monthsWithLessThan31Days.contains(monthNumber) | ||
|
||
if (dayNumber > 30 && monthHasLessThan31Days || dayNumber == 30 && monthNumber == 2) { | ||
Some(INVALID_DAY_FOR_MONTH_ERROR) | ||
} else if (dayNumber == 29 && monthNumber == 2 && !isALeapYear(yearNumber)) { | ||
Some(INVALID_DAY_FOR_MONTH_ERROR) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
private def checkIfFutureDateIsAllowed(day: Int, month: Int, year: Int, criteria: MetadataCriteria): Option[String] = | ||
if (!criteria.isFutureDateAllowed && LocalDateTime.now().isBefore(LocalDateTime.of(year, month, day, 0, 0))) { | ||
Some(FUTURE_DATE_ERROR) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
case object Text extends DataType with Product with Serializable { | ||
|
||
def checkValue(value: String, criteria: MetadataCriteria): Option[String] = { | ||
val definedValues = criteria.definedValues | ||
value match { | ||
case "" if criteria.required => Some(EMPTY_VALUE_ERROR) | ||
case v if definedValues.nonEmpty && !criteria.isMultiValueAllowed && v.split(",").length > 1 => Some(MULTI_VALUE_ERROR) | ||
case v if definedValues.nonEmpty && !v.split(",").toList.forall(definedValues.contains) => Some(UNDEFINED_VALUE_ERROR) | ||
case _ => None | ||
} | ||
} | ||
} | ||
|
||
case object Boolean extends DataType with Product with Serializable { | ||
def checkValue(value: String, criteria: MetadataCriteria, requiredMetadata: Option[Metadata]): Option[String] = { | ||
value match { | ||
case "" if criteria.required => | ||
if (isRequiredMetadataIsEmpty(criteria, requiredMetadata)) { | ||
None | ||
} else { | ||
Some(NO_OPTION_SELECTED_ERROR) | ||
} | ||
case v if criteria.requiredProperty.isDefined && requiredMetadata.exists(_.value.isEmpty) => Some(REQUIRED_PROPERTY_IS_EMPTY) | ||
case v if !criteria.definedValues.contains(v) => Some(UNDEFINED_VALUE_ERROR) | ||
case _ => None | ||
} | ||
} | ||
|
||
def isRequiredMetadataIsEmpty(criteria: MetadataCriteria, requiredMetadata: Option[Metadata]): Boolean = { | ||
criteria.requiredProperty.isDefined && requiredMetadata.exists(_.value.isEmpty) | ||
} | ||
} | ||
case object Decimal extends DataType with Product with Serializable | ||
|
||
object DataType { | ||
def get(dataType: String): DataType = { | ||
dataType match { | ||
case "Integer" => Integer | ||
case "DateTime" => DateTime | ||
case "Text" => Text | ||
case "Boolean" => Boolean | ||
case "Decimal" => Decimal | ||
} | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
src/main/scala/uk/gov/nationalarchives/tdr/validation/MetadataCriteria.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package uk.gov.nationalarchives.tdr.validation | ||
|
||
case class Metadata(name: String, value: String) | ||
case class MetadataCriteria( | ||
name: String, | ||
dataType: DataType, | ||
required: Boolean, | ||
isFutureDateAllowed: Boolean, | ||
isMultiValueAllowed: Boolean, | ||
definedValues: List[String], | ||
requiredProperty: Option[String] = None, | ||
dependencies: Option[Map[String, List[MetadataCriteria]]] = None, | ||
defaultValue: Option[String] = None | ||
) | ||
|
||
object MetadataProperty { | ||
val closureType = "ClosureType" | ||
val descriptiveType = "DescriptiveType" | ||
} | ||
|
||
object ErrorCode { | ||
val CLOSURE_STATUS_IS_MISSING = "CLOSURE_STATUS_IS_MISSING" | ||
val CLOSURE_METADATA_EXISTS_WHEN_FILE_IS_OPEN = "CLOSURE_METADATA_EXISTS_WHEN_FILE_IS_OPEN" | ||
val NUMBER_ONLY_ERROR = "NUMBER_ONLY_ERROR" | ||
val NEGATIVE_NUMBER_ERROR = "NEGATIVE_NUMBER_ERROR" | ||
val EMPTY_VALUE_ERROR = "EMPTY_VALUE_ERROR" | ||
val NO_OPTION_SELECTED_ERROR = "NO_OPTION_SELECTED_ERROR" | ||
val INVALID_DATE_FORMAT_ERROR = "INVALID_DATE_FORMAT_ERROR" | ||
val EMPTY_VALUE_ERROR_FOR_DAY = "EMPTY_VALUE_ERROR_FOR_DAY" | ||
val NUMBER_ERROR_FOR_DAY = "NUMBER_ERROR_FOR_DAY" | ||
val NEGATIVE_NUMBER_ERROR_FOR_DAY = "NEGATIVE_NUMBER_ERROR_FOR_DAY" | ||
val INVALID_NUMBER_ERROR_FOR_DAY = "INVALID_NUMBER_ERROR_FOR_DAY" | ||
val EMPTY_VALUE_ERROR_FOR_MONTH = "EMPTY_VALUE_ERROR_FOR_MONTH" | ||
val NUMBER_ERROR_FOR_MONTH = "NUMBER_ERROR_FOR_MONTH" | ||
val NEGATIVE_NUMBER_ERROR_FOR_MONTH = "NEGATIVE_NUMBER_ERROR_FOR_MONTH" | ||
val INVALID_NUMBER_ERROR_FOR_MONTH = "INVALID_NUMBER_ERROR_FOR_MONTH" | ||
val EMPTY_VALUE_ERROR_FOR_YEAR = "EMPTY_VALUE_ERROR_FOR_YEAR" | ||
val NUMBER_ERROR_FOR_YEAR = "NUMBER_ERROR_FOR_YEAR" | ||
val NEGATIVE_NUMBER_ERROR_FOR_YEAR = "NEGATIVE_NUMBER_ERROR_FOR_YEAR" | ||
val INVALID_NUMBER_ERROR_FOR_YEAR = "INVALID_NUMBER_ERROR_FOR_YEAR" | ||
val INVALID_DAY_FOR_MONTH_ERROR = "INVALID_DAY_FOR_MONTH_ERROR" | ||
val FUTURE_DATE_ERROR = "FUTURE_DATE_ERROR" | ||
val MULTI_VALUE_ERROR = "MULTI_VALUE_ERROR" | ||
val UNDEFINED_VALUE_ERROR = "UNDEFINED_VALUE_ERROR" | ||
val REQUIRED_PROPERTY_IS_EMPTY = "REQUIRED_PROPERTY_IS_EMPTY" | ||
} |
Oops, something went wrong.