Skip to content

Commit

Permalink
Merge pull request #1 from nationalarchives/TDR-3378_add_basic_structure
Browse files Browse the repository at this point in the history
Add required build, Dependencies and github workflows
  • Loading branch information
vimleshtna authored Oct 13, 2023
2 parents fd2fc72 + 32f0bfb commit c5bc0d1
Show file tree
Hide file tree
Showing 15 changed files with 782 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/build.yml
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 }}
15 changes: 15 additions & 0 deletions .github/workflows/deploy.yml
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 }}
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
.idea
.bsp
target
project/project
project/target
4 changes: 4 additions & 0 deletions .scalafmt.conf
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
61 changes: 61 additions & 0 deletions build.sbt
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,
)
)
6 changes: 6 additions & 0 deletions project/Dependencies.scala
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"
}
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.9.4
5 changes: 5 additions & 0 deletions project/plugins.sbt
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 src/main/scala/uk/gov/nationalarchives/tdr/validation/DataType.scala
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
}
}
}
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"
}
Loading

0 comments on commit c5bc0d1

Please sign in to comment.