Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Move builder methods withText and withXml to their own traits #6

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ env:

script:
- sbt "++$TRAVIS_SCALA_VERSION" "nitf${version}/test"
- sbt "++$TRAVIS_SCALA_VERSION" "nitf${version}/doc"

# These directories are cached to S3 at the end of the build
cache:
Expand All @@ -24,6 +25,6 @@ cache:
- $HOME/.sbt

before_cache:
# Cleanup the cached directories to avoid unnecessary cache updates
# Clean up the cached directories to avoid unnecessary cache updates
- find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete
- find $HOME/.sbt -name "*.lock" -print -delete
2 changes: 1 addition & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ object BuildSettings {
name := Metadata.projectName,
crossScalaVersions := Dependencies.scalaVersions,
scalaVersion := Dependencies.scalaVersions.min,
scalacOptions += "-target:jvm-1.8",
scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-target:jvm-1.8"),

dependencyCheckFailBuildOnCVSS := 4
)
Expand Down
1 change: 0 additions & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ object Dependencies {

val scalaTestVersion = "3.0.5"
val scalaTest = "org.scalatest" %% "scalatest" % scalaTestVersion
val scalactic = "org.scalactic" %% "scalactic" % scalaTestVersion

val xmlDiff = "com.github.andyglow" %% "scala-xml-diff" % "2.0.3"

Expand Down
123 changes: 89 additions & 34 deletions src/main/scala/Builders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object `package` {
type NoteType = TypeType2
type PublicationType = TypeType
type TaglineType = TypeType3
type TitleType = Type

val BareNitfNamespace: NamespaceBinding = toScope(None -> defaultScope.uri)

Expand All @@ -42,6 +43,31 @@ trait Builder[T] {
override def toString: String = build.toString
}

trait BlockContentBuilder {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class was moved from line 155.

def withNote(x: Note): this.type = withBlockContent(x)
def withFootnote(x: Fn): this.type = withBlockContent(x)
def withMedia(x: Media): this.type = withBlockContent(x)
def withParagraph(x: P): this.type = withBlockContent(x)
def withTable(x: Table): this.type = withBlockContent(x)
def withBlockQuote(x: Bq): this.type = withBlockContent(x)
def withOrderedList(x: Ol): this.type = withBlockContent(x)
def withPreformatted(x: Pre): this.type = withBlockContent(x)
def withUnorderedList(x: Ul): this.type = withBlockContent(x)
def withDefinitionList(x: Dl): this.type = withBlockContent(x)
def withHorizontalRule(x: Hr): this.type = withBlockContent(x)
def withNitfTable(x: NitfTable): this.type = withBlockContent(x)
def withSubordinateHeadline(x: Hl2): this.type = withBlockContent(x)

def withTextParagraph(x: String, markAsSummary: Boolean = true): this.type = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new helper method in this class. It used to exist as BodyHeadBuilder#withAbstract(String, Boolean).

var paragraphBuilder = new ParagraphBuilder().withText(x)
if (markAsSummary) paragraphBuilder = paragraphBuilder.asSummary
withParagraph(paragraphBuilder.build)
}

protected def withBlockContent[T <: BlockContentOption : CanWriteXML](x: T): this.type = withContent(dataRecord(x))
protected def withContent(x: DataRecord[_]): this.type
}

trait EnrichedTextBuilder {
def withAnchor(x: A): this.type = withEnrichedText(x)
def withChron(x: Chron): this.type = withEnrichedText(x)
Expand All @@ -62,26 +88,66 @@ trait EnrichedTextBuilder {
def withObjectTitle(x: ObjectTitle): this.type = withEnrichedText(x)
def withPronunciation(x: Pronounce): this.type = withEnrichedText(x)
def withVirtualLocation(x: Virtloc): this.type = withEnrichedText(x)
def withXml(x: NodeSeq): this.type = withContent(dataRecord(x))
def withText(x: String): this.type = withContent(dataRecord(x))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This two methods moved into MixedContentBuilder and AnyContentBuilder.


protected def withEnrichedText[T <: EnrichedTextOption : CanWriteXML](x: T): this.type = withContent(dataRecord(x))
protected def withContent(x: DataRecord[_]): this.type
}

trait MixedContentBuilder {
def withText(x: String): this.type = withContent(dataRecord(x))

/** Appends the given XML to the model object.
* Note that this method is _not_ type-safe!
* __No__ validation is performed to verify that the XML matches the expected schema..
*
* @deprecated Use multiple invocations of other methods to construct the data for a type-safe approach
*/
@deprecated("Use multiple invocations of other methods for a type-safe approach", since = "3.x.3")
def withXml(x: NodeSeq): this.type = withContent(dataRecord(x))

protected def withContent(x: DataRecord[_]): this.type

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the implementation is the same every time this trait is extended. Worth providing it as a default implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish I could! Because the implementation calls .copy, I don't think it's possible to do this other than with a macro.

Do you know of another way to do it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw it too late 😩 I suppose you could define an abstract class inheriting from Builder and MixedContentBuilder? That may not be very elegant though...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if I did this, I still won't be able to implement withContent. That's because case classes' .copy() is not defined in a trait.

}

/** An extension point to enable writing custom XML to NITF model objects that support extensions.
* This feature is supported by NITF 3.6 where the model objects contain a special field called ''any'', which acts as
* an extension point for provider-defined properties from other namespaces.
*
* This is an example of how it can be used (with versions 3.6.x only):
* {{{
* import com.gu.nitf.model.builders._
* import com.gu.nitf.scalaxb._
* import scalaxb._
*
* val builder = new NitfBuilder() with AnyExtensionsBuilder {
* protected override def withAny(x: DataRecord[_]) = { build = build.copy(any = build.any :+ x); this }
* }
*
* val nitf = builder.withXml(<my:extension/>).build
* val xml = scalaxb.toXML(nitf, None, None, toScope(Some("my") -> "http://www.example.com/my-extension"))
* }}}
*/
trait AnyContentBuilder { this: Builder[_ <: { def any: Seq[scalaxb.DataRecord[Any]] }] =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy complains about the structural type expression here. However, I don't think there is anything wrong with it. We're not accessing any of the fields of the Builder, and the generic type is erased at runtime anyway.

def withXml(x: NodeSeq): this.type = withAny(dataRecord(x))
protected def withAny(x: DataRecord[_]): this.type
}

class NitfBuilder(var build: Nitf = Nitf(body = Body())) extends Builder[Nitf] {
def withHead(x: Head): this.type = { build = build.copy(head = Option(x)); this }
def withBody(x: Body): this.type = { build = build.copy(body = x); this }
def withHead(x: Head): this.type = { build = build.copy(head = Option(x)); this }
def withUno(x: String): this.type = { build = build.copy(uno = Option(x)); this }
}

class HeadBuilder(var build: Head = Head()) extends Builder[Head] {
def withTitle(x: String): this.type = { build = build.copy(title = Option(x).map(t => Title(Seq(dataRecord(t))))); this }
def withTitle(x: String, titleType: Option[TitleType] = None): this.type = {
build = build.copy(title = Option(x).map(t => Title(Seq(dataRecord(t)), typeValue = titleType)))
this
}
def withDocData(x: Docdata): this.type = { build = build.copy(docdata = Option(x)); this }
def withPublicationData(x: Pubdata): this.type = { build = build.copy(pubdata = build.pubdata :+ x); this }
}

class DocDataBuilder(var build: Docdata = Docdata()) extends Builder[Docdata] {
def withDocId(x: String): this.type = withDocId(DocId(idString = Option(x)))
def withDocId(x: DocId): this.type = withDocDataOption(x)
def withCopyright(x: DocCopyright): this.type = withDocDataOption(x)
def withIssueDate(x: LocalDate): this.type = withDocDataOption(DateIssue(norm = optionalString(x)))
Expand All @@ -91,7 +157,7 @@ class DocDataBuilder(var build: Docdata = Docdata()) extends Builder[Docdata] {
build = build.copy(managementStatus = optionalString(x))
this
}
private def withDocDataOption[T <: DocdataOption : CanWriteXML](x: T): this.type = {
protected def withDocDataOption[T <: DocdataOption : CanWriteXML](x: T): this.type = {
build = build.copy(docdataoption = build.docdataoption :+ dataRecord(x))
this
}
Expand All @@ -115,17 +181,12 @@ class PublicationDataBuilder(var build: Pubdata = Pubdata()) extends Builder[Pub
class BodyBuilder(var build: Body = Body()) extends Builder[Body] {
def withHead(x: BodyHead): this.type = { build = build.copy(bodyHead = Option(x)); this }
def withContent(x: BodyContent): this.type = { build = build.copy(bodyContent = build.bodyContent :+ x); this }
def withEnd(x: BodyEnd): this.type = { build = build.copy(bodyEnd = Option(x)); this }
}

class BodyHeadBuilder(var build: BodyHead = BodyHead()) extends Builder[BodyHead] {
def withByline(x: String): this.type = withByline(Byline(Seq(dataRecord(x))))
def withHeadline(x: String): this.type = withHeadline(new HeadlineBuilder().withPrimaryHeadline(x))
def withAbstract(x: NodeSeq): this.type = withAbstract(Abstract(Seq(dataRecord(x))))
Copy link
Contributor Author

@hosamaly hosamaly Mar 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method was removed in line with the deprecation of withXml.

def withAbstract(x: String, markAsSummary: Boolean = true): this.type = {
var paragraphBuilder = new ParagraphBuilder().withText(x)
if (markAsSummary) paragraphBuilder = paragraphBuilder.asSummary
withAbstract(Abstract(Seq(dataRecord(paragraphBuilder.build))))
}

def withRights(x: Rights): this.type = { build = build.copy(rights = Option(x)); this }
def withSeries(x: Series): this.type = { build = build.copy(series = Option(x)); this }
Expand All @@ -141,37 +202,24 @@ class BodyHeadBuilder(var build: BodyHead = BodyHead()) extends Builder[BodyHead
}
}

class AbstractBuilder(var build: Abstract = Abstract()) extends Builder[Abstract] with BlockContentBuilder {
protected def withContent(x: DataRecord[_]): this.type = {
build = build.copy(abstractoption = build.abstractoption :+ x)
this
}
}

class HeadlineBuilder(var build: Hedline = Hedline(Hl1())) extends Builder[Hedline] {
def withPrimaryHeadline(x: Hl1): this.type = { build = build.copy(hl1 = x); this }
def withPrimaryHeadline(x: String): this.type = { withPrimaryHeadline(new PrimaryHeadlineBuilder(build.hl1).withText(x)) }
def withSubordinateHeadline(x: Hl2): this.type = { build = build.copy(hl2 = build.hl2 :+ x); this }
def withSubordinateHeadline(x: String): this.type = { withSubordinateHeadline(Hl2(Seq(dataRecord(x)))); this }
}

class PrimaryHeadlineBuilder(var build: Hl1 = Hl1()) extends Builder[Hl1] with EnrichedTextBuilder {
class PrimaryHeadlineBuilder(var build: Hl1 = Hl1()) extends Builder[Hl1] with EnrichedTextBuilder with MixedContentBuilder {
protected def withContent(x: DataRecord[_]): this.type = { build = build.copy(mixed = build.mixed :+ x); this }
}

trait BlockContentBuilder {
def withNote(x: Note): this.type = withBlockContent(x)
def withFootnote(x: Fn): this.type = withBlockContent(x)
def withMedia(x: Media): this.type = withBlockContent(x)
def withParagraph(x: P): this.type = withBlockContent(x)
def withTable(x: Table): this.type = withBlockContent(x)
def withBlockQuote(x: Bq): this.type = withBlockContent(x)
def withOrderedList(x: Ol): this.type = withBlockContent(x)
def withPreformatted(x: Pre): this.type = withBlockContent(x)
def withUnorderedList(x: Ul): this.type = withBlockContent(x)
def withDefinitionList(x: Dl): this.type = withBlockContent(x)
def withHorizontalRule(x: Hr): this.type = withBlockContent(x)
def withNitfTable(x: NitfTable): this.type = withBlockContent(x)
def withSubordinateHeadline(x: Hl2): this.type = withBlockContent(x)
def withXml(x: NodeSeq): this.type = withContent(dataRecord(x))

protected def withBlockContent[T <: BlockContentOption : CanWriteXML](x: T): this.type = withContent(dataRecord(x))
protected def withContent(x: DataRecord[_]): this.type
}

class BodyContentBuilder(var build: BodyContent = BodyContent()) extends Builder[BodyContent] with BlockContentBuilder {
def withBlock(x: Block): this.type = withContent(dataRecord(x))
protected override def withContent(x: DataRecord[_]): this.type = {
Expand Down Expand Up @@ -208,6 +256,11 @@ class MediaBuilder(var build: Media) extends Builder[Media] {
}
}

class MediaCaptionBuilder(var build: MediaCaption = MediaCaption())
extends Builder[MediaCaption] with BlockContentBuilder with EnrichedTextBuilder with MixedContentBuilder {
protected override def withContent(x: DataRecord[_]): this.type = { build = build.copy(build.mixed :+ x); this }
}

class MediaMetadataBuilder(var build: MediaMetadata) extends Builder[MediaMetadata] {
def this(name: String) = this(MediaMetadata(name = name))
def this(name: String, value: String) = this(MediaMetadata(name = name, valueAttribute = Option(value)))
Expand All @@ -216,7 +269,7 @@ class MediaMetadataBuilder(var build: MediaMetadata) extends Builder[MediaMetada
def withValue(x: String): this.type = { build = build.copy(valueAttribute = Option(x)); this }
}

class MediaReferenceBuilder(var build: MediaReference = MediaReference()) extends Builder[MediaReference] {
class MediaReferenceBuilder(var build: MediaReference = MediaReference()) extends Builder[MediaReference] with MixedContentBuilder {
def asNoFlow: this.type = { build = build.copy(noflow = Some(NoflowValue)); this }
def withSource(x: URI): this.type = withSource(x.toString)
def withSource(x: String): this.type = { build = build.copy(source = Option(x)); this }
Expand All @@ -232,9 +285,11 @@ class MediaReferenceBuilder(var build: MediaReference = MediaReference()) extend
def withSourceCredit(x: String): this.type = { build = build.copy(sourceCredit = Option(x)); this }
def withTimeUnitOfMeasure(x: String): this.type = { build = build.copy(timeUnitOfMeasure = Option(x)); this }
def withTimeLength(x: Int): this.type = { build = build.copy(time = Option(x.toString)); this }

protected override def withContent(x: DataRecord[_]): this.type = { build = build.copy(build.mixed :+ x); this }
}

class ParagraphBuilder(var build: P = P()) extends Builder[P] with EnrichedTextBuilder {
class ParagraphBuilder(var build: P = P()) extends Builder[P] with EnrichedTextBuilder with MixedContentBuilder {
def asLead: this.type = { build = build.copy(lede = Option("true")); this }
def asSummary: this.type = { build = build.copy(summary = Option("true")); this }
def asOptional: this.type = { build = build.copy(optionalText = Option("true")); this }
Expand Down
2 changes: 1 addition & 1 deletion src/test/scala/BuildersSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class BuildersSpec extends FunSpec {
.withHead(new BodyHeadBuilder()
.withHeadline("News Article")
.withByline("It took a lot of work to get there")
.withAbstract(<p>It wasn't easy, but they <em>never</em> gave up!</p>)
.withAbstract(new AbstractBuilder().withTextParagraph("It wasn't easy, but they never gave up!"))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose you had to remove the em to avoid having to parse the input string. Any chance the input will ever contain tags and/or references?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the <em> because I removed/deprecated the methods that accept XML.

The input can still contain an em, except we'd expect it to be added, type-safely, like this:

.withAbstract(new AbstractBuilder()
  .withParagraph(new ParagraphBuilder()
    .withText("It wasn't easy, but they ")
    .withEmphasis(Em(Seq(dataRecord("never"))))
    .withText(" gave up!")
  )
)

In another project, I intend to create a utility that converts Jsoup nodes to NITF nodes so that we can parse HTML and convert it to NITF.

)
.withContent(new BodyContentBuilder()
.withParagraph(new ParagraphBuilder().withText("It was done, really!"))
Expand Down
2 changes: 1 addition & 1 deletion src/test/scala/TwoWaySpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class TwoWaySpec extends FunSpec {
import TwoWaySpec._

describe("the parser") {
it("should parse and regenerate a sample file") {
it(s"should parse and regenerate a sample file (v$schemaVersion)") {

val example = XML.load(resource(s"nitf-example-$schemaVersion.xml"))
val schemaLocation = example.attribute(namespaces("xsi"), "schemaLocation").get.head.text
Expand Down