Skip to content
This repository has been archived by the owner on Apr 22, 2020. It is now read-only.

Unifies FileType and validation types #252

Merged
merged 3 commits into from
Apr 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions core/src/main/scala/sbtorgpolicies/github/GitHubOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class GitHubOps(owner: String, repo: String, accessToken: Option[String]) {
def readFileContents: IOResult[List[(String, String)]] = {
files.foldLeft[IOResult[List[(String, String)]]](Right(Nil)) {
case (Right(partialResult), file) =>
fileReader.getFileContent(file.getAbsolutePath).map { content =>
fileReader.getFileContent(file.getAbsolutePath) map { content =>
(relativePath(file), content) :: partialResult
}
case (Left(e), _) => Left(e)
Expand Down Expand Up @@ -170,7 +170,7 @@ class GitHubOps(owner: String, repo: String, accessToken: Option[String]) {
def getAllFilesAsGithub4sResponse(dir: File): Github4sResponse[List[File]] = {
val ghio: GHIO[GHResponse[List[File]]] = Free.pure {
fileReader
.fetchFilesRecursively(dir)
.fetchFilesRecursively(List(dir))
.bimap(
e => UnexpectedException(e.getMessage),
v => newGHResult(v)
Expand Down
73 changes: 38 additions & 35 deletions core/src/main/scala/sbtorgpolicies/io/FileReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ import scala.annotation.tailrec

class FileReader {

def getChildPath(parent: File, childPath: String): String =
new File(parent, childPath).getAbsolutePath

def exists(path: String): Boolean =
Either
.catchNonFatal(file(path).exists()) getOrElse false
Expand All @@ -47,44 +44,50 @@ class FileReader {
.catchNonFatal(IO.readBytes(file))
.leftMap(e => IOException(s"Error loading ${file.getAbsolutePath} content", Some(e)))

def fetchFilesRecursivelyFromPath(sourcePath: String, acceptedExtensions: List[String] = Nil): IOResult[List[File]] =
fetchFilesRecursively(sourcePath.toFile, acceptedExtensions)
private[this] val defaultValidDirs: (File) => Boolean = (f: File) => {
!Set("target", "bin", "output").contains(f.getName) && !f.getName.startsWith(".")
}

def fetchFilesRecursivelyFromPath(
sourcePath: String,
isFileSupported: (File) => Boolean = _ => true,
isDirSupported: (File) => Boolean = defaultValidDirs): IOResult[List[File]] =
fetchFilesRecursively(List(sourcePath.toFile))

def fetchFilesRecursively(sourceFile: File, acceptedExtensions: List[String] = Nil): IOResult[List[File]] =
def fetchFilesRecursively(
in: List[File],
isFileSupported: (File) => Boolean = _ => true,
isDirSupported: (File) => Boolean = defaultValidDirs): IOResult[List[File]] =
Either
.catchNonFatal {

@tailrec
def innerRecursive(files: List[File], accum: List[File]): List[File] = {

val acceptedFiles = files filter acceptedExtension
val filesDirectories = files filter (_.isDirectory) flatMap (_.listFiles().toList)

val newAccum = accum ++ acceptedFiles

filesDirectories match {
case Nil => newAccum
case _ => innerRecursive(filesDirectories, newAccum)
def findAllFiles(
in: List[File],
isFileSupported: (File) => Boolean,
isDirSupported: (File) => Boolean,
processedFiles: List[File] = Nil,
processedDirs: List[String] = Nil): List[File] = {

val allFiles: List[File] = processedFiles ++ in.filter(f => f.exists && f.isFile && isFileSupported(f))

in.filter { f =>
f.isDirectory &&
isDirSupported(f) &&
!processedDirs.contains(f.getCanonicalPath)
} match {
case Nil => allFiles
case list =>
val subFiles = list.flatMap(_.listFiles().toList)
findAllFiles(
subFiles,
isFileSupported,
isDirSupported,
allFiles,
processedDirs ++ list.map(_.getCanonicalPath))
}
}

def acceptedExtension(file: File): Boolean =
file.isFile &&
(acceptedExtensions.isEmpty ||
acceptedExtensions.foldLeft(false) { (b, ext) =>
b || file.getName.endsWith(ext)
})

(sourceFile.exists(), sourceFile.isFile, acceptedExtension(sourceFile)) match {
case (false, _, _) =>
Nil
case (_, true, false) =>
Nil
case (_, true, true) =>
List(sourceFile)
case _ =>
innerRecursive(Option(sourceFile.listFiles.toList).toList.flatten, Nil)
}
findAllFiles(in, isFileSupported, isDirSupported)
}
.leftMap(e => IOException(s"Error fetching files recursively from ${sourceFile.getAbsolutePath}", Some(e)))
.leftMap(e => IOException(s"Error fetching files recursively", Some(e)))
}
30 changes: 3 additions & 27 deletions core/src/main/scala/sbtorgpolicies/io/ReplaceTextEngine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,39 +33,15 @@ class ReplaceTextEngine {
val fileReader: FileReader = new FileReader
val fileWriter: FileWriter = new FileWriter

val excludeDirs: Set[String] = Set("target", "bin", "output")

final def replaceBlocks(
startBlockRegex: Regex,
endBlockRegex: Regex,
replacements: Map[String, String],
in: List[File],
isFileSupported: (File) => Boolean): List[ProcessedFile] =
findAllFiles(in, isFileSupported) map {
replaceBlocksInFile(startBlockRegex, endBlockRegex, replacements, _)
}

@tailrec
private[this] final def findAllFiles(
in: List[File],
isFileSupported: (File) => Boolean,
processedFiles: List[File] = Nil,
processedDirs: List[String] = Nil): List[File] = {

val allFiles: List[File] = processedFiles ++ in.filter(f => f.isFile && isFileSupported(f))

in.filter { f =>
f.isDirectory &&
!excludeDirs.contains(f.getName) &&
!f.getName.startsWith(".") &&
!processedDirs.contains(f.getCanonicalPath)
} match {
case Nil => allFiles
case list =>
val subFiles = list.flatMap(_.listFiles().toList)
findAllFiles(subFiles, isFileSupported, allFiles, processedDirs ++ list.map(_.getCanonicalPath))
isFileSupported: (File) => Boolean): IOResult[List[ProcessedFile]] =
fileReader.fetchFilesRecursively(in, isFileSupported) map { files =>
files.map(replaceBlocksInFile(startBlockRegex, endBlockRegex, replacements, _))
}
}

private[this] def replaceBlocksInFile(
startBlockRegex: Regex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,5 @@ trait ValidationFunctions {
validateTasks(content, "after_success", afterSuccessTasks)
}
}

object ValidationFunctions extends ValidationFunctions
4 changes: 3 additions & 1 deletion core/src/main/scala/sbtorgpolicies/templates/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package sbtorgpolicies.templates

import org.joda.time.DateTime
import sbtorgpolicies.rules.ValidationFunction
import sbtorgpolicies.templates.syntax._

import scala.util.matching.Regex
Expand All @@ -41,7 +42,8 @@ case class FileType(
templatePath: String,
outputPath: String,
replacements: Replacements,
fileSections: List[FileSection] = Nil)
fileSections: List[FileSection] = Nil,
validations: List[ValidationFunction] = Nil)

case class FileSection(
appendPosition: AppendPosition,
Expand Down
26 changes: 21 additions & 5 deletions core/src/main/scala/sbtorgpolicies/templates/templates.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import net.jcazevedo.moultingyaml._
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormat
import sbtorgpolicies.model._
import sbtorgpolicies.rules.ValidationFunctions._
import sbtorgpolicies.templates.badges.{BadgeBuilder, BadgeInformation}
import sbtorgpolicies.templates.sectionTemplates._
import sbtorgpolicies.templates.syntax._
Expand Down Expand Up @@ -55,7 +56,8 @@ package object templates {
"year" -> replaceableYear(startYear).asReplaceable,
"organizationName" -> ghSettings.organizationName.asReplaceable,
"organizationHomePage" -> ghSettings.organizationHomePage.asReplaceable
)
),
validations = List(requiredStrings(List(license.name)))
)
}

Expand Down Expand Up @@ -86,6 +88,8 @@ package object templates {
case None => s"[${dev.id}](https://github.com/${dev.id})"
}

def devListStrings(list: List[Dev]): List[String] = list.map(_.id) ++ list.flatMap(_.name)

FileType(
mandatory = true,
overWritable = true,
Expand All @@ -96,7 +100,8 @@ package object templates {
"name" -> projectName.asReplaceable,
"maintainers" -> maintainers.map(devTemplate).asReplaceable,
"contributors" -> contributors.map(devTemplate).asReplaceable
)
),
validations = List(requiredStrings(devListStrings(maintainers ++ contributors)))
)
}

Expand All @@ -117,7 +122,8 @@ package object templates {
"name" -> projectName.asReplaceable,
"organizationName" -> ghSettings.organizationName.asReplaceable,
"licenseName" -> license.name.asReplaceable
)
),
validations = List(requiredStrings(List(projectName, license.name)))
)
}

Expand Down Expand Up @@ -187,6 +193,8 @@ package object templates {
badgeBuilderList.map(_(info)).map(_.asMarkDown.getOrElse("")).mkString(" ").asReplaceable
}

def readmeSections(name: String): List[String] = List(s"$name in the wild")

FileType(
mandatory = true,
overWritable = false,
Expand Down Expand Up @@ -214,7 +222,8 @@ package object templates {
template = badgesSectionTemplate,
replacements = Map("badges" -> replaceableBadges)
)
)
),
validations = List(requiredStrings(readmeSections(projectName)))
)
}

Expand All @@ -230,7 +239,7 @@ package object templates {
)
}

def TravisFileType(crossScalaV: Seq[String]): FileType = {
def TravisFileType(crossScalaV: Seq[String], scriptCICommand: String, afterCISuccessCommand: String): FileType = {

import sbtorgpolicies.model.YamlFormats._

Expand All @@ -242,6 +251,13 @@ package object templates {
outputPath = travisFilePath,
replacements = Map(
"crossScalaVersions" -> crossScalaV.toYaml.prettyPrint.asReplaceable
),
validations = List(
validTravisFile(
crossScalaV,
Seq(scriptCICommand),
Seq(afterCISuccessCommand)
)
)
)
}
Expand Down
13 changes: 11 additions & 2 deletions core/src/test/scala/sbtorgpolicies/github/GitHubOpsTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,12 @@ class GitHubOpsTest extends TestOps {

val files: List[(File, String)] = filesAndContents.map(t => (new File(baseDir, t._1), t._2))

when(fileReaderMock.fetchFilesRecursively(any[File], any[List[String]]))
when(
fileReaderMock
.fetchFilesRecursively(
any[List[File]],
any[Function1[File, Boolean]].apply,
any[Function1[File, Boolean]].apply))
.thenReturn(files.map(_._1).asRight)

files foreach {
Expand Down Expand Up @@ -428,7 +433,11 @@ class GitHubOpsTest extends TestOps {

val ioException: IOException = IOException("Test error")

when(fileReaderMock.fetchFilesRecursively(any[File], any[List[String]]))
when(
fileReaderMock.fetchFilesRecursively(
any[List[File]],
any[Function1[File, Boolean]].apply,
any[Function1[File, Boolean]].apply))
.thenReturn(ioException.asLeft)

val result: Either[OrgPolicyException, Ref] =
Expand Down
20 changes: 10 additions & 10 deletions core/src/test/scala/sbtorgpolicies/rules/RulesTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import sbtorgpolicies.exceptions.ValidationException

class RulesTest extends TestOps {

val vfs = new ValidationFunctions {}
import ValidationFunctions._

test("emptyValidation should return Right for all strings") {

Expand All @@ -44,7 +44,7 @@ class RulesTest extends TestOps {
|End
""".stripMargin

vfs.requiredStrings(randomStrings)(content) shouldBeEq ().validNel
requiredStrings(randomStrings)(content) shouldBeEq ().validNel
}

check(property)
Expand All @@ -62,7 +62,7 @@ class RulesTest extends TestOps {

val realMissing = missing.filterNot(existing.contains).filter(_.nonEmpty)

val result = vfs.requiredStrings(existing ++ realMissing)(content)
val result = requiredStrings(existing ++ realMissing)(content)
if (realMissing.isEmpty) {
result shouldBeEq ().validNel
} else {
Expand Down Expand Up @@ -96,8 +96,8 @@ class RulesTest extends TestOps {
if (s.contains("User 1") && s.contains("User 2") && s.contains("User 3")) validResponse
else ValidationException("").invalidNel

vfs.requiredSection("\\#\\#\\#.*Changelog".r, "\\#\\#\\#".r, changelogValidation)(content) shouldBeEq validResponse
vfs.requiredSection("\\#\\#\\#.*Contributors".r, "\\#\\#\\#".r, contributorsValidation)(content) shouldBeEq validResponse
requiredSection("\\#\\#\\#.*Changelog".r, "\\#\\#\\#".r, changelogValidation)(content) shouldBeEq validResponse
requiredSection("\\#\\#\\#.*Contributors".r, "\\#\\#\\#".r, contributorsValidation)(content) shouldBeEq validResponse
}

test("requiredSection should return Left if the section is not included in the content") {
Expand All @@ -114,7 +114,7 @@ class RulesTest extends TestOps {

def changelogValidation(s: String): ValidationResult = ().validNel

vfs.requiredSection("\\#\\#\\#.*Changelog".r, "\\#\\#\\#".r, changelogValidation)(content) shouldBeEq invalidResponse
requiredSection("\\#\\#\\#.*Changelog".r, "\\#\\#\\#".r, changelogValidation)(content) shouldBeEq invalidResponse
}

test("requiredSection should return Left if the section is not valid") {
Expand All @@ -136,7 +136,7 @@ class RulesTest extends TestOps {
def contributorsValidation(s: String): ValidationResult =
ValidationException("Section not valid").invalidNel

vfs.requiredSection("\\#\\#\\#.*Contributors".r, "\\#\\#\\#".r, contributorsValidation)(content) shouldBeEq invalidResponse
requiredSection("\\#\\#\\#.*Contributors".r, "\\#\\#\\#".r, contributorsValidation)(content) shouldBeEq invalidResponse
}

test("requiredStrings and requiredSection should works as expected when combined") {
Expand All @@ -159,10 +159,10 @@ class RulesTest extends TestOps {
if (s.nonEmpty) validResponse else ValidationException("").invalidNel

def contributorsValidation(s: String): ValidationResult =
vfs.requiredStrings(List("User 1", "User 2", "User 3"))(s)
requiredStrings(List("User 1", "User 2", "User 3"))(s)

vfs.requiredSection("\\#\\#\\#.*Changelog".r, "\\#\\#\\#".r, changelogValidation)(content) shouldBeEq validResponse
vfs.requiredSection("\\#\\#\\#.*Contributors".r, "\\#\\#\\#".r, contributorsValidation)(content) shouldBeEq validResponse
requiredSection("\\#\\#\\#.*Changelog".r, "\\#\\#\\#".r, changelogValidation)(content) shouldBeEq validResponse
requiredSection("\\#\\#\\#.*Contributors".r, "\\#\\#\\#".r, contributorsValidation)(content) shouldBeEq validResponse
}

}
4 changes: 2 additions & 2 deletions project/ProjectPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ object ProjectPlugin extends AutoPlugin {
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.8.0"),
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0"),
addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.6.8"),
addSbtPlugin("com.47deg" % "sbt-dependencies" % "0.1.0"),
addSbtPlugin("com.47deg" % "sbt-microsites" % "0.5.3")
addSbtPlugin("com.47deg" % "sbt-dependencies" % "0.1.1"),
addSbtPlugin("com.47deg" % "sbt-microsites" % "0.5.4")
) ++
ScriptedPlugin.scriptedSettings ++ Seq(
scriptedDependencies := (compile in Test) map { _ =>
Expand Down
3 changes: 0 additions & 3 deletions src/main/scala/sbtorgpolicies/OrgPoliciesKeys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ sealed trait OrgPoliciesSettingsKeys {
"Optional. Directory where are placed the different templates it'll be used. " +
"By default, it'll be the resourcesDirectory + '/org/templates'")

val orgValidationListSetting: SettingKey[List[Validation]] =
settingKey[List[Validation]]("Validation list the plugin must check")

val orgUpdateDocFilesSetting: SettingKey[List[File]] =
settingKey[List[File]]("List of files and directories whose replace blocks will be replaced with the new values.")

Expand Down
Loading