mirror of
https://github.com/IRS-Public/direct-file.git
synced 2025-08-16 17:50:53 +00:00
initial commit
This commit is contained in:
parent
2f3ebd6693
commit
e0d5c84451
3413 changed files with 794524 additions and 1 deletions
4
direct-file/fact-graph-scala/.scalafmt.conf
Normal file
4
direct-file/fact-graph-scala/.scalafmt.conf
Normal file
|
@ -0,0 +1,4 @@
|
|||
version = "3.8.3"
|
||||
runner.dialect = scala3
|
||||
maxColumn = 120
|
||||
rewrite.trailingCommas.style = always
|
97
direct-file/fact-graph-scala/README.md
Normal file
97
direct-file/fact-graph-scala/README.md
Normal file
|
@ -0,0 +1,97 @@
|
|||
# Fact Graph Scala
|
||||
|
||||
This repo holds the Scala code that is compiled to Java and transpiled to JS. The project uses
|
||||
|
||||
- Java 21
|
||||
- Scala 3
|
||||
- ScalaJS to transpile to JS
|
||||
- SBT
|
||||
|
||||
## Installation
|
||||
|
||||
- (this section needs some love)
|
||||
- install coursier (will download, and run AdoptOpenJDK11)
|
||||
- change to version 21 with `eval "$(cs java --jvm 21 --env)"`
|
||||
- set up sbt using coursier
|
||||
- install [metals](https://marketplace.visualstudio.com/items?itemName=scalameta.metals) extension for your IDE
|
||||
|
||||
## General Usage
|
||||
|
||||
**Compile** the main sources in `src/main/scala` using `sbt compile`
|
||||
|
||||
**Delete** all generated files in the target directory using `sbt clean`
|
||||
|
||||
To clean and compile, use a one liner: `sbt clean compile`
|
||||
|
||||
**Console Access** Start the scala interpereter `sbt console`
|
||||
|
||||
**Reload** reload the build definition (build.sbt) `sbt reload`
|
||||
|
||||
**Run (not currently a feature)** eventually, using `sbt run` will run the main class for the project
|
||||
|
||||
**Test** using `sbt test` or to test only a single file use `sbt testOnly tinSpec.scala`
|
||||
|
||||
### How to get your Scala changes to the client
|
||||
|
||||
#### Quickstart
|
||||
From `/direct-file/direct-file/fact-graph-scala`
|
||||
|
||||
1. run `sbt` (to drop into the sbt shell)
|
||||
2. Let's say you are working on the Tin component and you updated the TinSpec. It would be quicker to run the TinSpec test ONLY rather than all the tests. This can be accomplished by running `testOnly *TinSpec`. Place any test name after the wildcard (*) and all matches will run. E.g, `testOnly *PinSpec` would run both the PinSpec and the IpPinSpec.
|
||||
3. run `test` to test everything
|
||||
4. run `scalafmt` to format the source
|
||||
5. run `compile`
|
||||
6. run `clean`
|
||||
7. run `fastOptJS` to transpile Scala to JS
|
||||
8. run `exit` to leave the sbt shell
|
||||
9. `cd` back to `df-client-app`
|
||||
10. if the client is running in another terminal, kill it (CTRL+C)
|
||||
11. run `npm run copy-transpiled-js` to get the transpiled files
|
||||
12. run `docker-compose build` to get the latest backend scala changes (depending on the Scala change this could be optional)
|
||||
13. run `npm run start` to start the client
|
||||
14. Load the checklist ( to load the factgraph on the client)
|
||||
15. use `debugFactGraphScala…` to test the new functionality (in the browser console). This is a key step to see if your Scala changes came through. E.g, if I needed to modify the IpPinFactory to allow all zeros to fail, I could do implement the change in Scala. Go through steps 1-10 above and then type `debugFactGraphScala.IpPinFactory('0000000')` in the browser console to see if this fails now (as it should). If it does, you can rest assured that your Scala changes have been transpiled and copied successfully to the factgraph on the client.
|
||||
16. When adding new Scala changes to the client, go to step 8.
|
||||
17. start client integration
|
||||
|
||||
#### Testing in VS Code
|
||||
|
||||
While Step 2 (above) allows us to test an individual file (some set of unit tests), what if we wanted to test just a single unit, ie, a single `describe` block or single `it` block? The following allows us to do that in VS Code:
|
||||
|
||||
1. Open VS code from the `fact-graph-scala` directory
|
||||
2. Click on the Metals extension. You can install it from [here](https://marketplace.visualstudio.com/items?itemName=scalameta.metals)
|
||||
3. Under BUILD COMMANDS - click on `Import build` in Metals
|
||||
4. Under PACKAGES - click on `factGraphJVM-test` folder in the navigator
|
||||
5. Under ONGOING COMPILATIONS - wait for compilations to complete
|
||||
6. Open any \*Spec file that you would like to run a single unit test against. The green arrow should show up along the file's line numbers vertically.
|
||||
|
||||
#### Details
|
||||
|
||||
For full transpilation, run `sbt fullOptJS`. For faster transpilation, run `sbt fastOptJS`. This will create the transpiled files. Copy transpiled files (main.js\*) from this repo to your local `js-factgraph-scala` within the df-client. This can be done two ways:
|
||||
|
||||
- in df-client-app run `npm run copy-transpiled-js` OR
|
||||
- in fact-graph-scala directory run:
|
||||
|
||||
```
|
||||
cp js/target/scala-3.2.0/fact-graph-opt/main.js* ../df-client/js-factgraph-scala/src/
|
||||
```
|
||||
|
||||
Generate the main.mjs and main.mjs.map files into `js/target/scala-3.2.0/fact-graph-opt`: `sbt fullOptJS`
|
||||
|
||||
Note: `mjs` file type is used here to support ES6 modules used in scala tests.
|
||||
For more information on the sbt-dotty plugin, see the [scala3-example-project](https://github.com/scala/scala3-example-project/blob/main/README.md).
|
||||
|
||||
#### Debug the factgraph in JS
|
||||
|
||||
There are some global variables exposed to be run in the console, after the factgraph loads, to troubleshoot:
|
||||
|
||||
```
|
||||
> debugFactGraph
|
||||
> debugFactGraphMeta
|
||||
> debugScalaFactGraphLib
|
||||
```
|
||||
|
||||
### Generating an SBOM
|
||||
|
||||
To generate a Software Bill of Materials (SBOM), run `sbt makeBom`. It will be missing some dependencies
|
||||
that we will have to merge in from our `manual-scala-sbom.xml` (see that file for more information).
|
24
direct-file/fact-graph-scala/TODO
Normal file
24
direct-file/fact-graph-scala/TODO
Normal file
|
@ -0,0 +1,24 @@
|
|||
- catch up on unit tests
|
||||
- Date type
|
||||
- hard validations
|
||||
- Option/MultiOption type
|
||||
- derived MultiOption (i.e. eligible filing statuses)
|
||||
- persist incomplete values
|
||||
- soft validations
|
||||
- cache and subscribe to facts/values
|
||||
- constraints
|
||||
- Metadata on FactDefinition
|
||||
- XML schema
|
||||
- load config from file
|
||||
- JSON persister
|
||||
- PDF export
|
||||
- MeF export
|
||||
- Excel export
|
||||
- cycle detection
|
||||
- clean up exceptions (consistency, messages, replacing throws with Trys)
|
||||
- dictionary/definition versioning
|
||||
- js interop
|
||||
- java interop
|
||||
- documentation
|
||||
- config testing
|
||||
- performance testing
|
51
direct-file/fact-graph-scala/build.sbt
Normal file
51
direct-file/fact-graph-scala/build.sbt
Normal file
|
@ -0,0 +1,51 @@
|
|||
import org.scalajs.linker.interface.OutputPatterns
|
||||
val scala3Version = "3.3.3"
|
||||
|
||||
lazy val root = project
|
||||
.in(file("."))
|
||||
.aggregate(factGraph.js, factGraph.jvm)
|
||||
.settings(
|
||||
name := "fact-graph",
|
||||
version := "0.1.0-SNAPSHOT",
|
||||
organization := "gov.irs.factgraph",
|
||||
scalaVersion := scala3Version,
|
||||
publish := {},
|
||||
publishLocal := {},
|
||||
libraryDependencies += "org.scala-js" %% "scalajs-stubs" % "1.1.0" % "provided",
|
||||
libraryDependencies += "com.lihaoyi" %% "upickle" % "3.1.0",
|
||||
libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.15" % Test,
|
||||
Test / testOptions += Tests.Argument("-oI")
|
||||
)
|
||||
|
||||
// without extra libraries the javascript built is around 400kb.
|
||||
lazy val factGraph = crossProject(JSPlatform, JVMPlatform)
|
||||
.crossType(CrossType.Full)
|
||||
.in(file("."))
|
||||
.settings(
|
||||
name := "fact-graph",
|
||||
version := "0.1.0-SNAPSHOT",
|
||||
organization := "gov.irs.factgraph",
|
||||
scalaVersion := scala3Version,
|
||||
scalaJSLinkerConfig ~= {
|
||||
// We output CommonJSModules, which scalajs will put through the closure compiler in
|
||||
// the `fullLinkJS` step. This outputs approximately 1mb of javascript, whereas the
|
||||
// ESModules are 4mb. I tried outputting ESModules assuming that our build
|
||||
// pipeline would eventually minify them with esbuild. However, esbuild turned
|
||||
// out to be really, really bad at minifying these specific modules. So we're stuck
|
||||
// on common js modules for now.
|
||||
_.withModuleKind(ModuleKind.CommonJSModule)
|
||||
},
|
||||
libraryDependencies += "org.scala-js" %% "scalajs-stubs" % "1.1.0" % "provided",
|
||||
// want to get rid of this eventually.
|
||||
// just don't have time to implement my own dates now!
|
||||
// it adds around 200kb to the built package.
|
||||
libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.5.0",
|
||||
libraryDependencies += "com.lihaoyi" %%% "upickle" % "3.1.0",
|
||||
libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.15" % Test,
|
||||
Test / testOptions += Tests.Argument("-oI")
|
||||
)
|
||||
.jvmSettings(
|
||||
libraryDependencies += "org.scala-js" %% "scalajs-stubs" % "1.1.0" % "provided"
|
||||
)
|
||||
.jsSettings(
|
||||
)
|
|
@ -0,0 +1,44 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import scala.scalajs.js
|
||||
import scala.scalajs.js.annotation.{JSExportTopLevel, JSExport}
|
||||
import gov.irs.factgraph.definitions.fact.{
|
||||
FactConfigElement,
|
||||
WritableConfigElementDigestWrapper,
|
||||
CompNodeConfigDigestWrapper,
|
||||
CompNodeDigestWrapper,
|
||||
}
|
||||
|
||||
@JSExportTopLevel("DigestNodeWrapper")
|
||||
class DigestNodeWrapper(
|
||||
val path: String,
|
||||
val writable: WritableConfigElementDigestWrapper | Null,
|
||||
val derived: CompNodeConfigDigestWrapper | Null,
|
||||
val placeholder: CompNodeConfigDigestWrapper | Null,
|
||||
) extends js.Object:
|
||||
/** A digest-node is the JSON serialization of a Fact, as produced by the direct-file Java application, from the XML
|
||||
* fact dictionary. This wrapper allows us to map digest JSON nodes into FactConfigElements, handling the necessary
|
||||
* type conversion and null safety matches
|
||||
*/
|
||||
def writableOption = this.writable match
|
||||
case null => None
|
||||
case node =>
|
||||
Some(WritableConfigElementDigestWrapper.toNative(node))
|
||||
|
||||
def derivedOption = this.derived match
|
||||
case null => None
|
||||
case _ => Some(CompNodeDigestWrapper.toNative(this.derived))
|
||||
|
||||
def placeholderOption = this.placeholder match
|
||||
case null => None
|
||||
case _ => Some(CompNodeDigestWrapper.toNative(this.placeholder))
|
||||
|
||||
@JSExportTopLevel("DigestNodeWrapperFactory")
|
||||
object DigestNodeWrapper:
|
||||
@JSExport
|
||||
def toNative(wrapper: DigestNodeWrapper) = FactConfigElement(
|
||||
wrapper.path,
|
||||
wrapper.writableOption,
|
||||
wrapper.derivedOption,
|
||||
wrapper.placeholderOption,
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
package gov.irs.factgraph
|
||||
import gov.irs.factgraph.FactDictionary
|
||||
import gov.irs.factgraph.definitions.FactDictionaryConfigTrait
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import gov.irs.factgraph.compnodes.RootNode
|
||||
|
||||
@JSExportTopLevel("FactDictionaryFactory")
|
||||
object JSFactDictionary:
|
||||
def apply(): FactDictionary =
|
||||
val dictionary = new FactDictionary()
|
||||
FactDefinition(RootNode(), Path.Root, Seq.empty, dictionary)
|
||||
dictionary
|
||||
|
||||
@JSExport
|
||||
def fromConfig(e: FactDictionaryConfigTrait): FactDictionary =
|
||||
// A lot like the Scala fact dictionary factory, but uses the JSMeta, which
|
||||
// contains useful methods the frontend needs
|
||||
val dictionary = this()
|
||||
JSMeta.fromConfig(e.meta, dictionary)
|
||||
e.facts.map(FactDefinition.fromConfig(_)(using dictionary))
|
||||
dictionary.freeze()
|
||||
dictionary
|
|
@ -0,0 +1,113 @@
|
|||
package gov.irs.factgraph
|
||||
import gov.irs.factgraph.persisters.*
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel, JSExportAll}
|
||||
import scala.scalajs.js
|
||||
import gov.irs.factgraph.limits.LimitViolation
|
||||
import gov.irs.factgraph.definitions.fact.LimitLevel
|
||||
import gov.irs.factgraph.monads.MaybeVector
|
||||
import gov.irs.factgraph.monads.Result
|
||||
|
||||
@JSExportTopLevel("Graph")
|
||||
@JSExportAll
|
||||
class JSGraph(
|
||||
override val dictionary: FactDictionary,
|
||||
override val persister: Persister,
|
||||
) extends Graph(dictionary, persister):
|
||||
|
||||
def toStringDictionary(): js.Dictionary[String] =
|
||||
// This is a debug function to allow for quick inspection
|
||||
// of the graph
|
||||
import js.JSConverters._
|
||||
this.persister.toStringMap().toJSDictionary
|
||||
|
||||
// Get a fact definition from the fact dictionary
|
||||
// In scala this is graph.apply
|
||||
def getFact(path: String) =
|
||||
import js.JSConverters._
|
||||
root.apply(Path(path)) match
|
||||
case MaybeVector.Single(x) =>
|
||||
x match
|
||||
case Result.Complete(v) => v
|
||||
case Result.Placeholder(v) => v
|
||||
case Result.Incomplete => null
|
||||
|
||||
case MaybeVector.Multiple(vect, c) =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"getFact returned multiple results for path $path, which is unsupported",
|
||||
)
|
||||
|
||||
@JSExport("toJSON")
|
||||
def toJson(indent: Int = -1): String =
|
||||
this.persister.toJson(indent)
|
||||
|
||||
def explainAndSolve(path: String): js.Array[js.Array[String]] =
|
||||
val rawExpl = this.explain(path)
|
||||
import js.JSConverters._
|
||||
return rawExpl.solves.map(l => l.map(p => p.toString).toJSArray).toJSArray
|
||||
|
||||
@JSExport("save")
|
||||
def jsSave(): SaveReturnValue =
|
||||
val rawSave = this.save();
|
||||
import js.JSConverters._
|
||||
return SaveReturnValue(
|
||||
rawSave._1,
|
||||
rawSave._2.map(f => LimitViolationWrapper.fromLimitViolation(f)).toJSArray,
|
||||
)
|
||||
|
||||
@JSExport("checkPersister")
|
||||
def jsCheckPersister(): js.Array[PersisterSyncIssueWrapper] =
|
||||
val raw = this.checkPersister();
|
||||
import js.JSConverters._
|
||||
return raw.map(f => PersisterSyncIssueWrapper.fromPersisterSyncIssue(f)).toJSArray
|
||||
|
||||
@JSExportTopLevel("GraphFactory")
|
||||
object JSGraph:
|
||||
@JSExport("apply")
|
||||
def apply(
|
||||
dictionary: FactDictionary,
|
||||
): JSGraph =
|
||||
this(dictionary, InMemoryPersisterJS.create())
|
||||
|
||||
@JSExport("apply")
|
||||
def apply(
|
||||
dictionary: FactDictionary,
|
||||
persister: Persister,
|
||||
): JSGraph =
|
||||
new JSGraph(dictionary, persister)
|
||||
|
||||
final class SaveReturnValue(
|
||||
val valid: Boolean,
|
||||
val limitViolations: js.Array[LimitViolationWrapper],
|
||||
) extends js.Object
|
||||
|
||||
final class LimitViolationWrapper(
|
||||
var limitName: String,
|
||||
var factPath: String,
|
||||
val level: String,
|
||||
val limit: String,
|
||||
val actual: String,
|
||||
) extends js.Object
|
||||
|
||||
object LimitViolationWrapper {
|
||||
def fromLimitViolation(lv: LimitViolation) =
|
||||
new LimitViolationWrapper(
|
||||
lv.limitName,
|
||||
lv.factPath,
|
||||
lv.LimitLevel.toString(),
|
||||
lv.limit,
|
||||
lv.actual,
|
||||
)
|
||||
}
|
||||
|
||||
final class PersisterSyncIssueWrapper(
|
||||
val path: String,
|
||||
val message: String,
|
||||
) extends js.Object
|
||||
|
||||
object PersisterSyncIssueWrapper {
|
||||
def fromPersisterSyncIssue(issue: PersisterSyncIssue) =
|
||||
new PersisterSyncIssueWrapper(
|
||||
issue.path,
|
||||
issue.message,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package gov.irs.factgraph
|
||||
import gov.irs.factgraph.Meta
|
||||
import gov.irs.factgraph.types.Enum
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import scala.scalajs.js
|
||||
import gov.irs.factgraph.definitions.meta.{EnumDeclarationOptionsTrait, EnumDeclarationTrait}
|
||||
import gov.irs.factgraph.definitions.meta.MetaConfigTrait
|
||||
|
||||
final case class JSEnumDeclarationOptions(val value: String) extends EnumDeclarationOptionsTrait
|
||||
|
||||
final case class JSEnumDelcaration(
|
||||
val id: String,
|
||||
val options: Seq[EnumDeclarationOptionsTrait],
|
||||
) extends EnumDeclarationTrait
|
||||
|
||||
class EnumDeclarationWrapper(val id: String, val options: js.Array[String]) extends js.Object
|
||||
|
||||
@JSExportTopLevel("DigestMetaWrapper")
|
||||
class DigestMetaWrapper(
|
||||
val version: String,
|
||||
) extends js.Object:
|
||||
def toNative(): JSMeta = JSMeta(
|
||||
version,
|
||||
)
|
||||
|
||||
@JSExportTopLevel("Meta")
|
||||
class JSMeta(version: String) extends Meta(version):
|
||||
override def getVersion() =
|
||||
version
|
||||
|
||||
@JSExportTopLevel("MetaFactory")
|
||||
object JSMeta:
|
||||
def empty(): JSMeta = new JSMeta("Invalid")
|
||||
@JSExport
|
||||
def fromConfig(e: MetaConfigTrait, factDictionary: FactDictionary): Unit =
|
||||
factDictionary.addMeta(new JSMeta(e.version))
|
|
@ -0,0 +1,15 @@
|
|||
package gov.irs.factgraph.definitions
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.FactConfigElement
|
||||
import gov.irs.factgraph.definitions.meta.MetaConfigTrait
|
||||
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import scala.scalajs.js
|
||||
@JSExportTopLevel("FactDictionaryConfig")
|
||||
object FactDictionaryConfig:
|
||||
@JSExport
|
||||
def create(
|
||||
meta: MetaConfigTrait,
|
||||
facts: scala.scalajs.js.Array[FactConfigElement],
|
||||
): FactDictionaryConfigElement =
|
||||
new FactDictionaryConfigElement(meta, facts.toSeq)
|
|
@ -0,0 +1,32 @@
|
|||
package gov.irs.factgraph.definitions.fact
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.{
|
||||
CommonOptionConfigTraits,
|
||||
CompNodeConfigElement,
|
||||
OptionConfigTrait,
|
||||
OptionConfig,
|
||||
}
|
||||
|
||||
import scala.scalajs.js
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
|
||||
case class CompNodeConfig(
|
||||
typeName: String,
|
||||
children: Iterable[CompNodeConfigTrait],
|
||||
options: Iterable[OptionConfigTrait],
|
||||
) extends CompNodeConfigTrait
|
||||
|
||||
class CompNodeConfigDigestWrapper(
|
||||
val typeName: String,
|
||||
val children: js.Array[CompNodeConfigDigestWrapper],
|
||||
val options: js.Dictionary[String],
|
||||
) extends js.Object
|
||||
|
||||
object CompNodeDigestWrapper:
|
||||
def toNative(wrapper: CompNodeConfigDigestWrapper): CompNodeConfig =
|
||||
new CompNodeConfig(
|
||||
wrapper.typeName,
|
||||
wrapper.children.toList.map(CompNodeDigestWrapper.toNative(_)),
|
||||
wrapper.options.map((key, value) => OptionConfig.create(key, value)),
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
package gov.irs.factgraph.definitions.fact
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, OptionConfigTrait}
|
||||
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import scala.scalajs.js
|
||||
|
||||
case class OptionConfig(name: String, value: String) extends OptionConfigTrait
|
||||
|
||||
object OptionConfig {
|
||||
def create(name: String, value: String): OptionConfig =
|
||||
OptionConfig(name, value)
|
||||
|
||||
def create(
|
||||
pairs: Iterable[(String, String)],
|
||||
): scala.scalajs.js.Array[OptionConfig] =
|
||||
val array = new scala.scalajs.js.Array[OptionConfig](pairs.size)
|
||||
pairs.map(x => create(x._1, x._2)).foreach(x => array.push(x))
|
||||
array
|
||||
|
||||
def path(path: String): scala.scalajs.js.Array[OptionConfig] =
|
||||
new scala.scalajs.js.Array[OptionConfig] {
|
||||
create(CommonOptionConfigTraits.PATH, path)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package gov.irs.factgraph.definitions.fact
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.WritableConfigElement
|
||||
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import scala.scalajs.js
|
||||
import gov.irs.factgraph.definitions.fact.{LimitConfigTrait, LimitLevel}
|
||||
|
||||
class LimitConfig(
|
||||
val operation: String,
|
||||
val level: LimitLevel,
|
||||
val node: CompNodeConfig,
|
||||
) extends LimitConfigTrait
|
||||
|
||||
class LimitConfigDigestWrapper(
|
||||
val operation: String,
|
||||
val level: String,
|
||||
val node: CompNodeConfigDigestWrapper,
|
||||
) extends js.Object
|
||||
|
||||
class WritableConfigElementDigestWrapper(
|
||||
val typeName: String,
|
||||
val options: js.Dictionary[String],
|
||||
val collectionItemAlias: String | Null,
|
||||
val limits: js.Array[LimitConfigDigestWrapper],
|
||||
) extends js.Object
|
||||
|
||||
object WritableConfigElementDigestWrapper:
|
||||
def makeNativeLimit(limitConfig: LimitConfigDigestWrapper): LimitConfigTrait =
|
||||
val level = LimitLevel.valueOf(limitConfig.level)
|
||||
new LimitConfig(
|
||||
limitConfig.operation,
|
||||
level,
|
||||
CompNodeDigestWrapper.toNative(limitConfig.node),
|
||||
)
|
||||
|
||||
def toNative(
|
||||
wrapper: WritableConfigElementDigestWrapper,
|
||||
): WritableConfigElement =
|
||||
val collectionItemAlias = wrapper.collectionItemAlias match
|
||||
case null => None
|
||||
case _ => Some(wrapper.collectionItemAlias)
|
||||
WritableConfigElement(
|
||||
wrapper.typeName,
|
||||
wrapper.options.map((key, value) => OptionConfig.create(key, value)),
|
||||
wrapper.limits.map((limitWrapper) => makeNativeLimit(limitWrapper)),
|
||||
collectionItemAlias,
|
||||
)
|
|
@ -0,0 +1,42 @@
|
|||
package gov.irs.factgraph.monads
|
||||
import scala.scalajs.js
|
||||
import scala.scalajs.js.annotation.JSExport
|
||||
|
||||
enum JSEither[+A, +B] {
|
||||
case Left(v: A)
|
||||
case Right(v: B)
|
||||
|
||||
@JSExport
|
||||
def left: A | Null = this match
|
||||
case Left(v) => v
|
||||
case _ => null
|
||||
|
||||
@JSExport
|
||||
def right: B | Null = this match
|
||||
case Right(v) => v
|
||||
case _ => null
|
||||
|
||||
@JSExport
|
||||
def isRight: Boolean = this match
|
||||
case Right(_) => true
|
||||
case _ => false
|
||||
|
||||
@JSExport
|
||||
def isLeft: Boolean = !this.isRight
|
||||
|
||||
@JSExport
|
||||
def map[C](f: js.Function1[B, js.Object]): js.Object | Null = this match
|
||||
case Right(v) => f(v)
|
||||
case _ => null
|
||||
|
||||
@JSExport
|
||||
def mapLeftRight[C](
|
||||
lf: js.Function1[A, js.Object],
|
||||
rf: js.Function1[B, js.Object],
|
||||
): js.Object = this match
|
||||
case Left(e) => lf(e)
|
||||
case Right(v) => rf(v)
|
||||
|
||||
// TODO: All of the other useful methods for monads are TK
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package gov.irs.factgraph.persisters
|
||||
import gov.irs.factgraph.types.WritableType
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import scala.scalajs.js
|
||||
|
||||
@JSExportTopLevel("JSPersister")
|
||||
object InMemoryPersisterJS:
|
||||
@JSExport
|
||||
def create(jsonString: String): InMemoryPersister =
|
||||
// We take in a json string so that scala.js can deserialize/read it
|
||||
// into the appropriate TypeContainer classes -- taking in a null-prototype
|
||||
// js object would not work.
|
||||
InMemoryPersister.apply(jsonString)
|
||||
|
||||
@JSExport
|
||||
def create(): InMemoryPersister =
|
||||
InMemoryPersister.apply()
|
|
@ -0,0 +1,63 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.matching.Regex
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
object AddressFactory:
|
||||
@JSExportTopLevel("AddressFactory")
|
||||
def apply(
|
||||
streetAddress: String,
|
||||
city: String,
|
||||
postalCode: String,
|
||||
stateOrProvence: String,
|
||||
streetAddressLine2: String = "",
|
||||
country: String = "United States of America",
|
||||
): JSEither[AddressValidationFailure, Address] = List(
|
||||
streetAddress,
|
||||
streetAddressLine2,
|
||||
city,
|
||||
postalCode,
|
||||
stateOrProvence,
|
||||
country,
|
||||
) match
|
||||
case List(
|
||||
streetAddress: String,
|
||||
streetAddressLine2: String,
|
||||
city: String,
|
||||
postalCode: String,
|
||||
stateOrProvence: String,
|
||||
country: String,
|
||||
) =>
|
||||
Try(
|
||||
new Address(
|
||||
streetAddress,
|
||||
city,
|
||||
postalCode,
|
||||
stateOrProvence,
|
||||
streetAddressLine2,
|
||||
country,
|
||||
),
|
||||
) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(e: AddressValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
AddressValidationFailure(
|
||||
"Invalid Address case 1",
|
||||
None.orNull,
|
||||
AddressFailureReason.InvalidAddress,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
AddressValidationFailure(
|
||||
"Invalid Address case 2",
|
||||
None.orNull,
|
||||
AddressFailureReason.InvalidAddress,
|
||||
),
|
||||
)
|
||||
|
||||
@JSExportTopLevel("formatAddressForHTML")
|
||||
def formatAddressForHTML(addr: Address): String =
|
||||
addr.toString().replace("\n", "<br />")
|
|
@ -0,0 +1,24 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.matching.Regex
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
object BankAccountFactory:
|
||||
@JSExportTopLevel("BankAccountFactory")
|
||||
def apply(
|
||||
accountType: String,
|
||||
routingNumber: String,
|
||||
accountNumber: String,
|
||||
): JSEither[BankAccountValidationFailure, BankAccount] =
|
||||
Try(new BankAccount(accountType, routingNumber, accountNumber)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(e: BankAccountValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
BankAccountValidationFailure(
|
||||
"Something unexpected went wrong",
|
||||
None.orNull,
|
||||
BankAccountFailureReason.InvalidBankAccount,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js
|
||||
import gov.irs.factgraph.types.Collection
|
||||
import java.util.UUID
|
||||
|
||||
object CollectionFactory:
|
||||
@js.annotation.JSExportTopLevel("CollectionFactory")
|
||||
def apply(items: js.Array[String]): Collection =
|
||||
new Collection(
|
||||
items.toVector.map(UUID.fromString(_)),
|
||||
)
|
||||
|
||||
@js.annotation.JSExportTopLevel("convertCollectionToArray")
|
||||
def unapply(c: Collection): js.Array[String] =
|
||||
import js.JSConverters._
|
||||
c.items.toJSArray.map(_.toString())
|
|
@ -0,0 +1,11 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js
|
||||
import gov.irs.factgraph.types.CollectionItem
|
||||
import java.util.UUID
|
||||
|
||||
object CollectionItemFactory:
|
||||
@js.annotation.JSExportTopLevel("CollectionItemFactory")
|
||||
def apply(item: String): CollectionItem =
|
||||
new CollectionItem(
|
||||
UUID.fromString(item),
|
||||
)
|
|
@ -0,0 +1,83 @@
|
|||
package gov.irs.factgraph.types
|
||||
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportAll, JSExportTopLevel}
|
||||
import gov.irs.factgraph.monads.Result
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import gov.irs.factgraph.validation.{ValidationFailure, ValidationFailureReason}
|
||||
import gov.irs.factgraph.Graph
|
||||
|
||||
@JSExportAll
|
||||
enum CollectionItemReferenceFailureReason extends ValidationFailureReason:
|
||||
type UserFriendlyReason = CollectionItemReferenceFailureReason
|
||||
case InvalidItem
|
||||
case EmptyCollection
|
||||
case InvalidCollection
|
||||
case NotACollection
|
||||
|
||||
def toUserFriendlyReason() =
|
||||
this
|
||||
|
||||
@JSExportAll
|
||||
final case class CollectionItemReferenceFailure(
|
||||
private val message: String = "",
|
||||
private val cause: Throwable = None.orNull,
|
||||
val validationMessage: CollectionItemReferenceFailureReason,
|
||||
) extends IllegalArgumentException(message, cause),
|
||||
ValidationFailure[CollectionItemReferenceFailureReason];
|
||||
|
||||
object CollectionItemReferenceFactory:
|
||||
@JSExportTopLevel("CollectionItemReferenceFactory")
|
||||
def apply(
|
||||
value: String,
|
||||
collectionPath: String,
|
||||
factGraph: Graph,
|
||||
): JSEither[CollectionItemReferenceFailure, CollectionItem] =
|
||||
if (value.length > 0) {
|
||||
factGraph.get(collectionPath) match
|
||||
case Result.Incomplete =>
|
||||
JSEither.Left(
|
||||
CollectionItemReferenceFailure(
|
||||
"Attempt to reference an empty collection",
|
||||
None.orNull,
|
||||
CollectionItemReferenceFailureReason.EmptyCollection,
|
||||
),
|
||||
)
|
||||
case Result.Complete(col: Collection) =>
|
||||
col.items.map(_.toString()).contains(value) match
|
||||
case true => JSEither.Right(CollectionItemFactory.apply(value))
|
||||
case false =>
|
||||
JSEither.Left(
|
||||
CollectionItemReferenceFailure(
|
||||
"Attempt to reference item not in collection",
|
||||
None.orNull,
|
||||
CollectionItemReferenceFailureReason.InvalidItem,
|
||||
),
|
||||
)
|
||||
case Result.Placeholder(col: Collection) =>
|
||||
col.items.map(_.toString()).contains(value) match
|
||||
case true => JSEither.Right(CollectionItemFactory.apply(value))
|
||||
case false =>
|
||||
JSEither.Left(
|
||||
CollectionItemReferenceFailure(
|
||||
"Attempt to reference item not in collection",
|
||||
None.orNull,
|
||||
CollectionItemReferenceFailureReason.InvalidItem,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
CollectionItemReferenceFailure(
|
||||
"Attempt to reference a path that isn't a collection",
|
||||
None.orNull,
|
||||
CollectionItemReferenceFailureReason.NotACollection,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
JSEither.Left(
|
||||
CollectionItemReferenceFailure(
|
||||
"Blank Collection Item ID",
|
||||
None.orNull,
|
||||
CollectionItemReferenceFailureReason.InvalidItem,
|
||||
),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.matching.Regex
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.util.{Try, Success, Failure}
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportAll, JSExportTopLevel}
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.lang.{Enum => JavaEnum}
|
||||
import gov.irs.factgraph.validation.{ValidationFailure, ValidationFailureReason}
|
||||
import scala.scalajs.js
|
||||
|
||||
@JSExportAll
|
||||
enum DayFailureReason extends JavaEnum[DayFailureReason], ValidationFailureReason:
|
||||
type UserFriendlyReason = DayFailureReason
|
||||
case InvalidDayDueToLeapYear
|
||||
case InvalidDay
|
||||
case InvalidMonth
|
||||
case InvalidDate
|
||||
case ExceedsMaxLimit
|
||||
case InvalidLimit
|
||||
|
||||
def toUserFriendlyReason() =
|
||||
this // Error reason is already user friendly
|
||||
|
||||
@JSExportAll
|
||||
final case class DayValidationFailure(
|
||||
private val message: String = "",
|
||||
private val cause: Throwable = None.orNull,
|
||||
val validationMessage: DayFailureReason,
|
||||
) extends IllegalArgumentException(message, cause),
|
||||
ValidationFailure[DayFailureReason];
|
||||
|
||||
object DayFactory:
|
||||
|
||||
def checkMax(
|
||||
inputDay: Day,
|
||||
max: js.UndefOr[String] = js.undefined,
|
||||
): JSEither[DayValidationFailure, Day] =
|
||||
var maxDay = Day(max.toOption)
|
||||
|
||||
maxDay match
|
||||
case Some(v) =>
|
||||
if (inputDay >= v) {
|
||||
JSEither.Left(
|
||||
DayValidationFailure(
|
||||
"Max date limit exceeded",
|
||||
None.orNull,
|
||||
DayFailureReason.ExceedsMaxLimit,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
JSEither.Right(inputDay)
|
||||
}
|
||||
case None if max.isDefined =>
|
||||
JSEither.Left(
|
||||
DayValidationFailure(
|
||||
"Invalid limit set",
|
||||
None.orNull,
|
||||
DayFailureReason.InvalidLimit,
|
||||
),
|
||||
)
|
||||
case None =>
|
||||
JSEither.Right(inputDay)
|
||||
|
||||
@JSExportTopLevel("DayFactory")
|
||||
def apply(
|
||||
s: String,
|
||||
max: js.UndefOr[String] = js.undefined,
|
||||
): JSEither[DayValidationFailure, Day] =
|
||||
Try(Day(s)) match
|
||||
case Success(v) =>
|
||||
checkMax(v, max)
|
||||
case Failure(e: DateTimeParseException) =>
|
||||
if (e.getMessage().contains("MonthOfYear"))
|
||||
JSEither.Left(
|
||||
DayValidationFailure(
|
||||
"Invalid Month: out of range (1-12)",
|
||||
None.orNull,
|
||||
DayFailureReason.InvalidMonth,
|
||||
),
|
||||
)
|
||||
else if (e.getMessage().contains("DayOfMonth"))
|
||||
JSEither.Left(
|
||||
DayValidationFailure(
|
||||
"Invalid Day: out of range (1-28/31)",
|
||||
None.orNull,
|
||||
DayFailureReason.InvalidDay,
|
||||
),
|
||||
)
|
||||
else if (e.getMessage().contains("leap year"))
|
||||
JSEither.Left(
|
||||
DayValidationFailure(
|
||||
"Invalid Day due to leap year",
|
||||
None.orNull,
|
||||
DayFailureReason.InvalidDayDueToLeapYear,
|
||||
),
|
||||
)
|
||||
else
|
||||
JSEither.Left(
|
||||
DayValidationFailure(
|
||||
"Invalid Date: invalid day given month",
|
||||
None.orNull,
|
||||
DayFailureReason.InvalidDay,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
DayValidationFailure(
|
||||
"Invalid Date",
|
||||
None.orNull,
|
||||
DayFailureReason.InvalidDate,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,54 @@
|
|||
package gov.irs.factgraph.types
|
||||
|
||||
import java.lang.Enum
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportAll, JSExportTopLevel}
|
||||
import scala.util.matching.Regex
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import gov.irs.factgraph.validation.{ValidationFailure, ValidationFailureReason}
|
||||
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.{Try, Success, Failure}
|
||||
import scala.scalajs.js
|
||||
|
||||
object DollarFactory:
|
||||
@JSExportTopLevel("DollarFactory")
|
||||
def apply(
|
||||
value: String,
|
||||
maxLimit: js.UndefOr[Double] = js.undefined,
|
||||
minLimit: js.UndefOr[Double] = js.undefined,
|
||||
): JSEither[DollarValidationFailure, Dollar] =
|
||||
// maxLimit is not defined or numericValue doesn't exceed the maxlimit
|
||||
Try(Dollar(value, allowRounding = false)) match
|
||||
case Success(v) =>
|
||||
if (maxLimit.isDefined && v > maxLimit.get) {
|
||||
// value exceeds limit
|
||||
return JSEither.Left(
|
||||
DollarValidationFailure(
|
||||
"Value exceeds max limit",
|
||||
None.orNull,
|
||||
DollarFailureReason.ExceedsMaxLimit,
|
||||
),
|
||||
)
|
||||
} else if (minLimit.isDefined && v < minLimit.get) {
|
||||
// value exceeds limit
|
||||
return JSEither.Left(
|
||||
DollarValidationFailure(
|
||||
"Value exceeds min limit",
|
||||
None.orNull,
|
||||
DollarFailureReason.ExceedsMinLimit,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
JSEither.Right(v)
|
||||
}
|
||||
case Failure(e: DollarValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
DollarValidationFailure(
|
||||
"Failed to parse Dollar",
|
||||
exception,
|
||||
DollarFailureReason.InvalidDollar,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,33 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.matching.Regex
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
object EinFactory:
|
||||
private val EinPattern: Regex = """^([0-9]{2})([0-9]{7})$""".r
|
||||
|
||||
@JSExportTopLevel("EinFactory")
|
||||
def applyEin(
|
||||
s: String,
|
||||
): JSEither[EinValidationFailure, Ein] = s match
|
||||
case EinPattern(prefix, serial) =>
|
||||
Try(new Ein(prefix, serial)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(e: EinValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
EinValidationFailure(
|
||||
"Invalid EIN case 1",
|
||||
None.orNull,
|
||||
EinFailureReason.InvalidEin,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
EinValidationFailure(
|
||||
"Invalid EIN case 2",
|
||||
None.orNull,
|
||||
EinFailureReason.InvalidEin,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,56 @@
|
|||
package gov.irs.factgraph.types
|
||||
|
||||
import java.lang.{Enum => JavaEnum}
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportAll, JSExportTopLevel}
|
||||
import scala.util.matching.Regex
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import gov.irs.factgraph.validation.{ValidationFailure, ValidationFailureReason}
|
||||
|
||||
// NOTE: These classes and types are specified just to simplify some front-end logstics
|
||||
// This pattern may need some reconsideration to minimize boilerplate
|
||||
|
||||
@JSExportAll
|
||||
enum EnumFailureReason extends JavaEnum[EnumFailureReason], ValidationFailureReason:
|
||||
type UserFriendlyReason = EnumFailureReason
|
||||
case BlankEnum
|
||||
case InvalidEnum
|
||||
|
||||
def toUserFriendlyReason() =
|
||||
this // Error reason is already user friendly
|
||||
|
||||
@JSExportAll
|
||||
final case class EnumValidationFailure(
|
||||
private val message: String = "",
|
||||
private val cause: Throwable = None.orNull,
|
||||
val validationMessage: EnumFailureReason,
|
||||
) extends IllegalArgumentException(message, cause),
|
||||
ValidationFailure[EnumFailureReason];
|
||||
|
||||
object EnumFactory:
|
||||
@JSExportTopLevel("EnumFactory")
|
||||
def apply(
|
||||
value: String,
|
||||
enumOptionsPath: String,
|
||||
): JSEither[EnumValidationFailure, Enum] =
|
||||
if (value.length > 0) {
|
||||
Try(new Enum(Some(value), enumOptionsPath)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
EnumValidationFailure(
|
||||
"Failed to initialize enum",
|
||||
None.orNull,
|
||||
EnumFailureReason.InvalidEnum,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
JSEither.Left(
|
||||
EnumValidationFailure(
|
||||
"Blank Enum",
|
||||
None.orNull,
|
||||
EnumFailureReason.InvalidEnum,
|
||||
),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.matching.Regex
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
object IpPinFactory:
|
||||
private val IpPinPattern: Regex = """^([0-9]{6})$""".r
|
||||
|
||||
@JSExportTopLevel("IpPinFactory")
|
||||
def applyIpPin(
|
||||
s: String,
|
||||
): JSEither[IpPinValidationFailure, IpPin] = s match
|
||||
case IpPinPattern(ippin) =>
|
||||
Try(new IpPin(ippin)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(e: IpPinValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
IpPinValidationFailure(
|
||||
"Invalid IP PIN case 1",
|
||||
None.orNull,
|
||||
IpPinFailureReason.InvalidIpPin,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
IpPinValidationFailure(
|
||||
"Invalid IP PIN case 2",
|
||||
None.orNull,
|
||||
IpPinFailureReason.InvalidIpPin,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,46 @@
|
|||
package gov.irs.factgraph.types
|
||||
|
||||
import java.lang.{Enum => JavaEnum}
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportAll, JSExportTopLevel}
|
||||
import scala.util.matching.Regex
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import gov.irs.factgraph.validation.{ValidationFailure, ValidationFailureReason}
|
||||
|
||||
// NOTE: These classes and types are specified just to simplify some front-end logstics
|
||||
// This pattern may need some reconsideration to minimize boilerplate
|
||||
|
||||
@JSExportAll
|
||||
enum MultiEnumFailureReason extends JavaEnum[MultiEnumFailureReason], ValidationFailureReason:
|
||||
type UserFriendlyReason = MultiEnumFailureReason
|
||||
case BlankEnum
|
||||
case InvalidEnum
|
||||
|
||||
def toUserFriendlyReason() =
|
||||
this // Error reason is already user friendly
|
||||
|
||||
@JSExportAll
|
||||
final case class MultiEnumValidationFailure(
|
||||
private val message: String = "",
|
||||
private val cause: Throwable = None.orNull,
|
||||
val validationMessage: MultiEnumFailureReason,
|
||||
) extends IllegalArgumentException(message, cause),
|
||||
ValidationFailure[MultiEnumFailureReason];
|
||||
|
||||
object MultiEnumFactory:
|
||||
@JSExportTopLevel("MultiEnumFactory")
|
||||
def apply(
|
||||
value: Set[String],
|
||||
enumOptionsPath: String,
|
||||
): JSEither[MultiEnumValidationFailure, MultiEnum] =
|
||||
Try(new MultiEnum(value, enumOptionsPath)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
MultiEnumValidationFailure(
|
||||
"Failed to initialize multiEnum",
|
||||
None.orNull,
|
||||
MultiEnumFailureReason.InvalidEnum,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,59 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.matching.Regex
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
object PhoneNumberFactory:
|
||||
private val UsPhonePattern: Regex = """^\+1(\d{3})(\d{3})(\d{4})$""".r
|
||||
private val E164Pattern: Regex = """^\+([1-9]{1,3})(\d{1,14})$""".r
|
||||
|
||||
@JSExportTopLevel("UsPhoneNumberFactory")
|
||||
def applyUsPhoneNumber(
|
||||
s: String,
|
||||
): JSEither[E164NumberValidationFailure, UsPhoneNumber] = s match
|
||||
case UsPhonePattern(area, office, line) =>
|
||||
Try(new UsPhoneNumber(area, office, line)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(e: E164NumberValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
E164NumberValidationFailure(
|
||||
"Malformed US Phone Number",
|
||||
None.orNull,
|
||||
E164NumberFailureReason.MalformedPhoneNumber,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
E164NumberValidationFailure(
|
||||
"Malformed US Phone Number",
|
||||
None.orNull,
|
||||
E164NumberFailureReason.MalformedPhoneNumber,
|
||||
),
|
||||
)
|
||||
|
||||
@JSExportTopLevel("InternationalPhoneNumberFactory")
|
||||
def applyInternationalPhoneNumber(
|
||||
s: String,
|
||||
): JSEither[E164NumberValidationFailure, InternationalPhoneNumber] = s match
|
||||
case E164Pattern(country, subscriber) =>
|
||||
Try(new InternationalPhoneNumber(country, subscriber)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(e: E164NumberValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
E164NumberValidationFailure(
|
||||
"Malformed International Phone Number",
|
||||
None.orNull,
|
||||
E164NumberFailureReason.MalformedPhoneNumber,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
E164NumberValidationFailure(
|
||||
"Malformed International Phone Number",
|
||||
None.orNull,
|
||||
E164NumberFailureReason.MalformedPhoneNumber,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,33 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.matching.Regex
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
object PinFactory:
|
||||
private val PinPattern: Regex = """^([0-9]{5})$""".r
|
||||
|
||||
@JSExportTopLevel("PinFactory")
|
||||
def applyPin(
|
||||
s: String,
|
||||
): JSEither[PinValidationFailure, Pin] = s match
|
||||
case PinPattern(pin) =>
|
||||
Try(new Pin(pin)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(e: PinValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
PinValidationFailure(
|
||||
"Invalid PIN case 1",
|
||||
None.orNull,
|
||||
PinFailureReason.InvalidPin,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
PinValidationFailure(
|
||||
"Invalid PIN case 2",
|
||||
None.orNull,
|
||||
PinFailureReason.InvalidPin,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,125 @@
|
|||
package gov.irs.factgraph.types
|
||||
import java.lang.{Enum => JavaEnum}
|
||||
import gov.irs.factgraph.validation.{ValidationFailure, ValidationFailureReason}
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportAll, JSExportTopLevel}
|
||||
import scala.scalajs.js
|
||||
import gov.irs.factgraph.compnodes.{StripChars}
|
||||
|
||||
enum StringFailureReason extends JavaEnum[StringFailureReason], ValidationFailureReason:
|
||||
type UserFriendlyReason = StringFailureReason
|
||||
case InvalidCharacters
|
||||
case InvalidEmployerNameLine2Characters
|
||||
case InvalidEmployerNameLine1Characters
|
||||
case InvalidCharactersNoNumbers
|
||||
case InvalidCharactersNumbersOnly
|
||||
case InvalidForm1099rBox7Codes
|
||||
case InvalidForm1099rBox11Year
|
||||
case InvalidMefRatioType
|
||||
|
||||
def toUserFriendlyReason() =
|
||||
this // Error reason is already user friendly
|
||||
|
||||
@JSExportAll
|
||||
final case class StringValidationFailure(
|
||||
private val message: String = "",
|
||||
private val cause: Throwable = None.orNull,
|
||||
val validationMessage: StringFailureReason,
|
||||
) extends IllegalArgumentException(message, cause),
|
||||
ValidationFailure[StringFailureReason];
|
||||
|
||||
// TODO: This basically replicates the logic of the actual limit processing
|
||||
// to be able raise the validation on a field. This needs a big refactor
|
||||
object StringFactory:
|
||||
val DefaultNamePattern = "[\\sA-Za-z0-9\\-]+"
|
||||
val DefaultNamePatternNoNumbers = "[\\sA-Za-z\\-]+"
|
||||
val EmployerNameLine1Pattern =
|
||||
"(([A-Za-z0-9#\\-\\(\\)]|&|')\\s?)*([A-Za-z0-9#\\-\\(\\)]|&|')"
|
||||
val EmployerNameLine2Pattern =
|
||||
"(([A-Za-z0-9#/%\\-\\(\\)]|&|')\\s?)*([A-Za-z0-9#/%\\-\\(\\)]|&|')"
|
||||
val NumbersOnlyPattern = "[0-9]+"
|
||||
val Form1099rBox7CodesPattern = "([A-HJ-NP-UWa-hj-np-uw1-9])(?!\\1)[A-HJ-NP-UWa-hj-np-uw1-9]?"
|
||||
val Form1099rBox11YearPattern = "[1-2][0-9][0-9][0-9]"
|
||||
val MefRatioTypeAsPercentPattern = "^100(\\.0{1,3})?|[0-9]{1,2}(\\.\\d{1,3})?$"
|
||||
|
||||
def checkMatch(
|
||||
input: String,
|
||||
maybePattern: js.UndefOr[String] = js.undefined,
|
||||
): JSEither[StringValidationFailure, String] =
|
||||
|
||||
/*
|
||||
Define a map of patterns to error messages. These were obtained from what was sent to LimitString
|
||||
on the client side
|
||||
*/
|
||||
val patternValidationMappings = Map(
|
||||
EmployerNameLine2Pattern -> (
|
||||
"Invalid characters for Employer Name field",
|
||||
StringFailureReason.InvalidEmployerNameLine2Characters,
|
||||
),
|
||||
EmployerNameLine1Pattern -> (
|
||||
"Invalid characters for Employer Name field",
|
||||
StringFailureReason.InvalidEmployerNameLine1Characters,
|
||||
),
|
||||
DefaultNamePattern -> (
|
||||
"Invalid characters",
|
||||
StringFailureReason.InvalidCharacters,
|
||||
),
|
||||
DefaultNamePatternNoNumbers -> (
|
||||
"Invalid characters",
|
||||
StringFailureReason.InvalidCharactersNoNumbers,
|
||||
),
|
||||
NumbersOnlyPattern -> (
|
||||
"Invalid characters, numbers only",
|
||||
StringFailureReason.InvalidCharactersNumbersOnly,
|
||||
),
|
||||
Form1099rBox7CodesPattern -> (
|
||||
"Invalid characters, only form 1099-R box 7 codes",
|
||||
StringFailureReason.InvalidForm1099rBox7Codes,
|
||||
),
|
||||
Form1099rBox11YearPattern -> (
|
||||
"Invalid characters, only valid 4-digit year",
|
||||
StringFailureReason.InvalidForm1099rBox11Year,
|
||||
),
|
||||
MefRatioTypeAsPercentPattern -> (
|
||||
"Invalid characters for pecentage field",
|
||||
StringFailureReason.InvalidMefRatioType,
|
||||
),
|
||||
)
|
||||
|
||||
maybePattern.toOption match
|
||||
case Some(pattern) =>
|
||||
if (pattern.r.matches(input)) {
|
||||
JSEither.Right(input)
|
||||
} else {
|
||||
patternValidationMappings.get(pattern) match {
|
||||
case Some((errorMessage, failureReason)) =>
|
||||
JSEither.Left(
|
||||
StringValidationFailure(
|
||||
errorMessage,
|
||||
None.orNull,
|
||||
failureReason,
|
||||
),
|
||||
)
|
||||
case None =>
|
||||
// Default error message and failure reason if pattern not found
|
||||
JSEither.Left(
|
||||
StringValidationFailure(
|
||||
"Invalid characters",
|
||||
None.orNull,
|
||||
StringFailureReason.InvalidCharacters,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
case None => JSEither.Right(input)
|
||||
|
||||
@JSExportTopLevel("StringFactory")
|
||||
def apply(
|
||||
s: String,
|
||||
pattern: js.UndefOr[String] = js.undefined,
|
||||
): JSEither[StringValidationFailure, String] =
|
||||
checkMatch(s, pattern)
|
||||
|
||||
@JSExportTopLevel("stripDisallowedCharacters")
|
||||
def stripDisallowedCharacters(input: String, allow: String): String =
|
||||
StripChars.strip(input, allow)
|
|
@ -0,0 +1,34 @@
|
|||
package gov.irs.factgraph.types
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
import scala.util.matching.Regex
|
||||
import gov.irs.factgraph.monads.JSEither
|
||||
import scala.util.{Try, Success, Failure}
|
||||
|
||||
object TinFactory:
|
||||
private val TinPattern: Regex = """^([0-9]{3})([0-9]{2})([0-9]{4})$""".r
|
||||
|
||||
@JSExportTopLevel("TinFactory")
|
||||
def applyTin(
|
||||
s: String,
|
||||
allowAllZeros: Boolean = false,
|
||||
): JSEither[TinValidationFailure, Tin] = s match
|
||||
case TinPattern(area, group, serial) =>
|
||||
Try(new Tin(area, group, serial, allowAllZeros)) match
|
||||
case Success(v) => JSEither.Right(v)
|
||||
case Failure(e: TinValidationFailure) => JSEither.Left(e)
|
||||
case Failure(exception) =>
|
||||
JSEither.Left(
|
||||
TinValidationFailure(
|
||||
"Invalid TIN case 1",
|
||||
None.orNull,
|
||||
TinFailureReason.InvalidTin,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
JSEither.Left(
|
||||
TinValidationFailure(
|
||||
"Invalid TIN case 2",
|
||||
None.orNull,
|
||||
TinFailureReason.InvalidTin,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,38 @@
|
|||
package gov.irs.factgraph.utils
|
||||
|
||||
import scala.scalajs.js
|
||||
import scala.scalajs.js.annotation.JSExportTopLevel
|
||||
|
||||
@JSExportTopLevel("unwrapScalaOptional")
|
||||
def unwrapScalaOptional[T](opt: Option[T]) =
|
||||
opt match
|
||||
case Some(value) => value
|
||||
case _ => null
|
||||
|
||||
@JSExportTopLevel("scalaListToJsArray")
|
||||
def scalaSeqToJsArray[T](seq: List[T]): js.Array[T] =
|
||||
import js.JSConverters._
|
||||
seq.toJSArray
|
||||
|
||||
@JSExportTopLevel("scalaMapToJsMap")
|
||||
def scalaMapToJsMap[K, V](map: collection.Map[K, V]): js.Map[K, V] =
|
||||
import js.JSConverters._
|
||||
map.toJSMap
|
||||
|
||||
@JSExportTopLevel("jsArrayToScalaList")
|
||||
def jsArrayToScalaList[T](seq: js.Array[T]): List[T] =
|
||||
import js.JSConverters._
|
||||
seq.toList
|
||||
|
||||
import js.JSConverters._
|
||||
seq.toList
|
||||
|
||||
@JSExportTopLevel("jsSetToScalaSet")
|
||||
def jsSetToScalaSet[T](seq: js.Set[T]): Set[T] =
|
||||
import js.JSConverters._
|
||||
seq.toSet
|
||||
|
||||
@JSExportTopLevel("scalaSetToJsSet")
|
||||
def scalaSetToJsSet[T](seq: Set[T]): js.Set[T] =
|
||||
import js.JSConverters._
|
||||
seq.toJSSet
|
|
@ -0,0 +1,32 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.FactDictionary
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigElement
|
||||
import gov.irs.factgraph.monads.Result
|
||||
import org.scalatest.funspec.AnyFunSpec
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
|
||||
class SwitchSpec extends AnyFunSpec:
|
||||
describe("Switch") {
|
||||
it("cannot be empty") {
|
||||
assertThrows[IllegalArgumentException] {
|
||||
CompNode.fromDerivedConfig(new CompNodeConfigElement("Switch"))
|
||||
}
|
||||
}
|
||||
|
||||
it("must contain a boolean condition") {
|
||||
// the behavior present in this test will crash the js application.
|
||||
// it is potentially possible to correct this, but the performance hit
|
||||
// and file size increase could be massive.
|
||||
// For more, see the Undefined Behaviors section
|
||||
// https://www.scala-js.org/doc/semantics.html
|
||||
}
|
||||
|
||||
it("all results must be of the same type") {
|
||||
// the behavior present in this test will crash the js application.
|
||||
// it is potentially possible to correct this, but the performance hit
|
||||
// and file size increase could be massive.
|
||||
// For more, see the Undefined Behaviors section
|
||||
// https://www.scala-js.org/doc/semantics.html
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package gov.irs.factgraph.types
|
||||
|
||||
import org.scalatest.funspec.AnyFunSpec
|
||||
|
||||
class DayFactorySpec extends AnyFunSpec:
|
||||
describe("Day with no limits") {
|
||||
assert(DayFactory("2012-10-10").right.toString === "2012-10-10")
|
||||
}
|
||||
|
||||
describe("Day with limit not exceeded") {
|
||||
assert(
|
||||
DayFactory("2010-10-10", "2010-10-11").right.toString === "2010-10-10"
|
||||
)
|
||||
}
|
||||
|
||||
describe("Day with limit exceeded") {
|
||||
val errorReason = DayFactory(
|
||||
"2010-10-10",
|
||||
"2010-10-10"
|
||||
).left.validationMessage.toUserFriendlyReason().toString()
|
||||
assert(errorReason === "ExceedsMaxLimit")
|
||||
}
|
||||
|
||||
describe("Day with invalid limit set") {
|
||||
val errorReason = DayFactory(
|
||||
"2010-10-10",
|
||||
"2010/10/11"
|
||||
).left.validationMessage.toUserFriendlyReason().toString()
|
||||
assert(errorReason === "InvalidLimit")
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package gov.irs.factgraph.types
|
||||
|
||||
import org.scalatest.funspec.AnyFunSpec
|
||||
|
||||
class DollarFactorySpec extends AnyFunSpec:
|
||||
describe("Dollar with no limits") {
|
||||
assert(DollarFactory.apply("301").right === 301.00)
|
||||
}
|
||||
|
||||
describe("Dollar with limit not exceeded") {
|
||||
assert(DollarFactory.apply("299", 300).right === 299.00)
|
||||
}
|
||||
|
||||
describe("Dollar with max limit exceeded") {
|
||||
assert(
|
||||
DollarFactory
|
||||
.apply("301", 300)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString() === "ExceedsMaxLimit"
|
||||
)
|
||||
}
|
||||
|
||||
describe("Dollar with min limit exceeded") {
|
||||
assert(
|
||||
DollarFactory
|
||||
.apply("10", 300, 100)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString() === "ExceedsMinLimit"
|
||||
)
|
||||
}
|
||||
|
||||
describe("Dollar with min limit exceeded with decimals") {
|
||||
assert(
|
||||
DollarFactory
|
||||
.apply("-10.00", 300, 0)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString() === "ExceedsMinLimit"
|
||||
)
|
||||
}
|
||||
|
||||
describe("Dollar with limit exceeded and non integer string value") {
|
||||
assert(
|
||||
DollarFactory
|
||||
.apply("test", 300)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString() === "InvalidCharacters"
|
||||
)
|
||||
}
|
||||
|
||||
describe("Dollar with decimal Min limit exceeded") {
|
||||
assert(
|
||||
DollarFactory
|
||||
.apply("101.23", 200.00, 101.25)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString() === "ExceedsMinLimit"
|
||||
)
|
||||
}
|
||||
|
||||
describe("Dollar with decimal Max limit exceeded") {
|
||||
assert(
|
||||
DollarFactory
|
||||
.apply("84.45", 84.43)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString() === "ExceedsMaxLimit"
|
||||
)
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
package gov.irs.factgraph.types
|
||||
|
||||
import org.scalatest.funspec.AnyFunSpec
|
||||
import scala.collection.Factory.StringFactory
|
||||
|
||||
class StringFactorySpec extends AnyFunSpec:
|
||||
|
||||
describe("String Factory with no Match limit") {
|
||||
it("one word") {
|
||||
assert(StringFactory.apply("Firstname").right === "Firstname")
|
||||
}
|
||||
it("multiple words") {
|
||||
assert(StringFactory.apply("Company Name").right === "Company Name")
|
||||
}
|
||||
}
|
||||
|
||||
describe("String Factory with DefaultNamePattern Match limit") {
|
||||
it("one word with no invalid characters") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Firstname", StringFactory.DefaultNamePattern)
|
||||
.right === "Firstname"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Company Name", StringFactory.DefaultNamePattern)
|
||||
.right === "Company Name"
|
||||
)
|
||||
}
|
||||
it("one word with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("Firstname$", StringFactory.DefaultNamePattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidCharacters")
|
||||
}
|
||||
it("multiple words with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("Firstname$ Lastname!", StringFactory.DefaultNamePattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidCharacters")
|
||||
}
|
||||
}
|
||||
|
||||
describe("String Factory with EmployerNameLine1Pattern Match limit") {
|
||||
it("one word with no invalid characters") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Firstname", StringFactory.EmployerNameLine1Pattern)
|
||||
.right === "Firstname"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters - test &") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Company & Name", StringFactory.EmployerNameLine1Pattern)
|
||||
.right === "Company & Name"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters - test '") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Matt's & Son", StringFactory.EmployerNameLine1Pattern)
|
||||
.right === "Matt's & Son"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters - test /, ', and & ") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Matt's & Son", StringFactory.EmployerNameLine1Pattern)
|
||||
.right === "Matt's & Son"
|
||||
)
|
||||
}
|
||||
it("one word with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("Son$", StringFactory.EmployerNameLine1Pattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidEmployerNameLine1Characters")
|
||||
}
|
||||
it("multiple words with invalid characters, !") {
|
||||
val errorReason = StringFactory
|
||||
.apply("c/o Matt's & Son!", StringFactory.EmployerNameLine1Pattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidEmployerNameLine1Characters")
|
||||
}
|
||||
it("multiple words with invalid characters, $") {
|
||||
val errorReason = StringFactory
|
||||
.apply("c/o Matt's $ Son", StringFactory.EmployerNameLine1Pattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidEmployerNameLine1Characters")
|
||||
}
|
||||
it("multiple words with two spaces characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("Firstname Lastname", StringFactory.EmployerNameLine1Pattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidEmployerNameLine1Characters")
|
||||
}
|
||||
}
|
||||
|
||||
describe("String Factory with EmployerNameLine2Pattern Match limit") {
|
||||
it("one word with no invalid characters") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Firstname", StringFactory.EmployerNameLine2Pattern)
|
||||
.right === "Firstname"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters - test &") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Company & Name", StringFactory.EmployerNameLine2Pattern)
|
||||
.right === "Company & Name"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters - test '") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Matt's & Son", StringFactory.EmployerNameLine2Pattern)
|
||||
.right === "Matt's & Son"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters - test /, ', and & ") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("c/o Matt's & Son", StringFactory.EmployerNameLine2Pattern)
|
||||
.right === "c/o Matt's & Son"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters - test /, ', %, and & ") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply(
|
||||
"c/o Matt's & Son % Junior",
|
||||
StringFactory.EmployerNameLine2Pattern
|
||||
)
|
||||
.right === "c/o Matt's & Son % Junior"
|
||||
)
|
||||
}
|
||||
it("one word with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("Son$", StringFactory.EmployerNameLine2Pattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidEmployerNameLine2Characters")
|
||||
}
|
||||
it("multiple words with invalid characters, !") {
|
||||
val errorReason = StringFactory
|
||||
.apply("c/o Matt's & Son!", StringFactory.EmployerNameLine2Pattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidEmployerNameLine2Characters")
|
||||
}
|
||||
it("multiple words with invalid characters, $") {
|
||||
val errorReason = StringFactory
|
||||
.apply("c/o Matt's $ Son", StringFactory.EmployerNameLine2Pattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidEmployerNameLine2Characters")
|
||||
}
|
||||
it("multiple words with two spaces characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("Firstname Lastname", StringFactory.EmployerNameLine2Pattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidEmployerNameLine2Characters")
|
||||
}
|
||||
}
|
||||
|
||||
describe("String Factory with Match limit not defined") {
|
||||
it("one word with no invalid characters") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Firstname", "[\\sA-Za-z0-9]+")
|
||||
.right === "Firstname"
|
||||
)
|
||||
}
|
||||
it("multiple words with no invalid characters") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("Company Name", "[\\sA-Za-z0-9]+")
|
||||
.right === "Company Name"
|
||||
)
|
||||
}
|
||||
it("one word with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("First%&'name$", "[\\sA-Za-z0-9]+")
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidCharacters")
|
||||
}
|
||||
it("multiple words with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("Firstname$ Lastname!", "[\\sA-Za-z0-9]+")
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidCharacters")
|
||||
}
|
||||
}
|
||||
|
||||
describe("String Factory with Numbers only Match Limit") {
|
||||
it("one word with no invalid characters") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("123", StringFactory.NumbersOnlyPattern)
|
||||
.right === "123"
|
||||
)
|
||||
}
|
||||
it("one word with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("123F", StringFactory.NumbersOnlyPattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidCharactersNumbersOnly")
|
||||
}
|
||||
it("multiple words with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("123 4", StringFactory.NumbersOnlyPattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidCharactersNumbersOnly")
|
||||
}
|
||||
}
|
||||
|
||||
describe("String Factory with Invalid Form 1099R Year Match Limit") {
|
||||
it("one word with no invalid characters") {
|
||||
assert(
|
||||
StringFactory
|
||||
.apply("1234", StringFactory.Form1099rBox11YearPattern)
|
||||
.right === "1234"
|
||||
)
|
||||
}
|
||||
it("one word with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("123F", StringFactory.Form1099rBox11YearPattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidForm1099rBox11Year")
|
||||
}
|
||||
it("multiple words with invalid characters") {
|
||||
val errorReason = StringFactory
|
||||
.apply("1234 F", StringFactory.Form1099rBox11YearPattern)
|
||||
.left
|
||||
.validationMessage
|
||||
.toUserFriendlyReason()
|
||||
.toString()
|
||||
assert(errorReason === "InvalidForm1099rBox11Year")
|
||||
}
|
||||
}
|
||||
|
||||
describe("String Factory with StripChars") {
|
||||
it("returns a string without an invalid character") {
|
||||
val stripCharsResult = StringFactory
|
||||
.stripDisallowedCharacters("test$", "A-Za-z")
|
||||
assert(
|
||||
stripCharsResult === "test"
|
||||
)
|
||||
}
|
||||
it("returns the same string if it doesn't have invalid characters") {
|
||||
val stripCharsResult = StringFactory
|
||||
.stripDisallowedCharacters("test", "A-Za-z")
|
||||
assert(
|
||||
stripCharsResult === "test"
|
||||
)
|
||||
}
|
||||
it("empty string returns an empty string") {
|
||||
val stripCharsResult = StringFactory
|
||||
.stripDisallowedCharacters("", "A-Za-z")
|
||||
assert(
|
||||
stripCharsResult === ""
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package gov.irs.factgraph.persisters
|
||||
import gov.irs.factgraph.types.WritableType
|
||||
import scala.jdk.CollectionConverters.MapHasAsScala
|
||||
|
||||
object InMemoryPersisterJava:
|
||||
def create(store: java.util.Map[String, WritableType]): InMemoryPersister =
|
||||
new InMemoryPersister(
|
||||
store.asScala.map((x, y) => (gov.irs.factgraph.Path.apply(x), y)).toMap,
|
||||
)
|
||||
|
||||
def create(): InMemoryPersister =
|
||||
InMemoryPersister.apply()
|
|
@ -0,0 +1,166 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.FactDictionary
|
||||
import gov.irs.factgraph.definitions.fact.{
|
||||
CommonOptionConfigTraits,
|
||||
CompNodeConfigElement
|
||||
}
|
||||
import gov.irs.factgraph.monads.Result
|
||||
import org.scalatest.funspec.AnyFunSpec
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import gov.irs.factgraph.definitions.fact.FactConfigElement
|
||||
import gov.irs.factgraph.FactDefinition
|
||||
import gov.irs.factgraph.Graph
|
||||
import gov.irs.factgraph.Path
|
||||
|
||||
class SwitchSpec extends AnyFunSpec:
|
||||
describe("Switch") {
|
||||
it("cannot be empty") {
|
||||
assertThrows[IllegalArgumentException] {
|
||||
CompNode.fromDerivedConfig(new CompNodeConfigElement("Switch"))
|
||||
}
|
||||
}
|
||||
|
||||
it("must contain a boolean condition") {
|
||||
assertThrows[UnsupportedOperationException] {
|
||||
CompNode.fromDerivedConfig(
|
||||
new CompNodeConfigElement(
|
||||
"Switch",
|
||||
Seq(
|
||||
new CompNodeConfigElement(
|
||||
"Case",
|
||||
Seq(
|
||||
new CompNodeConfigElement(
|
||||
"When",
|
||||
Seq(
|
||||
CompNodeConfigElement(
|
||||
"String",
|
||||
Seq.empty,
|
||||
CommonOptionConfigTraits.value("Hello")
|
||||
)
|
||||
)
|
||||
),
|
||||
new CompNodeConfigElement(
|
||||
"Then",
|
||||
Seq(
|
||||
CompNodeConfigElement(
|
||||
"String",
|
||||
Seq.empty,
|
||||
CommonOptionConfigTraits.value("World")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
it("can switch on a dependency") {
|
||||
val dictionary = FactDictionary.apply()
|
||||
val predicateConfig = FactConfigElement(
|
||||
"/predicate",
|
||||
None,
|
||||
Some(new CompNodeConfigElement("True")),
|
||||
None
|
||||
)
|
||||
FactDefinition.fromConfig(predicateConfig)(using dictionary)
|
||||
|
||||
val switchConfig = FactConfigElement(
|
||||
"/test-switch",
|
||||
None,
|
||||
Some(
|
||||
new CompNodeConfigElement(
|
||||
"Switch",
|
||||
Seq(
|
||||
new CompNodeConfigElement(
|
||||
"Case",
|
||||
Seq(
|
||||
new CompNodeConfigElement(
|
||||
"When",
|
||||
Seq(
|
||||
new CompNodeConfigElement(
|
||||
"Dependency",
|
||||
Seq.empty,
|
||||
CommonOptionConfigTraits.path("/predicate")
|
||||
)
|
||||
)
|
||||
),
|
||||
new CompNodeConfigElement(
|
||||
"Then",
|
||||
Seq(
|
||||
CompNodeConfigElement(
|
||||
"String",
|
||||
Seq.empty,
|
||||
CommonOptionConfigTraits.value("World")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
FactDefinition.fromConfig(switchConfig)(using dictionary)
|
||||
val graph = Graph(dictionary)
|
||||
|
||||
assert(graph.get("/predicate") == Result.Complete(true))
|
||||
val thens = graph.get("/test-switch")
|
||||
assert(thens == Result.Complete("World"))
|
||||
}
|
||||
|
||||
it("all results must be of the same type") {
|
||||
assertThrows[UnsupportedOperationException] {
|
||||
CompNode.fromDerivedConfig(
|
||||
new CompNodeConfigElement(
|
||||
"Switch",
|
||||
Seq(
|
||||
new CompNodeConfigElement(
|
||||
"Case",
|
||||
Seq(
|
||||
new CompNodeConfigElement(
|
||||
"When",
|
||||
Seq(new CompNodeConfigElement("False"))
|
||||
),
|
||||
new CompNodeConfigElement(
|
||||
"Then",
|
||||
Seq(
|
||||
CompNodeConfigElement(
|
||||
"Int",
|
||||
Seq.empty,
|
||||
CommonOptionConfigTraits.value("1")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
new CompNodeConfigElement(
|
||||
"Case",
|
||||
Seq(
|
||||
new CompNodeConfigElement(
|
||||
"When",
|
||||
Seq(new CompNodeConfigElement("True"))
|
||||
),
|
||||
new CompNodeConfigElement(
|
||||
"Then",
|
||||
Seq(
|
||||
CompNodeConfigElement(
|
||||
"Dollar",
|
||||
Seq.empty,
|
||||
CommonOptionConfigTraits.value("2.00")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
146
direct-file/fact-graph-scala/manual-scala-sbom.xml
Normal file
146
direct-file/fact-graph-scala/manual-scala-sbom.xml
Normal file
|
@ -0,0 +1,146 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
This is a manual hack because the sbt-bom generator that we use
|
||||
in scala seems to be missing:
|
||||
1. dependencies added via plugin and
|
||||
2. dependencies that come in as `provided`, even when given the
|
||||
appropriate flag.
|
||||
While I'd love to fix that generator, I'd moreso love to get an
|
||||
sbom to the IRS. So I'm writing this hacky sbom that we'll merge
|
||||
with the automatically output sbom.
|
||||
|
||||
I've gathered the transitive dependencies by going through the
|
||||
maven central repository on sonatype.
|
||||
-->
|
||||
<bom version="1" xmlns="http://cyclonedx.org/schema/bom/1.0">
|
||||
<components>
|
||||
<component type="library">
|
||||
<group>org.scala-js</group>
|
||||
<name>scalajs-stubs_3</name>
|
||||
<version>1.1.0</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>BSD New</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.scala-js/scalajs-stubs_3@1.1.0</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
|
||||
<component type="library">
|
||||
<group>org.scala-js</group>
|
||||
<name>sbt-scalajs</name>
|
||||
<version>1.13.0</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache-2.0</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.scala-js/sbt-scalajs@1.13.0</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
|
||||
|
||||
<component type="library">
|
||||
<group>org.scala-js</group>
|
||||
<name>scalajs-env-nodejs_2.12</name>
|
||||
<version>1.4.0</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache-2.0</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.scala-js/scalajs-env-nodejs_2.12@1.4.0</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
|
||||
|
||||
<component type="library">
|
||||
<group>org.scala-js</group>
|
||||
<name>scalajs-js-envs_2.12</name>
|
||||
<version>1.4.0</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache-2.0</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.scala-js/scalajs-js-envs_2.12@1.4.0</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
|
||||
<component type="library">
|
||||
<group>org.scala-js</group>
|
||||
<name>scalajs-linker-interface_2.12</name>
|
||||
<version>1.13.2</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache-2.0</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.scala-js/scalajs-linker-interface_2.12@1.13.2</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
|
||||
<component type="library">
|
||||
<group>org.scala-js</group>
|
||||
<name>scalajs-sbt-test-adapter_2.12</name>
|
||||
<version>1.13.2</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache-2.0</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.scala-js/scalajs-sbt-test-adapter_2.12@1.13.2</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
|
||||
<component type="library">
|
||||
<group>org.scoverage</group>
|
||||
<name>sbt-scoverage</name>
|
||||
<version>2.0.5</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache-2.0</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.scoverage/sbt-scoverage@2.0.5</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
|
||||
|
||||
<component type="library">
|
||||
<group>org.portable-scala</group>
|
||||
<name>sbt-scalajs-crossproject</name>
|
||||
<version>1.2.0</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>BSD3</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.portable-scala/sbt-scalajs-crossproject@1.2.0</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
|
||||
<component type="library">
|
||||
<group>org.portable-scala</group>
|
||||
<name>sbt-platform-deps</name>
|
||||
<version>1.0.0</version>
|
||||
<scope>required</scope>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>BSD3</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:maven/org.portable-scala/sbt-platform-deps@1.0.0</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
1
direct-file/fact-graph-scala/project/build.properties
Normal file
1
direct-file/fact-graph-scala/project/build.properties
Normal file
|
@ -0,0 +1 @@
|
|||
sbt.version=1.9.2
|
9
direct-file/fact-graph-scala/project/plugins.sbt
Normal file
9
direct-file/fact-graph-scala/project/plugins.sbt
Normal file
|
@ -0,0 +1,9 @@
|
|||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.5")
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2")
|
||||
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
|
||||
// This plugin is currently the only sbom builder for scala
|
||||
// but even then, it looks like it has limited usage.
|
||||
// If this plugin ever gives us issues, we should
|
||||
// be open to re-evaluating it.
|
||||
addSbtPlugin("io.github.siculo" %% "sbt-bom" % "0.3.0")
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
|
|
@ -0,0 +1,94 @@
|
|||
import gov.irs.factgraph.{FactDictionary, Graph}
|
||||
import gov.irs.factgraph.persisters.InMemoryPersister
|
||||
import gov.irs.factgraph.types.Dollar
|
||||
|
||||
// ###############################
|
||||
// ## 1. What's a Fact Graph? ##
|
||||
// ###############################
|
||||
//
|
||||
// Take a look at [Form 1040][1]. Line 12c asks the taxpayer to add lines 12a
|
||||
// and 12b. This is a common pattern on tax forms.
|
||||
//
|
||||
// [1]: https://www.irs.gov/pub/irs-pdf/f1040.pdf
|
||||
//
|
||||
// There is a relationship betwen lines 12a, 12b, and 12c. In order to know what
|
||||
// should be entered in 12c, we need to first know the values of 12a and 12b.
|
||||
//
|
||||
// This pattern might remind you of a spreadsheet. If we were to transcribe Form
|
||||
// 1040 into Excel, we might imagine that line 12c would be a formula like
|
||||
// =SUM(Line12A,Line12B). Excel spreadsheets are _declarative_, which is to say
|
||||
// we define the relationships between cells using formulas, but we leave it up
|
||||
// to Excel to determine how to solve the spreadsheet.
|
||||
//
|
||||
// Excel models the relationships between cells as a _directed acyclic graph_
|
||||
// (DAG). "Directed" means that the relationships are one way: the fact that
|
||||
// line 12c references line 12a does not affect the value 12a. "Acyclic" means
|
||||
// that these references can never form a loop: if the value of line 12a were to
|
||||
// depend on line 12c while 12c depends on 12a, Excel wouldn't know where to
|
||||
// start solving the spreadsheet; indeed, it would show an error.
|
||||
//
|
||||
// A Fact Graph is a declarative way of applying a complex set of rules, like
|
||||
// tax law, to partial information entered by a user. A Fact Graph is a DAG,
|
||||
// just like Excel. Instead of calling the vertices of the DAG "cells," we'll
|
||||
// call them "facts."
|
||||
//
|
||||
// Let's take the example of lines 12a–c from Form 1040. First, we'll create a
|
||||
// FactDictionary to define our facts.
|
||||
|
||||
val dictionary = FactDictionary.fromXml(
|
||||
<Dictionary>
|
||||
<Fact path="/line12a">
|
||||
<Writable><Dollar /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/line12b">
|
||||
<Writable><Dollar /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/line12c">
|
||||
<Derived>
|
||||
<Add>
|
||||
<Dependency path="../line12a" />
|
||||
<Dependency path="../line12b" />
|
||||
</Add>
|
||||
</Derived>
|
||||
</Fact>
|
||||
</Dictionary>
|
||||
)
|
||||
|
||||
// Lines 12a and 12b are Writable Facts. Each of these Facts stores a
|
||||
// user-entered value, in this case a Dollar amount.
|
||||
//
|
||||
// Line 12c is a Derived Fact. It adds lines 12a and 12b. As you might expect,
|
||||
// it also holds a Dollar amount; we don't need to specify that it is a Dollar
|
||||
// because the Fact Graph infers this from the definition.
|
||||
//
|
||||
// Let's hook up this FactDictionary in a new Graph.
|
||||
|
||||
val graph = Graph(
|
||||
dictionary,
|
||||
InMemoryPersister(
|
||||
"/line12a" -> Dollar("12550.00"),
|
||||
"/line12b" -> Dollar("300.00")
|
||||
)
|
||||
)
|
||||
|
||||
// We've provided the Graph with a Persister seeded with values for lines 12a
|
||||
// and 12b. As a result, we're all set to ask our Graph for an answer to the
|
||||
// value of line 12c.
|
||||
|
||||
graph.get("/line12c")
|
||||
|
||||
// Voila! Our first Fact Graph!
|
||||
//
|
||||
// Okay, so this is pretty basic. But from Facts just like this, we'll be able
|
||||
// to represent a system as complex as the Internal Revenue Code.
|
||||
//
|
||||
// (It's also worth noting that a real Fact Graph wouldn't ask the user to
|
||||
// specify the value of line 12a, for example. 12a would itself be a Derived
|
||||
// Fact, drawing from the taxpayer's filing status, age, etc. A real Fact Graph
|
||||
// would describe a quite deeply nested tree of Facts.)
|
||||
//
|
||||
// You may notice that the value of line 12c is wrapped in something called a
|
||||
// Result, and it's marked as "complete." This is a key concept of the Fact
|
||||
// Graph, as we'll explore in the next chapter.
|
|
@ -0,0 +1,185 @@
|
|||
import gov.irs.factgraph.{FactDictionary, Graph}
|
||||
import gov.irs.factgraph.persisters.InMemoryPersister
|
||||
|
||||
// #######################
|
||||
// ## 2. Completeness ##
|
||||
// #######################
|
||||
//
|
||||
// Let's create another FactDictionary.
|
||||
|
||||
val dictionary = FactDictionary.fromXml(
|
||||
<Dictionary>
|
||||
<Fact path="/factA">
|
||||
<Writable><Boolean /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/factB">
|
||||
<Writable><Boolean /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/factC">
|
||||
<Writable><Boolean /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/conclusion">
|
||||
<Derived>
|
||||
<All>
|
||||
<Dependency path="../factA" />
|
||||
<Dependency path="../factB" />
|
||||
<Dependency path="../factC" />
|
||||
</All>
|
||||
</Derived>
|
||||
</Fact>
|
||||
</Dictionary>
|
||||
)
|
||||
|
||||
// Instead of using Dollar amounts, our Facts are now Boolean (true/false)
|
||||
// values. And instead of our Derived Fact performing an Add operation, it now
|
||||
// performs an All. As you might expect, the conclusion is true if all of facts
|
||||
// A–C are true.
|
||||
|
||||
val graph = Graph(
|
||||
dictionary,
|
||||
InMemoryPersister(
|
||||
"/factA" -> true,
|
||||
"/factB" -> true,
|
||||
"/factC" -> false
|
||||
)
|
||||
)
|
||||
|
||||
graph.get("/conclusion")
|
||||
|
||||
// If you're familiar with tax concepts, you probably have some ideas for how
|
||||
// the All operation might be useful to us. For example, in order to claim a
|
||||
// child as a dependent, they must pass six tests. In other words, if six Facts
|
||||
// are all true, a Fact representing the dependent's claimability is also true.
|
||||
//
|
||||
// In the previous chapter, we described a Fact Graph as a declarative way of
|
||||
// applying complex rules to _partial_ information entered by a user. Thus far,
|
||||
// we have only looked at Fact Graphs where all of the Facts are known. Let's
|
||||
// change that.
|
||||
|
||||
graph.delete("/factC")
|
||||
graph.save()
|
||||
|
||||
graph.get("/factC")
|
||||
|
||||
// Pop quiz: now that fact C is incomplete, what should the value of the
|
||||
// conclusion be?
|
||||
//
|
||||
// Hint: there are no wrong answers.
|
||||
//
|
||||
// You might argue that the conclusion should be false. We said above that the
|
||||
// conclusion is true if all of its children are true, and fact C is missing,
|
||||
// not true; therefore not all of the children are true, and the conclusion
|
||||
// should be false.
|
||||
//
|
||||
// You might argue that the conclusion should be incomplete. The problem is our
|
||||
// lack of omniscience. Fact C represents a truth observable in the real world,
|
||||
// and thus the real value of the conclusion is knowable; it's just not known to
|
||||
// us. Until we have full knowledge, we should report that we don't know.
|
||||
//
|
||||
// You might even argue that the conclusion should be true, given that at least
|
||||
// the set of facts that we know thus far are all true.
|
||||
//
|
||||
// All of these possibilities are valid. Indeed, as a taxpayer completes a tax
|
||||
// return, we'll want the All operation to adopt each of these behaviors in
|
||||
// various situations.
|
||||
//
|
||||
// For example, we might imagine presenting the taxpayer with a running tally of
|
||||
// refund owed or balance due. There will be a set of tax credits for which we
|
||||
// have not yet determined the taxpayer's eligibility. In this case, it would be
|
||||
// far better to assume the taxpayer is ineligible until we can conclusively
|
||||
// prove their eligibility, rather than presuming eligilibity and taking credits
|
||||
// away from the taxpayer one by one.
|
||||
//
|
||||
// Conversely, we might imagine a Fact that represents whether the taxpayer's
|
||||
// situation is in scope for the product. Here, we want to presume exactly the
|
||||
// opposite, that the taxpayer is indeed eligible to use the product until we
|
||||
// identify some circumstance that the tool does not support.
|
||||
//
|
||||
// This ambiguity is a signal that we have not fully understood the problem at
|
||||
// hand. A Fact Graph is not just calculating values; it is also calculating
|
||||
// _completeness_. Let's check the value of our conclusion.
|
||||
|
||||
graph.get("/conclusion")
|
||||
|
||||
// As you can see, we default to reporting that the value of the result is
|
||||
// missing ("???"). This result is also, as should be expected, marked as
|
||||
// incomplete. But because we have separated these concepts of value and
|
||||
// completeness, we could have instead defined the Fact as:
|
||||
|
||||
<Fact path="/conclusion">
|
||||
<Derived>
|
||||
<All>
|
||||
<Dependency path="../factA" />
|
||||
<Dependency path="../factB" />
|
||||
<Dependency path="../factC" />
|
||||
</All>
|
||||
</Derived>
|
||||
<Placeholder><False /></Placeholder>
|
||||
</Fact>
|
||||
|
||||
// Note the addition of a Placeholder tag. If you like, you can update the
|
||||
// FactDictionary with this revised definition. If you do, we'll see that the
|
||||
// result of the conclusion has changed:
|
||||
|
||||
// graph.get("/conclusion") // : Result[Any] = Result(false, incomplete)
|
||||
|
||||
// The conclusion is still incomplete, but instead of "???," it now has the
|
||||
// value of false. This would allow us to calculate other Facts using this
|
||||
// value, although those Facts would also be marked as incomplete if they rely
|
||||
// on the placeholder value.
|
||||
//
|
||||
// Let's try one final example to bring it home.
|
||||
|
||||
graph.set("/factB", false)
|
||||
graph.save()
|
||||
|
||||
// Keeping in mind that fact C is still incomplete, what would we expect the
|
||||
// result of the conclusion to be? Let's check it out.
|
||||
|
||||
graph.get("/conclusion")
|
||||
|
||||
// Unsuprisingly, the value is false. But more significantly, despite fact C
|
||||
// still being missing, the result is now complete. This makes sense if we think
|
||||
// about whether the value of fact C could affect the result. Now that fact B is
|
||||
// false, the value of fact C no longer matters; no matter what, the children of
|
||||
// the All operation cannot all be true.
|
||||
//
|
||||
// We can even take it one step further and erase fact A as well.
|
||||
|
||||
graph.delete("/factA")
|
||||
graph.save()
|
||||
|
||||
graph.get("/conclusion")
|
||||
|
||||
// It doesn't matter! Take a moment and consider how this might be useful in the
|
||||
// context of a tax return.
|
||||
//
|
||||
// There is a vast quantity of information that could potentially affect an
|
||||
// individual's taxes. An effective tax filing product will need to be able to
|
||||
// determine what information is relevant, and what is not. We will want to only
|
||||
// ask questions that are relevant, that is to say, that could affect your taxes
|
||||
// one way or another. The moment that we knew that fact B was false, facts A
|
||||
// and C ceased to have any relevance, at least to our conclusion fact.
|
||||
//
|
||||
// The concepts of relevance and completeness are inextricable. You might
|
||||
// imagine an interface where each question contains a set of rules to
|
||||
// conditionally select the next question to present to the user. But this
|
||||
// interface would be incredibly fragile and difficult to maintain. And it would
|
||||
// risk the user reaching a state where they have not been asked for information
|
||||
// that is necessary to finish their tax return.
|
||||
//
|
||||
// On the other hand, imagine an interface that looks to the Fact Graph to
|
||||
// determine the relevance of a question. We can describe the goal of asking
|
||||
// about facts A, B, and C as to inform the conclusion. And if the interface
|
||||
// ever sees...
|
||||
|
||||
graph.get("/conclusion").complete
|
||||
|
||||
// ...it knows that it has sufficient information to draw a conclusion, and it
|
||||
// does't need to ask additional questions.
|
||||
//
|
||||
// Now that we've explored boolean logic a bit, the next chapter will dive into
|
||||
// numbers and numeric operations.
|
|
@ -0,0 +1,340 @@
|
|||
import gov.irs.factgraph.{Factual, FactDictionary, Graph}
|
||||
import gov.irs.factgraph.compnodes.CompNode
|
||||
import gov.irs.factgraph.persisters.InMemoryPersister
|
||||
import gov.irs.factgraph.types.Dollar
|
||||
|
||||
// ##################
|
||||
// ## 3. Numbers ##
|
||||
// ##################
|
||||
//
|
||||
// The core Fact Graph library supports three numeric types.
|
||||
//
|
||||
// * Int. A 32-bit signed integer.
|
||||
// * Rational. The ratio of two integers, i.e. x/y. We will represent whole
|
||||
// percentages as a Rational with a denominator of 100.
|
||||
// * Dollar. An opaque wrapper over Java's BigDecimal type, with operations
|
||||
// rounding to two decimal places using HALF_EVEN mode, i.e. rounding half
|
||||
// cents to the nearest even cent (also known as banker's rounding).
|
||||
// Rounding a Dollar amount to a whole number of dollars uses HALF_UP mode,
|
||||
// where 50 cents is rounded up, per IRS guidance.
|
||||
//
|
||||
// Numeric comparisons (GreaterThan, GreaterThanOrEqual, LessThan, and
|
||||
// LessThanOrEqual) require both left- and right-hand sides to be of the same
|
||||
// numeric type, as do Equal and NotEqual, as well as the numeric min/max
|
||||
// operations GreaterOf and LesserOf.
|
||||
//
|
||||
// However, the arithmatic operations (Add, Subtract, Multiple, and Divide) can
|
||||
// take any combination of numeric types. The type of the output can be found
|
||||
// using the following tables:
|
||||
//
|
||||
// When adding, subtracting, or multiplying:
|
||||
//
|
||||
// | || Int | Rational | Dollar |
|
||||
// | -------- || ----------- | ----------- | ------ |
|
||||
// | Int || Int | Rational[1] | Dollar |
|
||||
// | Rational || Rational[1] | Rational | Dollar |
|
||||
// | Dollar || Dollar | Dollar | Dollar |
|
||||
//
|
||||
// When dividing:
|
||||
//
|
||||
// | || Int | Rational | Dollar |
|
||||
// | -------- || ---------- | -------- | ------ |
|
||||
// | Int || _Rational_ | Rational | Dollar |
|
||||
// | Rational || Rational | Rational | Dollar |
|
||||
// | Dollar || Dollar | Dollar | Dollar |
|
||||
//
|
||||
// [1] Adding or subtracting two Rationals with the same denominator will
|
||||
// preserve the value of the denominator. Other operations will simplify the
|
||||
// resulting fraction, if possible.
|
||||
//
|
||||
// Let's take a look at an example.
|
||||
|
||||
given Factual = null
|
||||
// Note: Because we're using a CompNode outside of a Fact, we need to provide
|
||||
// this contextual parameter.
|
||||
|
||||
CompNode
|
||||
.fromXml(
|
||||
<Divide>
|
||||
<Dividend>
|
||||
<Int>1</Int>
|
||||
</Dividend>
|
||||
<Divisors>
|
||||
<Int>2</Int>
|
||||
</Divisors>
|
||||
</Divide>
|
||||
)
|
||||
.get(0)
|
||||
|
||||
// As we can see from the above tables, dividing an Int by an Int produces a
|
||||
// Rational, in this case, 1/2.
|
||||
//
|
||||
// While we can only have one Dividend, Divide will take multiple Divisors and
|
||||
// apply them sequentially.
|
||||
|
||||
CompNode
|
||||
.fromXml(
|
||||
<Divide>
|
||||
<Dividend>
|
||||
<Int>1</Int>
|
||||
</Dividend>
|
||||
<Divisors>
|
||||
<Int>2</Int>
|
||||
<Int>3</Int>
|
||||
</Divisors>
|
||||
</Divide>
|
||||
)
|
||||
.get(0)
|
||||
|
||||
// Let's explore two additional operations that are useful for taxes.
|
||||
|
||||
CompNode
|
||||
.fromXml(
|
||||
<StepwiseMultiply>
|
||||
<Multiplicand>
|
||||
<Dollar>9999.99</Dollar>
|
||||
</Multiplicand>
|
||||
<Rate>
|
||||
<Rational>50/1000</Rational>
|
||||
</Rate>
|
||||
</StepwiseMultiply>
|
||||
)
|
||||
.get(0)
|
||||
|
||||
// StepwiseMultiply takes two inputs, a Dollar amount and a rate, expressed as a
|
||||
// Rational. We'll multiply the amount by the rate as follows: the numerator and
|
||||
// the denominator of the rate are both whole dollars. We'll divide the amount
|
||||
// by the denominator, dropping any remainder, and then multiply by the
|
||||
// numerator. So $1,000 fits into $9,999.99 nine times, and nine times $50 is
|
||||
// $450. Sometimes tax law will prescribe this less precise form of
|
||||
// multiplication, for example in the phase-outs to the Child Tax Credit.
|
||||
|
||||
CompNode
|
||||
.fromXml(
|
||||
<Round>
|
||||
<Dollar>2.50</Dollar>
|
||||
</Round>
|
||||
)
|
||||
.get(0)
|
||||
|
||||
// Round, as you would expect, rounds a Dollar amount to the nearest whole
|
||||
// number of dollars. 50 cents is always rounded up, per IRS guidance.
|
||||
//
|
||||
// Let's put it all together by implementing the TY2021 tax tables for a Married
|
||||
// Filing Jointly couple or Qualifying Widow(er).
|
||||
|
||||
val dictionary = FactDictionary.fromXml(
|
||||
<Dictionary>
|
||||
<Fact path="/taxableIncome">
|
||||
<Writable><Dollar /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/tax">
|
||||
<Derived>
|
||||
<Round>
|
||||
<Switch>
|
||||
// Not over $19,900:
|
||||
// 10% of the taxable income
|
||||
<Case>
|
||||
<When>
|
||||
<LessThanOrEqual>
|
||||
<Left><Dependency path="../taxableIncome" /></Left>
|
||||
<Right><Dollar>19900</Dollar></Right>
|
||||
</LessThanOrEqual>
|
||||
</When>
|
||||
<Then>
|
||||
<Multiply>
|
||||
<Rational>10/100</Rational>
|
||||
<Dependency path="../taxableIncome" />
|
||||
</Multiply>
|
||||
</Then>
|
||||
</Case>
|
||||
|
||||
// Over $19,900 but not over $81,050:
|
||||
// $1,990 plus 12% of the excess over $19,900
|
||||
<Case>
|
||||
<When>
|
||||
<LessThanOrEqual>
|
||||
<Left><Dependency path="../taxableIncome" /></Left>
|
||||
<Right><Dollar>81050</Dollar></Right>
|
||||
</LessThanOrEqual>
|
||||
</When>
|
||||
<Then>
|
||||
<Add>
|
||||
<Dollar>1990</Dollar>
|
||||
<Multiply>
|
||||
<Rational>12/100</Rational>
|
||||
<Subtract>
|
||||
<Minuend>
|
||||
<Dependency path="../taxableIncome" />
|
||||
</Minuend>
|
||||
<Subtrahends>
|
||||
<Dollar>19900</Dollar>
|
||||
</Subtrahends>
|
||||
</Subtract>
|
||||
</Multiply>
|
||||
</Add>
|
||||
</Then>
|
||||
</Case>
|
||||
|
||||
// Over $81,050 but not over $172,750:
|
||||
// $9,328 plus 22% of the excess over $81,050
|
||||
<Case>
|
||||
<When>
|
||||
<LessThanOrEqual>
|
||||
<Left><Dependency path="../taxableIncome" /></Left>
|
||||
<Right><Dollar>172750</Dollar></Right>
|
||||
</LessThanOrEqual>
|
||||
</When>
|
||||
<Then>
|
||||
<Add>
|
||||
<Dollar>9328</Dollar>
|
||||
<Multiply>
|
||||
<Rational>22/100</Rational>
|
||||
<Subtract>
|
||||
<Minuend>
|
||||
<Dependency path="../taxableIncome" />
|
||||
</Minuend>
|
||||
<Subtrahends>
|
||||
<Dollar>81050</Dollar>
|
||||
</Subtrahends>
|
||||
</Subtract>
|
||||
</Multiply>
|
||||
</Add>
|
||||
</Then>
|
||||
</Case>
|
||||
|
||||
// Over $172,750 but not over $329,850:
|
||||
// $29,502 plus 24% of the excess over $172,750
|
||||
<Case>
|
||||
<When>
|
||||
<LessThanOrEqual>
|
||||
<Left><Dependency path="../taxableIncome" /></Left>
|
||||
<Right><Dollar>329850</Dollar></Right>
|
||||
</LessThanOrEqual>
|
||||
</When>
|
||||
<Then>
|
||||
<Add>
|
||||
<Dollar>29502</Dollar>
|
||||
<Multiply>
|
||||
<Rational>24/100</Rational>
|
||||
<Subtract>
|
||||
<Minuend>
|
||||
<Dependency path="../taxableIncome" />
|
||||
</Minuend>
|
||||
<Subtrahends>
|
||||
<Dollar>172750</Dollar>
|
||||
</Subtrahends>
|
||||
</Subtract>
|
||||
</Multiply>
|
||||
</Add>
|
||||
</Then>
|
||||
</Case>
|
||||
|
||||
// Over $329,850 but not over $418,850:
|
||||
// $67,206 plus 32% of the excess over $329,850
|
||||
<Case>
|
||||
<When>
|
||||
<LessThanOrEqual>
|
||||
<Left><Dependency path="../taxableIncome" /></Left>
|
||||
<Right><Dollar>418850</Dollar></Right>
|
||||
</LessThanOrEqual>
|
||||
</When>
|
||||
<Then>
|
||||
<Add>
|
||||
<Dollar>67206</Dollar>
|
||||
<Multiply>
|
||||
<Rational>32/100</Rational>
|
||||
<Subtract>
|
||||
<Minuend>
|
||||
<Dependency path="../taxableIncome" />
|
||||
</Minuend>
|
||||
<Subtrahends>
|
||||
<Dollar>329850</Dollar>
|
||||
</Subtrahends>
|
||||
</Subtract>
|
||||
</Multiply>
|
||||
</Add>
|
||||
</Then>
|
||||
</Case>
|
||||
|
||||
// Over $418,850 but not over $628,300:
|
||||
// $95,686 plus 35% of the excess over $418,850
|
||||
<Case>
|
||||
<When>
|
||||
<LessThanOrEqual>
|
||||
<Left><Dependency path="../taxableIncome" /></Left>
|
||||
<Right><Dollar>628300</Dollar></Right>
|
||||
</LessThanOrEqual>
|
||||
</When>
|
||||
<Then>
|
||||
<Add>
|
||||
<Dollar>95686</Dollar>
|
||||
<Multiply>
|
||||
<Rational>35/100</Rational>
|
||||
<Subtract>
|
||||
<Minuend>
|
||||
<Dependency path="../taxableIncome" />
|
||||
</Minuend>
|
||||
<Subtrahends>
|
||||
<Dollar>418850</Dollar>
|
||||
</Subtrahends>
|
||||
</Subtract>
|
||||
</Multiply>
|
||||
</Add>
|
||||
</Then>
|
||||
</Case>
|
||||
|
||||
// Over $628,300:
|
||||
// $168,993.50 plus 37% of the excess over $628,300
|
||||
<Case>
|
||||
<When>
|
||||
<True />
|
||||
</When>
|
||||
<Then>
|
||||
<Add>
|
||||
<Dollar>168993.50</Dollar>
|
||||
<Multiply>
|
||||
<Rational>37/100</Rational>
|
||||
<Subtract>
|
||||
<Minuend>
|
||||
<Dependency path="../taxableIncome" />
|
||||
</Minuend>
|
||||
<Subtrahends>
|
||||
<Dollar>628300</Dollar>
|
||||
</Subtrahends>
|
||||
</Subtract>
|
||||
</Multiply>
|
||||
</Add>
|
||||
</Then>
|
||||
</Case>
|
||||
</Switch>
|
||||
</Round>
|
||||
</Derived>
|
||||
</Fact>
|
||||
</Dictionary>
|
||||
)
|
||||
|
||||
val graph = Graph(
|
||||
dictionary,
|
||||
InMemoryPersister(
|
||||
"/taxableIncome" -> Dollar("75000.00")
|
||||
)
|
||||
)
|
||||
|
||||
graph.get("/tax")
|
||||
|
||||
// Note that when the arguments to an operation serve different roles, the
|
||||
// operations require us to explicilty label them. So while Add and Multiply can
|
||||
// take arguments in any order, Subtract and Divide use Minuend/Subtrahends and
|
||||
// Dividend/Divisors to avoid ambiguity. Similarly, comparison operations like
|
||||
// LessThanOrEqual explicitly specify Left and Right.
|
||||
//
|
||||
// As a bonus, we have introduced the Switch statement, which selects the first
|
||||
// Case where the When condition returns true. All of the Whens must be
|
||||
// booleans, while the Thens can be of any type, as long as it is the same
|
||||
// across all Cases.
|
||||
//
|
||||
// In the next chapter, we'll look at the powerful concept of Collections, which
|
||||
// is the final piece we'll need to enable the Fact Graph to model an entire
|
||||
// tax return.
|
|
@ -0,0 +1,202 @@
|
|||
import gov.irs.factgraph.{Factual, FactDictionary, Graph, Path}
|
||||
import gov.irs.factgraph.compnodes.CompNode
|
||||
import gov.irs.factgraph.persisters.InMemoryPersister
|
||||
import gov.irs.factgraph.types.{Collection, CollectionItem}
|
||||
import java.util.UUID
|
||||
|
||||
// ##################################
|
||||
// ## 4. Collections and Vectors ##
|
||||
// ##################################
|
||||
//
|
||||
// Taxes frequently feature sets of repeated items. A tax return may include
|
||||
// multiple filers, dependents, W-2s, 1099s, etc. We will call these sets of
|
||||
// repeated items Collections. Let's use dependents as an example.
|
||||
|
||||
val dictionary = FactDictionary.fromXml(
|
||||
<Dictionary>
|
||||
<Fact path="/taxYear">
|
||||
<Derived><Int>2021</Int></Derived>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/familyAndHousehold">
|
||||
<Writable><Collection /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/familyAndHousehold/*/yearOfBirth">
|
||||
<Writable><Int /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/familyAndHousehold/*/age">
|
||||
<Derived>
|
||||
<Subtract>
|
||||
<Minuend><Dependency path="/taxYear" /></Minuend>
|
||||
<Subtrahends><Dependency path="../yearOfBirth" /></Subtrahends>
|
||||
</Subtract>
|
||||
</Derived>
|
||||
</Fact>
|
||||
|
||||
// We're going to play with this dictionary quite a lot. We'll use CompNodes
|
||||
// that pretend to be this entryPoint fact in order to avoid needing to
|
||||
// redefine the dictionary repeatedly.
|
||||
<Fact path="/entryPoint">
|
||||
<Derived><True /></Derived>
|
||||
</Fact>
|
||||
</Dictionary>
|
||||
)
|
||||
|
||||
val dependentId1 = UUID.randomUUID()
|
||||
val dependentId2 = UUID.randomUUID()
|
||||
val dependentId3 = UUID.randomUUID()
|
||||
|
||||
val graph = Graph(
|
||||
dictionary,
|
||||
InMemoryPersister(
|
||||
"/familyAndHousehold" -> Collection(
|
||||
Vector(dependentId1, dependentId2, dependentId3)
|
||||
),
|
||||
s"/familyAndHousehold/#$dependentId1/yearOfBirth" -> 2013,
|
||||
s"/familyAndHousehold/#$dependentId2/yearOfBirth" -> 2016,
|
||||
s"/familyAndHousehold/#$dependentId3/yearOfBirth" -> 2018
|
||||
)
|
||||
)
|
||||
|
||||
// A Collection is represented as just another Fact, although it has some
|
||||
// special properties. Let's take a look.
|
||||
|
||||
graph.get("/familyAndHousehold")
|
||||
|
||||
// As with any Fact, it returns a Result, reflecting that the Collection could
|
||||
// be incomplete. The value contains a Vector with three UUIDs, representing
|
||||
// each of the collection items, in this case, dependents. We can access a
|
||||
// CollectionItem fact using the UUIDs.
|
||||
|
||||
graph.get(s"/familyAndHousehold/#$dependentId1")
|
||||
|
||||
// Note that we did not define this child fact; it was created automatically by
|
||||
// a feature called extracts. Essentially, we know that a Collection fact will
|
||||
// always have CollectionItem children, so the Fact Graph takes care of them for
|
||||
// us. Date facts also use this feature to create year, month, and day extracts.
|
||||
//
|
||||
// TODO: Date HASN'T BEEN IMPLEMENTED YET
|
||||
//
|
||||
// A CollectionItem contains one or more facts, which are repeated across all of
|
||||
// the items in the Collection.
|
||||
|
||||
graph.get(s"/familyAndHousehold/#$dependentId1/age")
|
||||
|
||||
// Looking closer at the definition of age, we can see something interesting
|
||||
// about how it describes its arguments. The dependents' year of birth uses a
|
||||
// relative path...
|
||||
|
||||
<Dependency path="../yearOfBirth" />
|
||||
|
||||
// ...indicating that the year of birth is specific to that particular
|
||||
// dependent. Meanwhile the tax year is an absolute path...
|
||||
|
||||
<Dependency path="/taxYear" />
|
||||
|
||||
// ...reflecting that the tax year is shared across the entire tax return. We
|
||||
// could also have written tax year as a relative path like so...
|
||||
|
||||
<Dependency path="../../../taxYear" />
|
||||
|
||||
// ...but this is a lot more cumbersome and unnecessary when dealing with a
|
||||
// single fact located at the root of the graph.
|
||||
//
|
||||
// We can use a wildcard (*) to get all of the children of a collection at once.
|
||||
|
||||
graph.getVect("/familyAndHousehold/*")
|
||||
|
||||
// We can also use this to get access all of the dependents' ages.
|
||||
|
||||
graph.getVect(s"/familyAndHousehold/*/age")
|
||||
|
||||
// Note that instead of using graph.get, we have used graph.getVect. If we look
|
||||
// at the return type, we'll see that we now have a MaybeVector of Results. A
|
||||
// MaybeVector can either be Single or Multiple.
|
||||
//
|
||||
// If the MaybeVector is Single, it will always contain just one Result. Indeed,
|
||||
// if you check out the implementation of .get in Graph.scala, you will see that
|
||||
// .get is just a convenience method that gives us the output of getVect if and
|
||||
// only if that output is Single.
|
||||
//
|
||||
// If the MaybeVector is Multiple, it contains any number of Results, including
|
||||
// zero. And just like the Result type, a Multiple MaybeVector has completeness,
|
||||
// reflecting whether the number of items in the Collection could change.
|
||||
|
||||
graph.get("/familyAndHousehold").complete
|
||||
graph.getVect("/familyAndHousehold/*").complete
|
||||
graph.getVect(s"/familyAndHousehold/*/age").complete
|
||||
|
||||
// Because the dependents Collection Fact is marked as complete, all of the
|
||||
// MaybeVectors of its children are also complete. If the Collection was
|
||||
// incomplete, say because the taxpayer was still in the process of adding
|
||||
// dependents, the MaybeVectors would be incomplete, letting us know that they
|
||||
// might still change.
|
||||
//
|
||||
// A Fact can use a wildcard in its definition. This fact will always return
|
||||
// multiple values:
|
||||
|
||||
given Factual = graph(Path("/entryPoint"))(0).get
|
||||
|
||||
CompNode
|
||||
.fromXml(
|
||||
<Dependency path="/familyAndHousehold/*/age" />
|
||||
)
|
||||
.get
|
||||
|
||||
// The definition will always be a Multiple, and must be accessed using
|
||||
// graph.getVect. More often, however, we'll aggregate these multiple values
|
||||
// using operations like Count and Sum. Here's an example.
|
||||
|
||||
CompNode
|
||||
.fromXml(
|
||||
<Count>
|
||||
<LessThanOrEqual>
|
||||
<Left><Dependency path="/familyAndHousehold/*/age" /></Left>
|
||||
<Right><Int>6</Int></Right>
|
||||
</LessThanOrEqual>
|
||||
</Count>
|
||||
)
|
||||
.get
|
||||
|
||||
// This fact counts the number of dependents who are six or younger. Now the
|
||||
// fact returns a Single, reflecting that however many dependents there are,
|
||||
// they will always be aggregated to a single count.
|
||||
//
|
||||
// Note that Single MaybeVectors don't have completeness. This makes sense
|
||||
// because they will always contain a single Result. But did we lose information
|
||||
// when aggregating the Multiple ages?
|
||||
//
|
||||
// No! If the Multiple MaybeVector had been incomplete, then the aggregated
|
||||
// Result would have been incomplete too, reflecting that if more dependents are
|
||||
// added, the count of those six and younger might change.
|
||||
//
|
||||
// Also note that before aggregating, we compared the Multiple to the value of
|
||||
// six. But <Int>6</Int> is Single! How did this work?
|
||||
//
|
||||
// From this, we can learn that all Fact Graph operations are _vectorized_. When
|
||||
// one of the arguments to an operation is Single, and another is Multiple, the
|
||||
// output will be a Multiple, calculated by applying the Single value to each of
|
||||
// the Multiple values in turn.
|
||||
//
|
||||
// We can even operate on multiple Multiple arguments, provided that they all
|
||||
// come from the same Collection and thus have the same length. For example,
|
||||
// take this useless fact that doubles the ages and adds one.
|
||||
|
||||
CompNode
|
||||
.fromXml(
|
||||
<Add>
|
||||
<Dependency path="/familyAndHousehold/*/age" />
|
||||
<Dependency path="/familyAndHousehold/*/age" />
|
||||
<Int>1</Int>
|
||||
</Add>
|
||||
)
|
||||
.get
|
||||
|
||||
// As long as all of the arguments are either Multiples of the same length or
|
||||
// Single, we can go wild combining them and let the Fact Graph do its thing.
|
||||
//
|
||||
// So if Collections and CollectionItems are Facts, then does that mean we could
|
||||
// operate on them like any other Facts? In short, yes, and the possibilities
|
||||
// that arise are described in the next chapter.
|
|
@ -0,0 +1,164 @@
|
|||
import gov.irs.factgraph.{FactDictionary, Graph}
|
||||
import gov.irs.factgraph.persisters.InMemoryPersister
|
||||
import gov.irs.factgraph.types.{Collection, CollectionItem, Dollar}
|
||||
import java.util.UUID
|
||||
|
||||
// ###################################################
|
||||
// ## 5. Aliased Collections and Collection Items ##
|
||||
// ###################################################
|
||||
//
|
||||
// In the last chapter, we described Collections and CollectionItems as Facts
|
||||
// with special properties. In this chapter, we'll explore some more of those
|
||||
// special proprties, and also look at how we can operate on Collections and
|
||||
// CollectionItems same as we would any other Fact.
|
||||
//
|
||||
// As we often do, let's start by building a FactDictionary with some examples.
|
||||
|
||||
val dictionary = FactDictionary.fromXml(
|
||||
<Dictionary>
|
||||
<Fact path="/filers">
|
||||
<Writable><Collection /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/filers/*/firstName">
|
||||
<Writable><String /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/filers/*/isPrimaryFiler">
|
||||
<Writable><Boolean /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/formW2s">
|
||||
<Writable><Collection /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/formW2s/*/filer">
|
||||
<Writable><CollectionItem collection="/filers" /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/formW2s/*/wagesTipsOtherComp">
|
||||
<Writable><Dollar /></Writable>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/primaryFiler">
|
||||
<Derived>
|
||||
<Find path="/filers">
|
||||
<Dependency path="isPrimaryFiler" />
|
||||
</Find>
|
||||
</Derived>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/primaryFilerW2s">
|
||||
<Derived>
|
||||
<Filter path="/formW2s">
|
||||
<Dependency path="filer/isPrimaryFiler" />
|
||||
</Filter>
|
||||
</Derived>
|
||||
</Fact>
|
||||
|
||||
<Fact path="/primaryFilerWagesTipsOtherComp">
|
||||
<Derived>
|
||||
<Sum>
|
||||
<Dependency path="/primaryFilerW2s/*/wagesTipsOtherComp" />
|
||||
</Sum>
|
||||
</Derived>
|
||||
</Fact>
|
||||
</Dictionary>
|
||||
)
|
||||
|
||||
val filerId1 = UUID.randomUUID()
|
||||
val filerId2 = UUID.randomUUID()
|
||||
|
||||
val w2Id1 = UUID.randomUUID()
|
||||
val w2Id2 = UUID.randomUUID()
|
||||
val w2Id3 = UUID.randomUUID()
|
||||
|
||||
val graph = Graph(
|
||||
dictionary,
|
||||
InMemoryPersister(
|
||||
"/filers" -> Collection(Vector(filerId1, filerId2)),
|
||||
"/formW2s" -> Collection(Vector(w2Id1, w2Id2, w2Id3)),
|
||||
s"/filers/#$filerId1/firstName" -> "Alice",
|
||||
s"/filers/#$filerId2/firstName" -> "Bob",
|
||||
s"/filers/#$filerId1/isPrimaryFiler" -> true,
|
||||
s"/filers/#$filerId2/isPrimaryFiler" -> false,
|
||||
s"/formW2s/#$w2Id1/wagesTipsOtherComp" -> Dollar("25000.00"),
|
||||
s"/formW2s/#$w2Id2/wagesTipsOtherComp" -> Dollar("50000.00"),
|
||||
s"/formW2s/#$w2Id3/wagesTipsOtherComp" -> Dollar("100000.00")
|
||||
)
|
||||
)
|
||||
|
||||
// Take a look at the "/formW2s/*/filer" fact. What do you think it does?
|
||||
|
||||
<Fact path="/formW2s/*/filer">
|
||||
<Writable><CollectionItem collection="/filers" /></Writable>
|
||||
</Fact>
|
||||
|
||||
// This CollectionItem is an alias. It allows us to store a reference to an item
|
||||
// in the filers Collection, in this case, denoting the filer to whom the W2
|
||||
// belongs. We can follow this alias, and access facts that are children of the
|
||||
// referenced CollectionItem.
|
||||
|
||||
graph.get(s"/formW2s/#$w2Id1/filer")
|
||||
graph.get(s"/formW2s/#$w2Id1/filer/firstName")
|
||||
|
||||
// Because the filer fact is incomplete, we don't know which firstName is being
|
||||
// referenced, and so firstName is also incomplete, even though all of the
|
||||
// firstName facts themselves are defined. Let's fix that by attaching each W2
|
||||
// to a filer.
|
||||
|
||||
graph.set(s"/formW2s/#$w2Id1/filer", CollectionItem(filerId2))
|
||||
graph.set(s"/formW2s/#$w2Id2/filer", CollectionItem(filerId1))
|
||||
graph.set(s"/formW2s/#$w2Id3/filer", CollectionItem(filerId1))
|
||||
graph.save()
|
||||
|
||||
graph.get(s"/formW2s/#$w2Id1/filer/firstName")
|
||||
|
||||
// As you might expect, just as we can have a Writable CollectionItem, we can
|
||||
// also define a Derived CollectionItem alias. The primaryFiler fact does
|
||||
// exactly that.
|
||||
|
||||
<Fact path="/primaryFiler">
|
||||
<Derived>
|
||||
<Find path="/filers">
|
||||
<Dependency path="isPrimaryFiler" />
|
||||
</Find>
|
||||
</Derived>
|
||||
</Fact>
|
||||
|
||||
graph.get("/primaryFiler/firstName")
|
||||
|
||||
// Note that the scope of the Dependency is different than we've seen before:
|
||||
// it's relative to the CollectionItem that we're checking, not to the location
|
||||
// of the primaryFiler fact.
|
||||
//
|
||||
// We can also have Derived Collections.
|
||||
|
||||
<Fact path="/primaryFilerW2s">
|
||||
<Derived>
|
||||
<Filter path="/formW2s">
|
||||
<Dependency path="filer/isPrimaryFiler" />
|
||||
</Filter>
|
||||
</Derived>
|
||||
</Fact>
|
||||
|
||||
graph.get("/primaryFilerW2s")
|
||||
|
||||
// Here we have an aliased Collection, where the predicate is following an
|
||||
// aliased CollectionItem to determine whether to include the W2. Around and
|
||||
// around it goes.
|
||||
//
|
||||
// Even though they are aliases, we can use these Derived Collections and
|
||||
// CollectionItems as we would any other.
|
||||
|
||||
<Fact path="/primaryFilerWagesTipsOtherComp">
|
||||
<Derived>
|
||||
<Sum>
|
||||
<Dependency path="/primaryFilerW2s/*/wagesTipsOtherComp" />
|
||||
</Sum>
|
||||
</Derived>
|
||||
</Fact>
|
||||
|
||||
graph.get("primaryFilerWagesTipsOtherComp")
|
||||
|
||||
// In the next chapter, we'll go deeper on navigating the Fact Graph.
|
|
@ -0,0 +1,119 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
// Only children that are at present determing the result of a node are included
|
||||
// in the explanation. Children are formatted as lists of lists.
|
||||
//
|
||||
// [[A, B]] indicates that both A and B will always be required in order to
|
||||
// calculate a result, while [[A], [B]] indicates that were the result of A or B
|
||||
// to independently change, we might no longer require the other one.
|
||||
//
|
||||
// Here are some examples to illustrate this concept.
|
||||
//
|
||||
// An <Any> node only cares about whether it has a true value; order doesn't
|
||||
// matter. As a result, a complete, true node will provide only a single child
|
||||
// explanation, indicating the child that made it true. However, if the node is
|
||||
// false, or incomplete, it will provide a child explanation for each of its
|
||||
// children using the [[A], [B]] format, as a change in any of them could affect
|
||||
// the result, and moot the others.
|
||||
//
|
||||
// An <All> node, on the other hand, only cares about whether it has a false
|
||||
// value. As a result, it works exactly the opposite, providing a single
|
||||
// explanation if it contains a complete, false node, and explaining everything
|
||||
// using the [[A], [B]] format if not.
|
||||
//
|
||||
// A <Switch> statement must check each case sequentially. Imagine a <Switch>
|
||||
// with three cases — WhenA, WhenB, and WhenC — each of which has an equivalent
|
||||
// then statement — ThenA, ThenB, and ThenC, respectively. If WhenA and WhenC
|
||||
// are false, and only WhenB is true, the node will have the following children:
|
||||
//
|
||||
// [[WhenA], [WhenB, ThenB]]
|
||||
//
|
||||
// Because a change to the values of ThenA, WhenC, and ThenC would not affect
|
||||
// the result. On the other hand, if WhenA was false, but WhenB was incomplete,
|
||||
// the children would be:
|
||||
//
|
||||
// [[WhenA], [WhenB]]
|
||||
//
|
||||
// This expresses that only a change in WhenA or WhenB could affect the result,
|
||||
// although a change in either of them could make ThenA, ThenB, WhenC, and/or
|
||||
// ThenC newly relevant.
|
||||
//
|
||||
// A <Placeholder> node only explains its source value; we aren't interested
|
||||
// in explaining default values.
|
||||
|
||||
enum Explanation:
|
||||
case Constant
|
||||
case Writable(complete: Boolean, path: Path)
|
||||
case Operation(childList: List[List[Explanation]])
|
||||
case Dependency(
|
||||
complete: Boolean,
|
||||
source: Path,
|
||||
target: Path,
|
||||
childList: List[List[Explanation]],
|
||||
)
|
||||
case NotAttachedToGraph
|
||||
|
||||
def children: List[List[Explanation]] = this match
|
||||
case Operation(children) => children
|
||||
case Dependency(_, _, _, children) => children
|
||||
case _ => List()
|
||||
|
||||
def solves: List[List[Path]] = this match
|
||||
case Writable(false, path) => List(List(path))
|
||||
case _ =>
|
||||
// X = [[A, B], [C, D]]
|
||||
// Y = [[E, F]]
|
||||
// Z = [[G], [H]]
|
||||
//
|
||||
// [[X], [Y]] = [[A, B], [C, D], [E, F]]
|
||||
// [[X], [Z]] = [[A, B], [C, D], [G], [H]]
|
||||
// [[X, Y]] = [[A, B, E, F], [C, D, E, F]]
|
||||
// [[X, Z]] = [[A, B, G], [A, B, H], [C, D, G], [C, D, H]]
|
||||
// [[X, Y, Z]] = [[A, B, E, F, G], [A, B, E, F, H], [C, D, E, F, G], [C, D, E, F, H]]
|
||||
|
||||
// [[X, Y], [Z]] -> [[[[A, B], [C, D]], [[E, F]]], [[[G], [H]]]]
|
||||
val expandedSets = for {
|
||||
set <- children
|
||||
} yield for {
|
||||
explanation <- set
|
||||
} yield explanation.solves
|
||||
|
||||
expandedSets.flatMap { (setOfSets) => // [[[A, B], [C, D]], [[E, F]]]
|
||||
setOfSets.reduce { (acc, newSets) =>
|
||||
// acc: [[A, B], [C, D]]
|
||||
// newSets: [[E, F]]
|
||||
for {
|
||||
set1 <- acc // [A, B]
|
||||
set2 <- newSets // [E, F]
|
||||
} yield set1 ++ set2
|
||||
}
|
||||
}
|
||||
|
||||
def incompleteDependencies: List[(Path, Path)] =
|
||||
findIncompleteDependencies(List(this), List()).reverse
|
||||
|
||||
@annotation.tailrec
|
||||
private def findIncompleteDependencies(
|
||||
list: List[Explanation],
|
||||
acc: List[(Path, Path)],
|
||||
): List[(Path, Path)] = list match
|
||||
case explanation :: next =>
|
||||
val children = explanation.children.flatten
|
||||
val incompletes = explanation match
|
||||
case Dependency(false, source, target, _) => (source, target) +: acc
|
||||
case _ => acc
|
||||
findIncompleteDependencies(children ++ next, incompletes)
|
||||
case Nil => acc
|
||||
|
||||
object Explanation:
|
||||
def opWithInclusiveChildren(children: List[Explanation]): Explanation =
|
||||
Explanation.Operation(List(children))
|
||||
|
||||
def opWithInclusiveChildren(children: Explanation*): Explanation =
|
||||
opWithInclusiveChildren(children.toList)
|
||||
|
||||
def opWithExclusiveChildren(children: List[Explanation]): Explanation =
|
||||
Explanation.Operation(for child <- children yield List(child))
|
||||
|
||||
def opWithExclusiveChildren(children: Explanation*): Explanation =
|
||||
opWithExclusiveChildren(children.toList)
|
|
@ -0,0 +1,314 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import gov.irs.factgraph.operators.*
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.types.{CollectionItem, WritableType}
|
||||
import gov.irs.factgraph.compnodes.Placeholder
|
||||
|
||||
enum Expression[A]:
|
||||
case Constant(a: Option[A])
|
||||
case Writable(klass: Class[A])
|
||||
case Dependency(path: Path)
|
||||
case Switch(cases: List[(Expression[Boolean], Expression[A])])
|
||||
case Extract[A, X](f: (source: Result[X]) => Result[A]) extends Expression[A]
|
||||
case Unary[A, X](
|
||||
x: Expression[X],
|
||||
op: UnaryOperator[A, X],
|
||||
) extends Expression[A]
|
||||
case Binary[A, L, R](
|
||||
lhs: Expression[L],
|
||||
rhs: Expression[R],
|
||||
op: BinaryOperator[A, L, R],
|
||||
) extends Expression[A]
|
||||
case Arity4[A, W, X, Y, Z](
|
||||
arg1: Expression[W],
|
||||
arg2: Expression[X],
|
||||
arg3: Expression[Y],
|
||||
arg4: Expression[Z],
|
||||
op: Arity4Operator[A, W, X, Y, Z],
|
||||
) extends Expression[A]
|
||||
case Reduce(
|
||||
xs: List[Expression[A]],
|
||||
op: ReduceOperator[A],
|
||||
)
|
||||
// Returns the set of strings where the corresponding booleans are true.
|
||||
// If any string or boolean are incomplete, ConditionalList will return
|
||||
// Result.Incomplete
|
||||
case ConditionalList(options: List[(Expression[Boolean], Expression[String])]) extends Expression[List[String]]
|
||||
case Aggregate[A, X](
|
||||
x: Expression[X],
|
||||
op: AggregateOperator[A, X],
|
||||
) extends Expression[A]
|
||||
case Collect[A, X](
|
||||
path: Path,
|
||||
x: Expression[X],
|
||||
op: CollectOperator[A, X],
|
||||
) extends Expression[A]
|
||||
|
||||
def isWritable: Boolean = this match
|
||||
case Writable(_) => true
|
||||
case _ => Placeholder.isWritablePlaceholder(this)
|
||||
|
||||
def get(using Factual): MaybeVector[Result[A]] = this match
|
||||
case Constant(a) => MaybeVector(Result(a))
|
||||
case Writable(klass) => MaybeVector(getSavedResult(klass))
|
||||
case Dependency(path) => dependency(path, dependencies.result)
|
||||
case Switch(cases) => switch(cases, switches.result)
|
||||
case Extract(f) => extract(f)
|
||||
case Unary(x, op) => op(x.get)
|
||||
case Binary(lhs, rhs, op) => op(lhs.get, rhs.getThunk)
|
||||
case Arity4(arg1, arg2, arg3, arg4, op) =>
|
||||
op(arg1.get, arg2.get, arg3.get, arg4.get)
|
||||
case Reduce(xs, op) => op(xs.head.get, xs.tail.map(_.getThunk))
|
||||
case ConditionalList(l) => MaybeVector(conditionSet(l))
|
||||
case Aggregate(x, op) => MaybeVector(op(x.getThunk))
|
||||
case Collect(path, x, op) => MaybeVector(collect(path, x, op))
|
||||
|
||||
def getThunk(using Factual): MaybeVector[Thunk[Result[A]]] = this match
|
||||
case Constant(a) => MaybeVector(Thunk(() => Result(a)))
|
||||
case Writable(klass) => MaybeVector(Thunk(() => getSavedResult(klass)))
|
||||
case Dependency(path) => dependency(path, dependencies.thunk)
|
||||
case Switch(cases) => switch(cases, switches.thunk)
|
||||
case Extract(f) => extractThunk(f)
|
||||
case Unary(x, op) => op.thunk(x.getThunk)
|
||||
case Binary(lhs, rhs, op) => op.thunk(lhs.getThunk, rhs.getThunk)
|
||||
case Arity4(arg1, arg2, arg3, arg4, op) =>
|
||||
op.thunk(arg1.getThunk, arg2.getThunk, arg3.getThunk, arg4.getThunk)
|
||||
case Reduce(xs, op) => op.thunk(xs.head.getThunk, xs.tail.map(_.getThunk))
|
||||
case ConditionalList(l) => MaybeVector(Thunk(() => conditionSet(l)))
|
||||
case Aggregate(x, op) => MaybeVector(op.thunk(x.getThunk))
|
||||
case Collect(path, x, op) => MaybeVector(Thunk(() => collect(path, x, op)))
|
||||
|
||||
def explain(using fact: Factual): MaybeVector[Explanation] = this match
|
||||
case Constant(_) => MaybeVector(Explanation.Constant)
|
||||
case Writable(_) =>
|
||||
for result <- get yield Explanation.Writable(result.complete, fact.path)
|
||||
case Dependency(path) => dependency(path, dependencies.explain)
|
||||
case Switch(cases) => switchExplain(cases)
|
||||
case Extract(f) => fact(PathItem.Parent)(0).get.explain
|
||||
case Unary(x, op) => op.explain(x)
|
||||
case Binary(lhs, rhs, op) => op.explain(lhs, rhs)
|
||||
case Arity4(arg1, arg2, arg3, arg4, op) =>
|
||||
op.explain(arg1, arg2, arg3, arg4)
|
||||
case Reduce(xs, op) => op.explain(xs)
|
||||
case ConditionalList(l) => conditionExplain(l)
|
||||
case Aggregate(x, op) => op.explain(x)
|
||||
case Collect(path, x, op) => op.explain(path, x)
|
||||
|
||||
def set(value: WritableType)(using fact: Factual): Unit = this match
|
||||
case Writable(_) =>
|
||||
fact match
|
||||
case fact: Fact => fact.graph.persister.setFact(fact, value)
|
||||
case _ =>
|
||||
|
||||
def delete()(using fact: Factual): Unit = this match
|
||||
case Writable(_) =>
|
||||
fact match
|
||||
case fact: Fact => fact.graph.persister.deleteFact(fact)
|
||||
case _ =>
|
||||
|
||||
private def getSavedResult(klass: Class[A])(using fact: Factual): Result[A] =
|
||||
fact match
|
||||
case fact: Fact => fact.graph.persister.getSavedResult(fact.path, klass)
|
||||
case _ => Result.Incomplete
|
||||
|
||||
private def dependency[X](
|
||||
path: Path,
|
||||
f: (Result[Factual], Path, Path) => MaybeVector[X],
|
||||
)(using
|
||||
fact: Factual,
|
||||
): MaybeVector[X] =
|
||||
for {
|
||||
result <- fact(path)
|
||||
vect <- f(result, fact.path, path)
|
||||
} yield vect
|
||||
|
||||
private object dependencies:
|
||||
def result(
|
||||
result: Result[Factual],
|
||||
_1: Path,
|
||||
_2: Path,
|
||||
): MaybeVector[Result[A]] = result match
|
||||
case Result(fact, complete) =>
|
||||
val results = fact.get.asInstanceOf[MaybeVector[Result[A]]]
|
||||
if (!complete) results.map(_.asPlaceholder) else results
|
||||
case _ => MaybeVector(Result.Incomplete)
|
||||
|
||||
def thunk(
|
||||
result: Result[Factual],
|
||||
_1: Path,
|
||||
_2: Path,
|
||||
): MaybeVector[Thunk[Result[A]]] =
|
||||
result match
|
||||
case Result(fact, complete) =>
|
||||
val thunks = fact.size match
|
||||
case Factual.Size.Single =>
|
||||
MaybeVector(Thunk(() => fact.get(0).asInstanceOf[Result[A]]))
|
||||
case Factual.Size.Multiple =>
|
||||
fact.getThunk.asInstanceOf[MaybeVector[Thunk[Result[A]]]]
|
||||
|
||||
if (!complete)
|
||||
for {
|
||||
thunk <- thunks
|
||||
} yield for {
|
||||
result <- thunk
|
||||
} yield result.asPlaceholder
|
||||
else thunks
|
||||
case _ => MaybeVector(Thunk(() => Result.Incomplete))
|
||||
|
||||
def explain(
|
||||
result: Result[Factual],
|
||||
source: Path,
|
||||
target: Path,
|
||||
): MaybeVector[Explanation] =
|
||||
result match
|
||||
case Result(fact, complete) =>
|
||||
for {
|
||||
explanation <- fact.explain
|
||||
} yield Explanation.Dependency(
|
||||
complete,
|
||||
source,
|
||||
target,
|
||||
List(List(explanation)),
|
||||
)
|
||||
case _ =>
|
||||
MaybeVector(Explanation.Dependency(false, source, target, List()))
|
||||
|
||||
private def switch[X](
|
||||
cases: List[(Expression[Boolean], Expression[A])],
|
||||
f: List[(Thunk[Result[Boolean]], Thunk[Result[A]])] => X,
|
||||
)(using Factual): MaybeVector[X] =
|
||||
val thunks = cases.map((bool, a) => (bool.getThunk, a.getThunk))
|
||||
MaybeVector.vectorizeListTuple2(f, thunks)
|
||||
|
||||
private def switchExplain(
|
||||
cases: List[(Expression[Boolean], Expression[A])],
|
||||
)(using Factual): MaybeVector[Explanation] =
|
||||
val caseVectors = cases.map((bool, a) =>
|
||||
(
|
||||
bool.getThunk,
|
||||
bool.explain,
|
||||
a.explain,
|
||||
), // Potential optimization: thunk the explanations
|
||||
)
|
||||
|
||||
MaybeVector.vectorizeListTuple3(switches.explain, caseVectors)
|
||||
|
||||
private object switches:
|
||||
def result(
|
||||
cases: List[(Thunk[Result[Boolean]], Thunk[Result[A]])],
|
||||
): Result[A] =
|
||||
resultRecurse(cases, true)
|
||||
|
||||
@annotation.tailrec
|
||||
private def resultRecurse(
|
||||
cases: List[(Thunk[Result[Boolean]], Thunk[Result[A]])],
|
||||
accComplete: Boolean,
|
||||
): Result[A] = cases match
|
||||
case (bool, a) :: next =>
|
||||
val complete = bool.get.complete && accComplete
|
||||
|
||||
bool.get.value match
|
||||
case Some(true) => if (complete) a.get else a.get.asPlaceholder
|
||||
case _ => resultRecurse(next, complete)
|
||||
case Nil => Result.Incomplete
|
||||
|
||||
def thunk(
|
||||
cases: List[(Thunk[Result[Boolean]], Thunk[Result[A]])],
|
||||
): Thunk[Result[A]] =
|
||||
Thunk(() => result(cases))
|
||||
|
||||
def explain(
|
||||
cases: List[(Thunk[Result[Boolean]], Explanation, Explanation)],
|
||||
): Explanation =
|
||||
explainRecurse(cases, Explanation.Operation(List()))
|
||||
|
||||
@annotation.tailrec
|
||||
def explainRecurse(
|
||||
cases: List[(Thunk[Result[Boolean]], Explanation, Explanation)],
|
||||
explanation: Explanation,
|
||||
): Explanation = cases match
|
||||
case (bool, boolExp, aExp) :: next =>
|
||||
bool.get match
|
||||
case Result.Complete(true) =>
|
||||
Explanation.Operation(
|
||||
explanation.children :+ List(boolExp, aExp),
|
||||
)
|
||||
case Result.Complete(false) =>
|
||||
explainRecurse(
|
||||
next,
|
||||
Explanation.Operation(explanation.children :+ List(boolExp)),
|
||||
)
|
||||
case _ =>
|
||||
Explanation.Operation(
|
||||
explanation.children :+ List(boolExp),
|
||||
)
|
||||
case Nil => explanation
|
||||
|
||||
private def extract[X](
|
||||
f: (source: Result[X]) => Result[A],
|
||||
)(using fact: Factual): MaybeVector[Result[A]] =
|
||||
val parent = fact(PathItem.Parent)(0).get
|
||||
for {
|
||||
result <- parent.get
|
||||
} yield f(result.asInstanceOf[Result[X]])
|
||||
|
||||
private def extractThunk[X](
|
||||
f: (source: Result[X]) => Result[A],
|
||||
)(using fact: Factual): MaybeVector[Thunk[Result[A]]] =
|
||||
val parent = fact(PathItem.Parent)(0).get
|
||||
for {
|
||||
thunk <- parent.getThunk
|
||||
} yield for {
|
||||
result <- thunk
|
||||
} yield f(result.asInstanceOf[Result[X]])
|
||||
|
||||
def collect[X](path: Path, x: Expression[X], op: CollectOperator[A, X])(using
|
||||
fact: Factual,
|
||||
): Result[A] =
|
||||
val vect = for {
|
||||
item <- fact(path :+ PathItem.Wildcard)
|
||||
thunk <- x.getThunk(using item.get)
|
||||
} yield (item.get.get(0).get.asInstanceOf[CollectionItem], thunk)
|
||||
|
||||
op(vect)
|
||||
|
||||
def conditionSet(list: List[(Expression[Boolean], Expression[String])])(using
|
||||
fact: Factual,
|
||||
): Result[List[String]] =
|
||||
val thunks = list.map(o => (o._1.getThunk, o._2.getThunk))
|
||||
|
||||
// If any predicates or options are incomplete, we return Incomplete.
|
||||
if (thunks.exists((b, s) => b(0).get.complete == false || s(0).get.complete == false)) {
|
||||
return Result.Incomplete
|
||||
}
|
||||
|
||||
val strings = for {
|
||||
f <- thunks.filter((b, s) => {
|
||||
// if a boolean node is false, we exclude its
|
||||
// value from the returned list.
|
||||
b(0).get.complete && b(0).get.get == true &&
|
||||
// Similarly, if the value we're returning is incomplete, we exclude it.
|
||||
s(0).get.complete
|
||||
})
|
||||
} yield {
|
||||
f._2(0).get.get
|
||||
}
|
||||
Result.Complete(strings.toList)
|
||||
|
||||
def conditionExplain(list: List[(Expression[Boolean], Expression[String])])(using
|
||||
fact: Factual,
|
||||
): MaybeVector[Explanation] =
|
||||
// First we call explain on each bool and string
|
||||
val explanations = for (bool, str) <- list yield (bool.explain, str.explain)
|
||||
|
||||
// The result is a list of tuples of MaybeVectors, so we need to vectorize
|
||||
// this operation, which flattens the lists of tuples and returns a
|
||||
// MaybeVector of an Explanation, with the flattened lists as its children
|
||||
MaybeVector.vectorizeListTuple2(
|
||||
(expList: List[(Explanation, Explanation)]) =>
|
||||
Explanation.opWithInclusiveChildren(
|
||||
expList.flatten { case (a, b) => List(a, b) },
|
||||
),
|
||||
explanations,
|
||||
)
|
|
@ -0,0 +1,275 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import gov.irs.factgraph.compnodes.{CollectionItemNode, CollectionNode, CompNode, RootNode}
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.types.{Collection, CollectionItem, WritableType}
|
||||
import gov.irs.factgraph.limits.*
|
||||
|
||||
import java.util.UUID
|
||||
import scala.annotation.tailrec
|
||||
import scala.scalajs.js.annotation.JSExport
|
||||
import scala.scalajs.js.annotation.JSExportAll
|
||||
|
||||
final class Fact(
|
||||
@JSExport val value: CompNode,
|
||||
@JSExport val path: Path,
|
||||
@JSExport val limits: Seq[Limit],
|
||||
@JSExport val graph: Graph,
|
||||
@JSExport val parent: Option[Fact],
|
||||
@JSExport val meta: Factual.Meta,
|
||||
) extends Factual:
|
||||
given Factual = this
|
||||
export meta.*
|
||||
|
||||
@tailrec
|
||||
def root: Fact = parent match
|
||||
case Some(parent) => parent.root
|
||||
case None => this
|
||||
|
||||
override def get: MaybeVector[Result[value.Value]] =
|
||||
graph.resultCache
|
||||
.getOrElseUpdate(path, { value.get })
|
||||
.asInstanceOf[MaybeVector[Result[value.Value]]]
|
||||
|
||||
override def getThunk: MaybeVector[Thunk[Result[value.Value]]] =
|
||||
value.getThunk
|
||||
|
||||
override def explain: MaybeVector[Explanation] = value.explain
|
||||
|
||||
/** Set the value of a writable fact.
|
||||
*
|
||||
* @param a
|
||||
* The new value.
|
||||
* @param allowCollectionItemDelete
|
||||
* Whether to allow setting a Collection in a way that removes items.
|
||||
*/
|
||||
def set(a: WritableType, allowCollectionItemDelete: Boolean = false): Unit =
|
||||
if (!value.expr.isWritable)
|
||||
throw new Exception(s"${path} is not writable")
|
||||
|
||||
if (!allowCollectionItemDelete)
|
||||
a match
|
||||
case Collection(newValues) =>
|
||||
graph.getVect(path) match
|
||||
case MaybeVector.Single(Result(Collection(oldValues), _)) =>
|
||||
if (oldValues.exists(oldValue => !newValues.contains(oldValue)))
|
||||
throw new Exception(
|
||||
s"Cannot use set() to remove items(s) from collection ${path}",
|
||||
)
|
||||
case _ =>
|
||||
case _ =>
|
||||
|
||||
if (!value.ValueClass.isAssignableFrom(a.getClass()))
|
||||
throw new Exception(
|
||||
s"${path} is expecting '${value.ValueClass}', received '${a.getClass()}' instead",
|
||||
)
|
||||
|
||||
graph.persister.setFact(this, a)
|
||||
|
||||
def validate(): Seq[LimitViolation] =
|
||||
limits.map(x => x.run()).filter(x => x.isDefined).map(x => x.get)
|
||||
|
||||
def delete(): Unit =
|
||||
val collectionPath = parent.get.path
|
||||
graph.getVect(collectionPath) match
|
||||
case MaybeVector.Single(Result(Collection(collection), _)) =>
|
||||
val deleted = path.getMemberId.get
|
||||
graph.persister.deleteFact(this, true)
|
||||
graph(collectionPath)(0).get
|
||||
.set(Collection(collection.filter((uuid) => uuid != deleted)), true)
|
||||
case _ if value.expr.isWritable =>
|
||||
graph.persister.deleteFact(this, false)
|
||||
case _ =>
|
||||
|
||||
override def apply(path: Path): MaybeVector[Result[Fact]] =
|
||||
this(path, true)
|
||||
|
||||
override def apply(key: PathItem): MaybeVector[Result[Fact]] =
|
||||
this(List(key), true)
|
||||
|
||||
private def apply(
|
||||
path: Path,
|
||||
accComplete: Boolean,
|
||||
): MaybeVector[Result[Fact]] =
|
||||
if (path.absolute) root(path.items, accComplete)
|
||||
else this(path.items, accComplete)
|
||||
|
||||
private def apply(
|
||||
pathItems: List[PathItem],
|
||||
accComplete: Boolean,
|
||||
): MaybeVector[Result[Fact]] = pathItems match
|
||||
case PathItem.Parent :: next => applyNext(parent, next, accComplete)
|
||||
case PathItem.Child(_) :: _ => applyChild(pathItems, accComplete)
|
||||
case PathItem.Wildcard :: _ => applyWildcard(pathItems, accComplete)
|
||||
case PathItem.Member(id) :: _ => applyMember(id, pathItems, accComplete)
|
||||
case PathItem.Unknown :: _ => applyUnknown(pathItems)
|
||||
case Nil => MaybeVector(Result(this, accComplete))
|
||||
|
||||
private def applyChild(
|
||||
pathItems: List[PathItem],
|
||||
accComplete: Boolean,
|
||||
): MaybeVector[Result[Fact]] = value match
|
||||
case CollectionItemNode(item, Some(alias)) =>
|
||||
applyNextFollowingAlias(item, alias, pathItems, accComplete)
|
||||
case _ => applyNext(getChild(pathItems.head), pathItems.tail, accComplete)
|
||||
|
||||
private def applyWildcard(
|
||||
pathItems: List[PathItem],
|
||||
accComplete: Boolean,
|
||||
): MaybeVector[Result[Fact]] = value match
|
||||
case CollectionNode(collection, Some(alias)) =>
|
||||
mapCollectionItems(collection, accComplete) { id =>
|
||||
followAlias(alias, PathItem.Member(id) +: pathItems.tail, true)
|
||||
}
|
||||
case CollectionNode(collection, None) =>
|
||||
mapCollectionItems(collection, accComplete) { id =>
|
||||
applyNext(getMember(PathItem.Member(id)), pathItems.tail, true)
|
||||
}
|
||||
case _ => MaybeVector(Result.Incomplete)
|
||||
|
||||
private def applyMember(
|
||||
id: UUID,
|
||||
pathItems: List[PathItem],
|
||||
accComplete: Boolean,
|
||||
): MaybeVector[Result[Fact]] = value match
|
||||
case CollectionNode(collection, Some(alias)) =>
|
||||
mapCollectionItemIfInCollection(collection, id) {
|
||||
followAlias(alias, pathItems, accComplete)
|
||||
}
|
||||
case CollectionNode(collection, None) =>
|
||||
mapCollectionItemIfInCollection(collection, id) {
|
||||
applyNext(getMember(pathItems.head), pathItems.tail, accComplete)
|
||||
}
|
||||
case _ => MaybeVector(Result.Incomplete)
|
||||
|
||||
private def applyUnknown(
|
||||
pathItems: List[PathItem],
|
||||
): MaybeVector[Result[Fact]] = value match
|
||||
case CollectionNode(collection, Some(alias)) =>
|
||||
followAlias(alias, pathItems, false)
|
||||
case CollectionNode(collection, None) =>
|
||||
applyNext(getMember(pathItems.head), pathItems.tail, false)
|
||||
case _ => MaybeVector(Result.Incomplete)
|
||||
|
||||
private def applyNext(
|
||||
optFact: Option[Fact],
|
||||
next: List[PathItem],
|
||||
accComplete: Boolean,
|
||||
): MaybeVector[Result[Fact]] =
|
||||
for {
|
||||
result <- MaybeVector(Result(optFact))
|
||||
vect <- result match
|
||||
case Result(fact, complete) => fact(next, complete && accComplete)
|
||||
case _ => MaybeVector(Result.Incomplete)
|
||||
} yield vect
|
||||
|
||||
private def applyNextFollowingAlias(
|
||||
collectionItemExpr: Expression[CollectionItem],
|
||||
alias: Path,
|
||||
next: List[PathItem],
|
||||
accComplete: Boolean,
|
||||
): MaybeVector[Result[Fact]] =
|
||||
for {
|
||||
result <- collectionItemExpr.get
|
||||
vect <- result match
|
||||
case Result(item, complete) =>
|
||||
followAlias(
|
||||
alias,
|
||||
PathItem.Member(item.id) +: next,
|
||||
complete && accComplete,
|
||||
)
|
||||
case _ => followAlias(alias, PathItem.Unknown +: next, false)
|
||||
} yield vect
|
||||
|
||||
private def followAlias(
|
||||
alias: Path,
|
||||
next: List[PathItem],
|
||||
accComplete: Boolean,
|
||||
): MaybeVector[Result[Fact]] =
|
||||
this(alias ++ next, accComplete)
|
||||
|
||||
private def getChild(key: PathItem): Option[Fact] =
|
||||
val childPath = path :+ key
|
||||
graph.factCache.getOrElseUpdate(childPath, { makeFact(key) })
|
||||
|
||||
private def getMember(key: PathItem): Option[Fact] =
|
||||
val memberPath = path :+ key
|
||||
graph.factCache.getOrElseUpdate(memberPath, { makeExtract(key) })
|
||||
|
||||
private def makeFact(key: PathItem): Option[Fact] =
|
||||
makeExtract(key).orElse({
|
||||
graph
|
||||
.dictionary(meta.abstractPath :+ key)
|
||||
.map(_.attachToGraph(this, key))
|
||||
})
|
||||
|
||||
private def makeExtract(key: PathItem): Option[Fact] =
|
||||
value
|
||||
.extract(key)
|
||||
.map(node =>
|
||||
val extractMeta = Factual.Meta(
|
||||
size,
|
||||
meta.abstractPath :+ key.asAbstract,
|
||||
)
|
||||
Fact(node, this, limits, key, extractMeta),
|
||||
)
|
||||
|
||||
private def mapCollectionItems(
|
||||
collectionExpr: Expression[Collection],
|
||||
accComplete: Boolean,
|
||||
): (f: UUID => MaybeVector[Result[Fact]]) => MaybeVector[Result[Fact]] =
|
||||
f =>
|
||||
for {
|
||||
collection <- collectionExpr.get
|
||||
vect <- collection match
|
||||
case Result(collection, complete) =>
|
||||
for {
|
||||
id <- MaybeVector(collection.items, complete && accComplete)
|
||||
result <- f(id)
|
||||
} yield result
|
||||
case _ => MaybeVector(Nil, false)
|
||||
} yield vect
|
||||
|
||||
private def mapCollectionItemIfInCollection(
|
||||
collectionExpr: Expression[Collection],
|
||||
id: UUID,
|
||||
): (=> MaybeVector[Result[Fact]]) => MaybeVector[Result[Fact]] =
|
||||
x =>
|
||||
for {
|
||||
collection <- collectionExpr.get
|
||||
result <- collection match
|
||||
case Result(collection, _) if collection.items.contains(id) => x
|
||||
case _ => MaybeVector(Result.Incomplete)
|
||||
} yield result
|
||||
|
||||
object Fact:
|
||||
private val RootNode = new RootNode()
|
||||
private val RootMeta = Factual.Meta(
|
||||
Factual.Size.Single,
|
||||
Path.Root,
|
||||
)
|
||||
|
||||
def apply(graph: Graph): Fact = new Fact(
|
||||
RootNode,
|
||||
Path.Root,
|
||||
Seq.empty,
|
||||
graph,
|
||||
None,
|
||||
RootMeta,
|
||||
)
|
||||
|
||||
def apply(
|
||||
value: CompNode,
|
||||
parent: Fact,
|
||||
limits: Seq[Limit],
|
||||
pathItem: PathItem,
|
||||
meta: Factual.Meta,
|
||||
): Fact =
|
||||
new Fact(
|
||||
value,
|
||||
parent.path :+ pathItem,
|
||||
limits,
|
||||
parent.graph,
|
||||
Some(parent),
|
||||
meta,
|
||||
)
|
|
@ -0,0 +1,183 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import gov.irs.factgraph
|
||||
import gov.irs.factgraph.compnodes.{CollectionItemNode, CollectionNode, CompNode, WritableNode}
|
||||
import gov.irs.factgraph.definitions.fact.{FactConfigTrait, LimitConfigTrait}
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.limits.*
|
||||
import gov.irs.factgraph.compnodes.Placeholder
|
||||
import scala.scalajs.js.annotation.JSExport
|
||||
import scala.scalajs.js.annotation.JSExportAll
|
||||
|
||||
final class FactDefinition(
|
||||
private val cnBuilder: Factual ?=> CompNode,
|
||||
val path: Path,
|
||||
private val limitsBuilder: Factual ?=> Seq[Limit],
|
||||
val dictionary: FactDictionary,
|
||||
) extends Factual:
|
||||
given Factual = this
|
||||
|
||||
def attachToGraph(parent: Fact, key: PathItem): Fact =
|
||||
Fact(value, parent, limits, key, meta)
|
||||
|
||||
def asTuple: (Path, FactDefinition) = (path, this)
|
||||
|
||||
@JSExport
|
||||
lazy val value: CompNode = cnBuilder
|
||||
|
||||
@JSExport
|
||||
lazy val limits: Seq[Limit] = limitsBuilder.map(x => x)
|
||||
|
||||
@JSExport
|
||||
lazy val meta: Factual.Meta = Factual.Meta(
|
||||
size,
|
||||
abstractPath,
|
||||
)
|
||||
|
||||
lazy val size: Factual.Size = value.getThunk match
|
||||
case MaybeVector.Single(_) => Factual.Size.Single
|
||||
case MaybeVector.Multiple(_, _) => Factual.Size.Multiple
|
||||
|
||||
@JSExport
|
||||
def abstractPath: Path = path
|
||||
|
||||
override def get: MaybeVector[Result[value.Value]] = size match
|
||||
case Factual.Size.Single => MaybeVector(Result.Incomplete)
|
||||
case Factual.Size.Multiple => MaybeVector(Nil, true)
|
||||
|
||||
override def getThunk: MaybeVector[Thunk[Result[value.Value]]] = size match
|
||||
case Factual.Size.Single => MaybeVector(Thunk(() => Result.Incomplete))
|
||||
case Factual.Size.Multiple => MaybeVector(Nil, true)
|
||||
|
||||
override def explain: MaybeVector[Explanation] = size match
|
||||
case Factual.Size.Single => MaybeVector(Explanation.NotAttachedToGraph)
|
||||
case Factual.Size.Multiple => MaybeVector(Nil, true)
|
||||
|
||||
private def root: FactDefinition =
|
||||
dictionary(Path.Root).get
|
||||
|
||||
private def parent: Option[FactDefinition] =
|
||||
path.parent.flatMap(_.head) match
|
||||
case Some(PathItem.Wildcard) =>
|
||||
for {
|
||||
parentPath <- path.parent
|
||||
grandparentPath <- parentPath.parent
|
||||
collection <- dictionary(grandparentPath)
|
||||
collectionItem <- collection(PathItem.Unknown)(0).value
|
||||
} yield collectionItem
|
||||
case _ =>
|
||||
for {
|
||||
parentPath <- path.parent
|
||||
fact <- dictionary(parentPath)
|
||||
} yield fact
|
||||
|
||||
override def apply(path: Path): MaybeVector[Result[FactDefinition]] =
|
||||
if (path.absolute) root(path.items) else this(path.items)
|
||||
|
||||
override def apply(key: PathItem): MaybeVector[Result[FactDefinition]] =
|
||||
this(List(key))
|
||||
|
||||
private def apply(
|
||||
pathItems: List[PathItem],
|
||||
): MaybeVector[Result[FactDefinition]] = pathItems match
|
||||
case PathItem.Parent :: next => getNext(parent, next)
|
||||
case PathItem.Child(_) :: _ => applyChild(pathItems)
|
||||
case PathItem.Wildcard :: _ => applyWildcard(pathItems)
|
||||
case PathItem.Member(_) :: _ => MaybeVector(Result.Incomplete)
|
||||
case PathItem.Unknown :: _ => applyUnknown(pathItems)
|
||||
case Nil => MaybeVector(Result.Complete(this))
|
||||
|
||||
private def applyChild(
|
||||
pathItems: List[PathItem],
|
||||
): MaybeVector[Result[FactDefinition]] = value match
|
||||
case CollectionItemNode(_, Some(alias)) =>
|
||||
this((alias :+ PathItem.Unknown) ++ pathItems)
|
||||
case _ => getNext(getChild(pathItems.head), pathItems.tail)
|
||||
|
||||
private def applyWildcard(
|
||||
pathItems: List[PathItem],
|
||||
): MaybeVector[Result[FactDefinition]] = value match
|
||||
case CollectionNode(_, Some(alias)) =>
|
||||
this(alias ++ pathItems)
|
||||
case CollectionNode(_, None) =>
|
||||
getNext(getExtract(PathItem.Unknown), pathItems.tail).toMultiple
|
||||
case _ => MaybeVector(Result.Incomplete)
|
||||
|
||||
private def applyUnknown(
|
||||
pathItems: List[PathItem],
|
||||
): MaybeVector[Result[FactDefinition]] = value match
|
||||
case CollectionNode(_, Some(alias)) =>
|
||||
this(alias ++ pathItems)
|
||||
case CollectionNode(_, None) =>
|
||||
getNext(getExtract(pathItems.head), pathItems.tail)
|
||||
case _ => MaybeVector(Result.Incomplete)
|
||||
|
||||
private def getNext(
|
||||
optFact: Option[FactDefinition],
|
||||
next: List[PathItem],
|
||||
): MaybeVector[Result[FactDefinition]] =
|
||||
for {
|
||||
result <- MaybeVector(Result(optFact))
|
||||
vect <- result.value match
|
||||
case Some(fact) => fact(next)
|
||||
case None => MaybeVector(Result.Incomplete)
|
||||
} yield vect
|
||||
|
||||
private def getChild(key: PathItem): Option[FactDefinition] =
|
||||
getExtract(key).orElse(dictionary(path :+ key))
|
||||
|
||||
private def getExtract(key: PathItem): Option[FactDefinition] =
|
||||
value
|
||||
.extract(key)
|
||||
.map(node =>
|
||||
new FactDefinition(
|
||||
Factual ?=> node,
|
||||
path :+ key.asAbstract,
|
||||
Seq.empty,
|
||||
dictionary,
|
||||
),
|
||||
)
|
||||
|
||||
object FactDefinition:
|
||||
def apply(
|
||||
cnBuilder: Factual ?=> CompNode,
|
||||
path: Path,
|
||||
limits: Factual ?=> Seq[Limit],
|
||||
dictionary: FactDictionary,
|
||||
): FactDefinition =
|
||||
require(path.isAbstract)
|
||||
|
||||
val definition = new FactDefinition(cnBuilder, path, limits, dictionary)
|
||||
dictionary.addDefinition(definition)
|
||||
|
||||
definition
|
||||
|
||||
def fromConfig(e: FactConfigTrait)(using FactDictionary): FactDefinition =
|
||||
// if neither of them or both of them
|
||||
if (e.writable.isEmpty && e.derived.isEmpty || (e.writable.isDefined && e.derived.isDefined))
|
||||
throw new IllegalArgumentException(
|
||||
"Fact must have exactly one Writable or Derived",
|
||||
)
|
||||
val isWritable = e.writable.isDefined
|
||||
|
||||
val cnBuilder: Factual ?=> CompNode =
|
||||
val node =
|
||||
if (isWritable) WritableNode.fromConfig(e.writable.get)
|
||||
else CompNode.fromDerivedConfig(e.derived.get)
|
||||
|
||||
e.placeholder match
|
||||
case Some(default) =>
|
||||
Placeholder(node, CompNode.fromDerivedConfig(default))
|
||||
case None => node
|
||||
|
||||
val limits: Factual ?=> Seq[Limit] =
|
||||
if (isWritable)
|
||||
e.writable.get.limits
|
||||
.map(x => Limit.fromConfig(x))
|
||||
.toSeq
|
||||
.concat(WritableNode.fromConfig(e.writable.get).getIntrinsicLimits())
|
||||
else
|
||||
Seq.empty
|
||||
|
||||
val dictionary = summon[FactDictionary]
|
||||
this(cnBuilder, Path(e.path), limits, dictionary)
|
|
@ -0,0 +1,77 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import gov.irs.factgraph.compnodes.RootNode
|
||||
import gov.irs.factgraph.definitions.FactDictionaryConfigTrait
|
||||
import gov.irs.factgraph.definitions.meta.{EnumDeclarationTrait, MetaConfigTrait}
|
||||
import gov.irs.factgraph.Meta
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import gov.irs.factgraph.compnodes.MultiEnumNode
|
||||
import gov.irs.factgraph.compnodes.EnumNode
|
||||
|
||||
class FactDictionary:
|
||||
private val definitions: mutable.Map[Path, FactDefinition] = mutable.Map()
|
||||
private var frozen: Boolean = false
|
||||
private var meta: MetaConfigTrait = Meta.empty()
|
||||
|
||||
def getPaths(): Iterable[Path] =
|
||||
definitions.keys
|
||||
|
||||
def freeze(): Unit =
|
||||
for {
|
||||
(_, definition) <- definitions
|
||||
} definition.meta
|
||||
if (meta == Meta.empty())
|
||||
throw new UnsupportedOperationException(
|
||||
"Must provide meta information to FactDictionary",
|
||||
)
|
||||
frozen = true
|
||||
|
||||
def apply(path: Path): Option[FactDefinition] = definitions.get(path)
|
||||
|
||||
@JSExport("getDefinition")
|
||||
def apply(path: String): FactDefinition | Null =
|
||||
definitions.get(Path(path)) match
|
||||
case Some(value) => value
|
||||
case _ => null
|
||||
|
||||
@JSExport
|
||||
def getMeta(): MetaConfigTrait = meta
|
||||
|
||||
@JSExport("getOptionsPathForEnum")
|
||||
def getOptionsPathForEnum(enumPath: String): Option[String] =
|
||||
val factDef = this(enumPath)
|
||||
factDef.value match
|
||||
case value: EnumNode => Some(value.enumOptionsPath.toString)
|
||||
case value: MultiEnumNode => Some(value.enumOptionsPath.toString)
|
||||
case _ => None
|
||||
|
||||
protected[factgraph] def addDefinition(definition: FactDefinition): Unit =
|
||||
if (frozen)
|
||||
throw new UnsupportedOperationException(
|
||||
"cannot add definitions to a frozen FactDictionary",
|
||||
)
|
||||
|
||||
definitions.addOne(definition.asTuple)
|
||||
|
||||
protected[factgraph] def addMeta(metaConfigTrait: MetaConfigTrait): Unit =
|
||||
if (frozen)
|
||||
throw new UnsupportedOperationException(
|
||||
"Meta configuration must be added before freezing the dictionary",
|
||||
)
|
||||
meta = metaConfigTrait
|
||||
|
||||
object FactDictionary:
|
||||
def apply(): FactDictionary =
|
||||
val dictionary = new FactDictionary()
|
||||
FactDefinition(RootNode(), Path.Root, Seq.empty, dictionary)
|
||||
dictionary
|
||||
|
||||
@JSExport
|
||||
def fromConfig(e: FactDictionaryConfigTrait): FactDictionary =
|
||||
val dictionary = this()
|
||||
Meta.fromConfig(e.meta, dictionary)
|
||||
e.facts.map(FactDefinition.fromConfig(_)(using dictionary))
|
||||
dictionary.freeze()
|
||||
dictionary
|
|
@ -0,0 +1,32 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import gov.irs.factgraph.compnodes.CompNode
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.limits.*
|
||||
|
||||
trait Factual:
|
||||
def value: CompNode
|
||||
def path: Path
|
||||
def meta: Factual.Meta
|
||||
def size: Factual.Size
|
||||
def abstractPath: Path
|
||||
|
||||
def limits: Seq[Limit]
|
||||
def get: MaybeVector[Result[?]]
|
||||
def getThunk: MaybeVector[Thunk[Result[?]]]
|
||||
def explain: MaybeVector[Explanation]
|
||||
|
||||
def isWritable: Boolean = value.isWritable
|
||||
|
||||
def apply(path: Path): MaybeVector[Result[Factual]]
|
||||
def apply(key: PathItem): MaybeVector[Result[Factual]]
|
||||
|
||||
object Factual:
|
||||
enum Size:
|
||||
case Single
|
||||
case Multiple
|
||||
|
||||
final class Meta(
|
||||
val size: Size,
|
||||
val abstractPath: Path,
|
||||
)
|
|
@ -0,0 +1,120 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import gov.irs.factgraph.limits.LimitViolation
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.persisters.*
|
||||
import gov.irs.factgraph.types.{Collection, CollectionItem, WritableType, Enum}
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import scala.collection.mutable
|
||||
import scala.scalajs.js.annotation.JSExportAll
|
||||
|
||||
class Graph(val dictionary: FactDictionary, val persister: Persister):
|
||||
val root: Fact = Fact(this)
|
||||
|
||||
private[factgraph] val factCache = mutable.HashMap[Path, Option[Fact]]()
|
||||
private[factgraph] val resultCache =
|
||||
mutable.HashMap[Path, MaybeVector[Result[Any]]]()
|
||||
|
||||
export root.apply
|
||||
|
||||
@JSExport("get")
|
||||
def get(path: String): Result[Any] = get(Path(path))
|
||||
|
||||
@JSExport("getWithPath")
|
||||
def get(path: Path): Result[Any] = getVect(path) match
|
||||
case MaybeVector.Single(result) => result
|
||||
case MaybeVector.Multiple(_, _) =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"must use getVect to access '$path'",
|
||||
)
|
||||
|
||||
@JSExport("getVect")
|
||||
def getVect(path: String): MaybeVector[Result[Any]] = getVect(Path(path))
|
||||
@JSExport("getVectWithPath")
|
||||
def getVect(path: Path): MaybeVector[Result[Any]] =
|
||||
for {
|
||||
fact <- this(path)
|
||||
values <- fact match
|
||||
case Result(fact, complete) =>
|
||||
if (complete) fact.get
|
||||
else fact.get.map(_.asPlaceholder)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"path '$path' was not found",
|
||||
)
|
||||
} yield values
|
||||
|
||||
@JSExport("explain")
|
||||
def explain(path: String): Explanation = explain(Path(path))
|
||||
@JSExport("explainWithPath")
|
||||
def explain(path: Path): Explanation =
|
||||
val explanations = for {
|
||||
fact <- this(path)
|
||||
explanation <- fact match
|
||||
// Note that we are discarding the completeness of the path's
|
||||
// resolution; we are providing an explanation of a *fact's* result,
|
||||
// not why a particular *path* returns a potentially incomplete result.
|
||||
case Result(fact, _) =>
|
||||
fact.explain
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"path '$path' was not found",
|
||||
)
|
||||
} yield explanation
|
||||
|
||||
explanations match
|
||||
case MaybeVector.Single(explanation) => explanation
|
||||
case MaybeVector.Multiple(_, _) =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"path '$path' resolves to a vector",
|
||||
)
|
||||
|
||||
@JSExport("set")
|
||||
def set(path: String, value: WritableType): Unit = set(Path(path), value)
|
||||
@JSExport("setWithPath")
|
||||
def set(path: Path, value: WritableType): Unit =
|
||||
for {
|
||||
result <- this(path)
|
||||
fact <- result
|
||||
} fact.set(value)
|
||||
|
||||
@JSExport("delete")
|
||||
def delete(path: String): Unit = delete(Path(path))
|
||||
@JSExport("deleteWithPath")
|
||||
def delete(path: Path): Unit =
|
||||
for {
|
||||
result <- this(path)
|
||||
fact <- result
|
||||
} fact.delete()
|
||||
|
||||
def checkPersister(): Seq[PersisterSyncIssue] =
|
||||
persister.syncWithDictionary(this)
|
||||
|
||||
def save(): (Boolean, Seq[LimitViolation]) =
|
||||
factCache.clear()
|
||||
resultCache.clear()
|
||||
|
||||
val out = persister.save()
|
||||
|
||||
// Don't cache invalid results
|
||||
if !out._1 then resultCache.clear()
|
||||
|
||||
out
|
||||
|
||||
@JSExport("getDictionary")
|
||||
def getDictionary() = this.dictionary
|
||||
|
||||
def getCollectionPaths(collectionPath: String): Seq[String] =
|
||||
val paths = for
|
||||
pathPerhapsWithWildcards <- Path(collectionPath).populateWildcards(this)
|
||||
pathWithoutWildcards <- pathPerhapsWithWildcards.populateWildcards(this)
|
||||
yield pathWithoutWildcards.toString
|
||||
|
||||
paths.toSeq
|
||||
|
||||
object Graph:
|
||||
def apply(dictionary: FactDictionary): Graph =
|
||||
this(dictionary, InMemoryPersister())
|
||||
|
||||
def apply(dictionary: FactDictionary, persister: Persister): Graph =
|
||||
new Graph(dictionary, persister)
|
|
@ -0,0 +1,14 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import gov.irs.factgraph.FactDictionary
|
||||
import gov.irs.factgraph.definitions.meta.{EnumDeclarationTrait, MetaConfigTrait}
|
||||
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
|
||||
import gov.irs.factgraph.definitions.meta.EnumDeclarationOptionsTrait
|
||||
|
||||
case class Meta(val version: String) extends MetaConfigTrait:
|
||||
def getVersion() = version
|
||||
|
||||
object Meta:
|
||||
def empty(): Meta = new Meta("Invalid")
|
||||
def fromConfig(e: MetaConfigTrait, factDictionary: FactDictionary): Unit =
|
||||
factDictionary.addMeta(new Meta(e.version))
|
|
@ -0,0 +1,44 @@
|
|||
package gov.irs.factgraph
|
||||
import gov.irs.factgraph.persisters.TypeContainer
|
||||
import gov.irs.factgraph.types.WritableType
|
||||
import ujson.Value
|
||||
import upickle.default.{read, write}
|
||||
|
||||
// When we make changes to the Fact Graph or Fact Dictionary, those changes not only get deployed to
|
||||
// our backend servers, they get deployed to the users' browsers as well.
|
||||
//
|
||||
// In order to safely make changes to the Fact Graph, we can define programmatic changes that modify
|
||||
// existing Fact Graphs. That way in-progress Fact Graphs (which live in the user's browser) can be
|
||||
// modified to fit the new requirements, before we attempt to load them in.
|
||||
object Migrations {
|
||||
val MigrationsFieldName = "/meta/migrationsApplied";
|
||||
|
||||
// For each new migration, make a function for it, then add it to this list
|
||||
// Migrations numbers should increase monotonically, i.e. m0_, m1_, m2_, ...
|
||||
// It's very important that these migrations stay in order, so don't re-order the list.
|
||||
// The leading number (i.e. m1) is a way of explicitly denoting that order, but it's the List
|
||||
// order itself that matters for ensuring consistency.
|
||||
private val AllMigrations = List(
|
||||
m1_BlankMigration,
|
||||
m2_DeleteInvalidAddresses,
|
||||
)
|
||||
val TotalMigrations: Int = AllMigrations.length
|
||||
|
||||
def run(data: Map[String, Value], numMigrations: Int): Map[Path, WritableType] =
|
||||
AllMigrations
|
||||
.drop(numMigrations) // get the missing migrations
|
||||
.foldLeft(data)((data, migration) => migration(data)) // apply each of them
|
||||
.map((k, v) => (Path(k), read[TypeContainer](v).item)) // convert the result to Map[Path, WritableType]
|
||||
|
||||
// Blank migration to test the mechanism
|
||||
private def m1_BlankMigration(data: Map[String, Value]): Map[String, Value] =
|
||||
data
|
||||
|
||||
// Remove addresses that don't match MeF validation
|
||||
private def m2_DeleteInvalidAddresses(data: Map[String, Value]): Map[String, Value] =
|
||||
data.filterNot((_, value) =>
|
||||
value("$type").value == "gov.irs.factgraph.persisters.AddressWrapper" &&
|
||||
!value("item")("streetAddress").str.matches("[A-Za-z0-9]( ?[A-Za-z0-9\\-/])*"),
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
import gov.irs.factgraph.monads.{MaybeVector, Result}
|
||||
import gov.irs.factgraph.types.CollectionItem
|
||||
import java.util.UUID
|
||||
|
||||
final case class Path(private val _items: List[PathItem], absolute: Boolean):
|
||||
def ++(rhs: Path): Path =
|
||||
if (rhs.absolute) rhs
|
||||
else Path(rhs._items ++ _items, absolute)
|
||||
|
||||
def ++(rhs: Seq[PathItem]): Path =
|
||||
val items = rhs.foldLeft(_items)((items, item) => item +: items)
|
||||
Path(items, absolute)
|
||||
|
||||
def :+(item: PathItem): Path = Path(item +: _items, absolute)
|
||||
|
||||
def items: List[PathItem] = _items.reverse
|
||||
|
||||
def head: Option[PathItem] = _items match
|
||||
case head :: _ => Some(head)
|
||||
case Nil => None
|
||||
|
||||
def parent: Option[Path] = _items match
|
||||
case head :: next => Some(Path(next, absolute))
|
||||
case Nil => None
|
||||
|
||||
def isAbstract: Boolean = absolute && _items.forall(_.isAbstract)
|
||||
|
||||
def isKnown: Boolean = absolute && _items.forall(_.isKnown)
|
||||
|
||||
def isWildcard: Boolean = absolute && _items.exists(_.isWildcard)
|
||||
|
||||
def isCollectionMember: Boolean =
|
||||
absolute && _items.exists(_.isCollectionMember)
|
||||
|
||||
def isCollectionItem: Boolean = absolute && _items(0).isCollectionMember
|
||||
|
||||
def getMemberId: Option[UUID] =
|
||||
val itemOpt = _items.find((item) => item.isCollectionMember)
|
||||
if itemOpt.isEmpty then None
|
||||
else
|
||||
itemOpt.get match
|
||||
case PathItem.Member(uuid) => Some(uuid)
|
||||
case _ => None
|
||||
|
||||
def asAbstract: Path =
|
||||
items.foldLeft(items.head match
|
||||
case PathItem.Parent => Path("")
|
||||
case _ => Path("/"),
|
||||
)((pathSoFar: Path, nextPathItem: PathItem) =>
|
||||
pathSoFar :+ (
|
||||
nextPathItem match
|
||||
case PathItem.Member(_) => PathItem(PathItem.WildcardKey)
|
||||
case _ => nextPathItem
|
||||
),
|
||||
)
|
||||
|
||||
def populateWildcards(
|
||||
graph: Graph,
|
||||
pathsWithoutWildcards: Seq[Path] = Nil,
|
||||
): Seq[Path] =
|
||||
isWildcard match
|
||||
case false => this :: pathsWithoutWildcards.toList
|
||||
case true =>
|
||||
this
|
||||
.populateFirstWildcard(graph)
|
||||
.flatMap((pathPerhapsWithWildcards: Path) =>
|
||||
pathPerhapsWithWildcards
|
||||
.populateWildcards(graph, pathsWithoutWildcards),
|
||||
)
|
||||
|
||||
private def populateFirstWildcard(graph: Graph): Seq[Path] =
|
||||
val pathItemsBeforeFirstWildcard = _items.reverse.takeWhile(!_.isWildcard)
|
||||
val pathItemsAfterFirstWildcard =
|
||||
_items.reverse.dropWhile(!_.isWildcard).dropWhile(_.isWildcard)
|
||||
|
||||
val pathBeforeWildcard =
|
||||
pathItemsBeforeFirstWildcard.foldLeft(Path(Path.Delimiter)) { (pathSoFar, pathItem) =>
|
||||
pathSoFar :+ pathItem
|
||||
}
|
||||
try
|
||||
val vector =
|
||||
graph.getVect(pathBeforeWildcard :+ PathItem(PathItem.WildcardKey))
|
||||
|
||||
vector.match
|
||||
case MaybeVector.Multiple[Result[CollectionItem]](multiple, complete) if complete =>
|
||||
for result <- multiple
|
||||
yield (pathBeforeWildcard :+ PathItem(
|
||||
s"${PathItem.MemberPrefix}${result.value.get.id}",
|
||||
)) ++ pathItemsAfterFirstWildcard
|
||||
case _ => Nil // collection is empty
|
||||
catch case e: UnsupportedOperationException => Nil
|
||||
|
||||
override def toString: String =
|
||||
val prefix = if (absolute) Path.Delimiter else ""
|
||||
val path = items.map(_.toString()).mkString(Path.Delimiter)
|
||||
s"$prefix$path"
|
||||
|
||||
object Path:
|
||||
val Delimiter = "/"
|
||||
|
||||
val Root: Path = new Path(Nil, true)
|
||||
val Relative: Path = new Path(Nil, false)
|
||||
|
||||
def apply(str: String): Path = str match
|
||||
case Delimiter => Root
|
||||
case "" => Relative
|
||||
case _ =>
|
||||
val item_strs = str.split(Delimiter, 0)
|
||||
val absolute = item_strs(0).isEmpty
|
||||
|
||||
val items = item_strs
|
||||
.drop(if (absolute) 1 else 0)
|
||||
.map(PathItem(_))
|
||||
.reverse
|
||||
.toList
|
||||
|
||||
Path(items, absolute)
|
|
@ -0,0 +1,54 @@
|
|||
package gov.irs.factgraph
|
||||
import upickle.default.ReadWriter
|
||||
import java.util.UUID
|
||||
|
||||
enum PathItem derives ReadWriter:
|
||||
case Child(key: Symbol)
|
||||
case Parent
|
||||
|
||||
// Collections
|
||||
case Member(id: UUID)
|
||||
case Wildcard
|
||||
case Unknown
|
||||
|
||||
def isKnown: Boolean = this match
|
||||
case Child(_) | Member(_) => true
|
||||
case _ => false
|
||||
|
||||
def isAbstract: Boolean = this match
|
||||
case Child(_) | Wildcard => true
|
||||
case _ => false
|
||||
|
||||
def isWildcard: Boolean = this match
|
||||
case Wildcard => true
|
||||
case _ => false
|
||||
|
||||
def isCollectionMember: Boolean = this match
|
||||
case Member(_) => true
|
||||
case _ => false
|
||||
|
||||
def asAbstract: PathItem = this match
|
||||
case Member(_) | Unknown => Wildcard
|
||||
case _ => this
|
||||
|
||||
override def toString: String = this match
|
||||
case Wildcard => PathItem.WildcardKey
|
||||
case Unknown => PathItem.UnknownKey
|
||||
case Parent => PathItem.ParentKey
|
||||
case Member(id) => s"${PathItem.MemberPrefix}${id}"
|
||||
case Child(key) => key.name
|
||||
|
||||
object PathItem:
|
||||
val WildcardKey = "*"
|
||||
private val UnknownKey = "?"
|
||||
private val ParentKey = ".."
|
||||
|
||||
val MemberPrefix = '#'
|
||||
|
||||
def apply(str: String): PathItem = str match
|
||||
case WildcardKey => PathItem.Wildcard
|
||||
case UnknownKey => PathItem.Unknown
|
||||
case ParentKey => PathItem.Parent
|
||||
case _ if str.charAt(0) == MemberPrefix =>
|
||||
PathItem.Member(UUID.fromString(str.substring(1)))
|
||||
case _ => PathItem.Child(Symbol(str))
|
|
@ -0,0 +1,3 @@
|
|||
package gov.irs.factgraph
|
||||
|
||||
case class PersisterSyncIssue(path: String, message: String)
|
|
@ -0,0 +1,169 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.types.{*, given}
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.{BinaryOperator, ReduceOperator}
|
||||
import gov.irs.factgraph.util.Seq.itemsHaveSameRuntimeClass
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
// Sum is used across multiple child nodes. If you're trying to add
|
||||
// a maybe vector from a collection, use the `Sum` node.
|
||||
object Add extends CompNodeFactory:
|
||||
override val Key: String = "Add"
|
||||
|
||||
def apply(nodes: Seq[CompNode]): CompNode =
|
||||
if (itemsHaveSameRuntimeClass(nodes))
|
||||
reduceAdd(nodes)
|
||||
else
|
||||
nodes.reduceLeft(binaryAdd)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val addends = CompNode.getConfigChildNodes(e)
|
||||
this(addends)
|
||||
|
||||
private def reduceAdd(nodes: Seq[CompNode]): CompNode =
|
||||
nodes.head match
|
||||
case node: IntNode =>
|
||||
IntNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[IntNode]].map(_.expr),
|
||||
summon[AddReduceOperator[Int]],
|
||||
),
|
||||
)
|
||||
case node: DollarNode =>
|
||||
DollarNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[DollarNode]].map(_.expr),
|
||||
summon[AddReduceOperator[Dollar]],
|
||||
),
|
||||
)
|
||||
case node: RationalNode =>
|
||||
RationalNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[RationalNode]].map(_.expr),
|
||||
summon[AddReduceOperator[Rational]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot add a ${nodes.head.getClass.getName}",
|
||||
)
|
||||
|
||||
private def binaryAdd(lhs: CompNode, rhs: CompNode): CompNode =
|
||||
(lhs, rhs).match
|
||||
case (lhs: IntNode, rhs: IntNode) =>
|
||||
IntNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Int, Int, Int]],
|
||||
),
|
||||
)
|
||||
case (lhs: DollarNode, rhs: DollarNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Dollar, Dollar, Dollar]],
|
||||
),
|
||||
)
|
||||
case (lhs: RationalNode, rhs: RationalNode) =>
|
||||
RationalNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Rational, Rational, Rational]],
|
||||
),
|
||||
)
|
||||
case (lhs: IntNode, rhs: DollarNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Dollar, Int, Dollar]],
|
||||
),
|
||||
)
|
||||
case (lhs: DollarNode, rhs: IntNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Dollar, Dollar, Int]],
|
||||
),
|
||||
)
|
||||
case (lhs: IntNode, rhs: RationalNode) =>
|
||||
RationalNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Rational, Int, Rational]],
|
||||
),
|
||||
)
|
||||
case (lhs: RationalNode, rhs: IntNode) =>
|
||||
RationalNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Rational, Rational, Int]],
|
||||
),
|
||||
)
|
||||
case (lhs: RationalNode, rhs: DollarNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Dollar, Rational, Dollar]],
|
||||
),
|
||||
)
|
||||
case (lhs: DollarNode, rhs: RationalNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[AddBinaryOperator[Dollar, Dollar, Rational]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot add a ${lhs.getClass.getName} and a ${rhs.getClass.getName}",
|
||||
)
|
||||
|
||||
private final class AddReduceOperator[A: Numeric] extends ReduceOperator[A]:
|
||||
override protected def reduce(x: A, y: A): A = Numeric[A].plus(x, y)
|
||||
|
||||
@unused
|
||||
private object AddReduceOperator:
|
||||
implicit val intOperator: AddReduceOperator[Int] = AddReduceOperator[Int]
|
||||
implicit val dollarOperator: AddReduceOperator[Dollar] =
|
||||
AddReduceOperator[Dollar]
|
||||
implicit val rationalOperator: AddReduceOperator[Rational] =
|
||||
AddReduceOperator[Rational]
|
||||
// implicit def numericOperator[A: Numeric]: AddReduceOperator[A] =
|
||||
// AddReduceOperator[A]
|
||||
|
||||
private trait AddBinaryOperator[A, L, R] extends BinaryOperator[A, L, R]
|
||||
|
||||
@unused
|
||||
private object AddBinaryOperator:
|
||||
implicit val intIntOperator: AddBinaryOperator[Int, Int, Int] =
|
||||
(lhs: Int, rhs: Int) => lhs + rhs
|
||||
implicit val dollarDollarOperator: AddBinaryOperator[Dollar, Dollar, Dollar] =
|
||||
(lhs: Dollar, rhs: Dollar) => lhs + rhs
|
||||
implicit val rationalRationalOperator: AddBinaryOperator[Rational, Rational, Rational] =
|
||||
(lhs: Rational, rhs: Rational) => lhs + rhs
|
||||
implicit val dollarIntOperator: AddBinaryOperator[Dollar, Dollar, Int] =
|
||||
(lhs: Dollar, rhs: Int) => Numeric[Dollar].plus(lhs, rhs)
|
||||
implicit val intDollarOperator: AddBinaryOperator[Dollar, Int, Dollar] =
|
||||
(lhs: Int, rhs: Dollar) => Numeric[Dollar].plus(lhs, rhs)
|
||||
implicit val rationalIntOperator: AddBinaryOperator[Rational, Rational, Int] =
|
||||
(lhs: Rational, rhs: Int) => Numeric[Rational].plus(lhs, rhs)
|
||||
implicit val intRationalOperator: AddBinaryOperator[Rational, Int, Rational] =
|
||||
(lhs: Int, rhs: Rational) => Numeric[Rational].plus(lhs, rhs)
|
||||
implicit val dollarRationalOperator: AddBinaryOperator[Dollar, Dollar, Rational] =
|
||||
(lhs: Dollar, rhs: Rational) => Numeric[Dollar].plus(lhs, rhs)
|
||||
implicit val rationalDollarOperator: AddBinaryOperator[Dollar, Rational, Dollar] =
|
||||
(lhs: Rational, rhs: Dollar) => Numeric[Dollar].plus(lhs, rhs)
|
|
@ -0,0 +1,80 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.monads.{MaybeVector, Result}
|
||||
import gov.irs.factgraph.types.Address
|
||||
|
||||
final case class AddressNode(expr: Expression[Address]) extends CompNode:
|
||||
type Value = Address
|
||||
override def ValueClass = classOf[Address]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[Address],
|
||||
): CompNode =
|
||||
AddressNode(expr)
|
||||
|
||||
override def extract(key: PathItem): Option[CompNode] =
|
||||
key match
|
||||
case PathItem.Child(Symbol("streetAddress")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[Address]) => x.map(y => y.streetAddress)),
|
||||
),
|
||||
)
|
||||
case PathItem.Child(Symbol("city")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[Address]) => x.map(y => y.city)),
|
||||
),
|
||||
)
|
||||
case PathItem.Child(Symbol("postalCode")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[Address]) => x.map(y => y.postalCode)),
|
||||
),
|
||||
)
|
||||
case PathItem.Child(Symbol("stateOrProvence")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[Address]) => x.map(y => y.stateOrProvence)),
|
||||
),
|
||||
)
|
||||
case PathItem.Child(Symbol("streetAddressLine2")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[Address]) => x.map(y => y.streetAddressLine2)),
|
||||
),
|
||||
)
|
||||
case PathItem.Child(Symbol("country")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[Address]) => x.map(y => y.country)),
|
||||
),
|
||||
)
|
||||
case PathItem.Child(Symbol("foreignAddress")) =>
|
||||
Some(
|
||||
BooleanNode(
|
||||
Expression.Extract((x: Result[Address]) => x.map(y => y.foreignAddress())),
|
||||
),
|
||||
)
|
||||
case _ => None
|
||||
|
||||
object AddressNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "Address"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
new AddressNode(
|
||||
Expression.Writable(classOf[Address]),
|
||||
)
|
||||
|
||||
def apply(value: Address): AddressNode = new AddressNode(
|
||||
Expression.Constant(Some(value)),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(Address(e.getOptionValue(CommonOptionConfigTraits.VALUE).get))
|
|
@ -0,0 +1,80 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.{Explanation, Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.operators.ReduceOperator
|
||||
import gov.irs.factgraph.monads.{MaybeVector, Result, Thunk}
|
||||
|
||||
object All extends CompNodeFactory:
|
||||
override val Key: String = "All"
|
||||
|
||||
private val operator = AllOperator()
|
||||
|
||||
def apply(nodes: Seq[BooleanNode]): BooleanNode =
|
||||
BooleanNode(Expression.Reduce(nodes.map(_.expr).toList, operator))
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val conditions = CompNode.getConfigChildNodes(e)
|
||||
|
||||
if (conditions.forall(_.isInstanceOf[BooleanNode]))
|
||||
this(conditions.asInstanceOf[Seq[BooleanNode]])
|
||||
else
|
||||
throw new UnsupportedOperationException(
|
||||
"all children of <All> must be BooleanNodes",
|
||||
)
|
||||
|
||||
private final class AllOperator extends ReduceOperator[Boolean]:
|
||||
// $COVERAGE-OFF$
|
||||
override protected def reduce(x: Boolean, y: Boolean): Boolean = ???
|
||||
// $COVERAGE-ON$
|
||||
|
||||
override def apply(
|
||||
head: Result[Boolean],
|
||||
tail: List[Thunk[Result[Boolean]]],
|
||||
): Result[Boolean] = head match
|
||||
case Result.Complete(false) => Result.Complete(false)
|
||||
case _ => accumulator(tail, head)
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def accumulator(
|
||||
thunks: List[Thunk[Result[Boolean]]],
|
||||
a: Result[Boolean],
|
||||
): Result[Boolean] = thunks match
|
||||
case thunk :: thunks =>
|
||||
thunk.get match
|
||||
case Result.Complete(false) => Result.Complete(false)
|
||||
case Result(value, complete) =>
|
||||
accumulator(
|
||||
thunks,
|
||||
a.flatMap(aValue => Result(value && aValue, complete)),
|
||||
)
|
||||
case _ => accumulator(thunks, Result.Incomplete)
|
||||
case Nil => a
|
||||
|
||||
override def explain(
|
||||
xs: List[Expression[_]],
|
||||
)(using Factual): MaybeVector[Explanation] =
|
||||
val caseVectors = xs.map(x => (x.getThunk, x.explain))
|
||||
|
||||
MaybeVector.vectorizeListTuple2(
|
||||
cases => explainRecurse(cases, Explanation.Operation(List())),
|
||||
caseVectors,
|
||||
)
|
||||
|
||||
@annotation.tailrec
|
||||
private def explainRecurse(
|
||||
cases: List[(Thunk[Result[_]], Explanation)],
|
||||
explanation: Explanation,
|
||||
): Explanation = cases match
|
||||
case (x, xExp) :: next =>
|
||||
x.get match
|
||||
case Result.Complete(false) =>
|
||||
Explanation.opWithInclusiveChildren(xExp)
|
||||
case _ =>
|
||||
explainRecurse(
|
||||
next,
|
||||
Explanation.Operation(explanation.children :+ List(xExp)),
|
||||
)
|
||||
case Nil => explanation
|
|
@ -0,0 +1,107 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.{Explanation, Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.operators.ReduceOperator
|
||||
import gov.irs.factgraph.monads.{MaybeVector, Result, Thunk}
|
||||
|
||||
object Any extends CompNodeFactory:
|
||||
override val Key: String = "Any"
|
||||
|
||||
private val operator = AnyOperator()
|
||||
|
||||
def apply(nodes: Seq[BooleanNode]): BooleanNode =
|
||||
BooleanNode(Expression.Reduce(nodes.map(_.expr).toList, operator))
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val conditions = CompNode.getConfigChildNodes(e)
|
||||
|
||||
if (conditions.forall(_.isInstanceOf[BooleanNode]))
|
||||
this(conditions.asInstanceOf[Seq[BooleanNode]])
|
||||
else
|
||||
throw new UnsupportedOperationException(
|
||||
"all children of <Any> must be BooleanNodes",
|
||||
)
|
||||
|
||||
private final class AnyOperator extends ReduceOperator[Boolean]:
|
||||
// $COVERAGE-OFF$
|
||||
override protected def reduce(x: Boolean, y: Boolean): Boolean = ???
|
||||
// $COVERAGE-ON$
|
||||
|
||||
override def apply(
|
||||
head: Result[Boolean],
|
||||
tail: List[Thunk[Result[Boolean]]],
|
||||
): Result[Boolean] = head match
|
||||
case Result.Complete(true) => Result.Complete(true)
|
||||
case _ => accumulator(tail, head)
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def accumulator(
|
||||
thunks: List[Thunk[Result[Boolean]]],
|
||||
a: Result[Boolean],
|
||||
): Result[Boolean] = thunks match
|
||||
case thunk :: thunks =>
|
||||
val result = thunk.get
|
||||
|
||||
result match
|
||||
case Result.Complete(true) => Result.Complete(true)
|
||||
case Result.Placeholder(true) =>
|
||||
accumulator(thunks, Result.Placeholder(true))
|
||||
case Result.Incomplete =>
|
||||
if (a == Result.Placeholder(true)) then
|
||||
// any set containing Placeholder(true) returns Placeholder(true),
|
||||
// unless it includes a Complete(true) result
|
||||
accumulator(thunks, Result.Placeholder(true))
|
||||
else
|
||||
// otherwise, any set containing Incomplete returns Incomplete
|
||||
accumulator(thunks, Result.Incomplete)
|
||||
case _ => // Complete(false) or Placeholder(false)
|
||||
if (a.complete) then
|
||||
// if the accumulated result is Complete(false), we'll use the new
|
||||
// result, which is either Complete(false) or Placeholder(false)
|
||||
//
|
||||
// ACCUMULATED NEW RESULT
|
||||
// Complete(false) + Complete(false) = Complete(false)
|
||||
// Complete(false) + Placeholder(false) = Placeholder(false)
|
||||
accumulator(thunks, result)
|
||||
else
|
||||
// otherwise, we'll use the accumulated result, which is either
|
||||
// Placeholder(true), Placeholder(false), or Incomplete
|
||||
//
|
||||
// ACCUMULATED NEW RESULT
|
||||
// Placeholder(true) + Complete(false) = Placeholder(true)
|
||||
// Placeholder(true) + Placeholder(false) = Placeholder(true)
|
||||
// Placeholder(false) + Complete(false) = Placeholder(false)
|
||||
// Placeholder(false) + Placeholder(false) = Placeholder(false)
|
||||
// Incomplete + Complete(false) = Incomplete
|
||||
// Incomplete + Placeholder(false) = Incomplete
|
||||
accumulator(thunks, a)
|
||||
case Nil => a
|
||||
|
||||
override def explain(
|
||||
xs: List[Expression[_]],
|
||||
)(using Factual): MaybeVector[Explanation] =
|
||||
val caseVectors = xs.map(x => (x.getThunk, x.explain))
|
||||
|
||||
MaybeVector.vectorizeListTuple2(
|
||||
cases => explainRecurse(cases, Explanation.Operation(List())),
|
||||
caseVectors,
|
||||
)
|
||||
|
||||
@annotation.tailrec
|
||||
private def explainRecurse(
|
||||
cases: List[(Thunk[Result[_]], Explanation)],
|
||||
explanation: Explanation,
|
||||
): Explanation = cases match
|
||||
case (x, xExp) :: next =>
|
||||
x.get match
|
||||
case Result.Complete(true) =>
|
||||
Explanation.opWithInclusiveChildren(xExp)
|
||||
case _ =>
|
||||
explainRecurse(
|
||||
next,
|
||||
Explanation.Operation(explanation.children :+ List(xExp)),
|
||||
)
|
||||
case Nil => explanation
|
|
@ -0,0 +1,53 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.UnaryOperator
|
||||
import gov.irs.factgraph.types.Rational
|
||||
|
||||
object AsDecimalString extends CompNodeFactory:
|
||||
override val Key: String = "AsDecimalString"
|
||||
|
||||
private val defaultScale = 2
|
||||
private val defaultScaleAsString = defaultScale.toString()
|
||||
private val defaultOperator = RationalAsDecimalString(defaultScale)
|
||||
|
||||
def apply(node: CompNode, scale: Int): StringNode =
|
||||
val operator =
|
||||
if (scale == defaultScale) defaultOperator
|
||||
else RationalAsDecimalString(scale)
|
||||
node match
|
||||
case node: RationalNode =>
|
||||
StringNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
operator,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot execute AsDecimalString on a ${node.getClass.getName}",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
CompNode.getConfigChildNode(e) match
|
||||
case x: RationalNode =>
|
||||
this(
|
||||
x,
|
||||
e.getOptionValue(CommonOptionConfigTraits.SCALE)
|
||||
.getOrElse(defaultScaleAsString)
|
||||
.toIntOption
|
||||
.get,
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"invalid child type: ${e.typeName}",
|
||||
)
|
||||
|
||||
private final class RationalAsDecimalString(val scale: Int) extends UnaryOperator[String, Rational]:
|
||||
override protected def operation(x: Rational): String =
|
||||
(BigDecimal(x.numerator) / BigDecimal(x.denominator))
|
||||
.setScale(scale, BigDecimal.RoundingMode.HALF_UP)
|
||||
.toString()
|
|
@ -0,0 +1,86 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.UnaryOperator
|
||||
import gov.irs.factgraph.types.{Dollar, Enum, EmailAddress, Ein, Tin}
|
||||
|
||||
object AsString extends CompNodeFactory:
|
||||
override val Key: String = "AsString"
|
||||
|
||||
private val enumOperator = EnumAsStringOperator()
|
||||
private val emailAddressOperator = EmailAsStringOperator()
|
||||
private val dollarOperator = DollarAsStringOperator()
|
||||
private val einOperator = EinAsStringOperator()
|
||||
private val tinOperator = TinAsStringOperator()
|
||||
|
||||
def apply(node: CompNode): StringNode =
|
||||
node match
|
||||
case node: EnumNode =>
|
||||
StringNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
enumOperator,
|
||||
),
|
||||
)
|
||||
case node: EmailAddressNode =>
|
||||
StringNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
emailAddressOperator,
|
||||
),
|
||||
)
|
||||
case node: DollarNode =>
|
||||
StringNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
dollarOperator,
|
||||
),
|
||||
)
|
||||
case node: EinNode =>
|
||||
StringNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
einOperator,
|
||||
),
|
||||
)
|
||||
case node: TinNode =>
|
||||
StringNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
tinOperator,
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot execute AsString on a ${node.getClass.getName}",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
CompNode.getConfigChildNode(e) match
|
||||
case x: EnumNode => this(x)
|
||||
case x: EmailAddressNode => this(x)
|
||||
case x: DollarNode => this(x)
|
||||
case x: EinNode => this(x)
|
||||
case x: TinNode => this(x)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"invalid child type: ${e.typeName}",
|
||||
)
|
||||
|
||||
private final class EnumAsStringOperator extends UnaryOperator[String, Enum]:
|
||||
override protected def operation(x: Enum): String = x.getValue()
|
||||
|
||||
private final class EmailAsStringOperator extends UnaryOperator[String, EmailAddress]:
|
||||
override protected def operation(x: EmailAddress): String = x.toString()
|
||||
|
||||
private final class DollarAsStringOperator extends UnaryOperator[String, Dollar]:
|
||||
override protected def operation(x: Dollar): String = x.toString()
|
||||
|
||||
private final class EinAsStringOperator extends UnaryOperator[String, Ein]:
|
||||
override protected def operation(x: Ein): String = x.toString()
|
||||
|
||||
private final class TinAsStringOperator extends UnaryOperator[String, Tin]:
|
||||
override protected def operation(x: Tin): String = x.toString()
|
|
@ -0,0 +1,56 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.monads.{MaybeVector, Result}
|
||||
import gov.irs.factgraph.types.BankAccount
|
||||
|
||||
final case class BankAccountNode(expr: Expression[BankAccount]) extends CompNode:
|
||||
type Value = BankAccount
|
||||
override def ValueClass = classOf[BankAccount]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[BankAccount],
|
||||
): CompNode =
|
||||
BankAccountNode(expr)
|
||||
|
||||
override def extract(key: PathItem): Option[CompNode] =
|
||||
key match
|
||||
case PathItem.Child(Symbol("accountType")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[BankAccount]) => x.map(y => y.accountType)),
|
||||
),
|
||||
)
|
||||
case PathItem.Child(Symbol("routingNumber")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[BankAccount]) => x.map(y => y.routingNumber)),
|
||||
),
|
||||
)
|
||||
case PathItem.Child(Symbol("accountNumber")) =>
|
||||
Some(
|
||||
StringNode(
|
||||
Expression.Extract((x: Result[BankAccount]) => x.map(y => y.accountNumber)),
|
||||
),
|
||||
)
|
||||
case _ => None
|
||||
|
||||
object BankAccountNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "BankAccount"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
new BankAccountNode(
|
||||
Expression.Writable(classOf[BankAccount]),
|
||||
)
|
||||
|
||||
def apply(value: BankAccount): BankAccountNode = new BankAccountNode(
|
||||
Expression.Constant(Some(value)),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
throw new NotImplementedError("BankAccoutNode.fromDerivedConfig")
|
|
@ -0,0 +1,43 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.{CompNodeConfigTrait, WritableConfigTrait}
|
||||
|
||||
final case class BooleanNode(expr: Expression[Boolean]) extends CompNode:
|
||||
type Value = Boolean
|
||||
|
||||
override def ValueClass =
|
||||
classOf[java.lang.Boolean].asInstanceOf[Class[Boolean]]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[Boolean],
|
||||
): CompNode =
|
||||
BooleanNode(expr)
|
||||
|
||||
object BooleanNode extends WritableNodeFactory:
|
||||
override val Key: String = "Boolean"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
new BooleanNode(
|
||||
Expression.Writable(
|
||||
classOf[java.lang.Boolean].asInstanceOf[Class[Boolean]],
|
||||
),
|
||||
)
|
||||
|
||||
def apply(value: Boolean): BooleanNode = if (value) True.node else False.node
|
||||
|
||||
object True extends CompNodeFactory:
|
||||
override val Key: String = "True"
|
||||
val node: BooleanNode = new BooleanNode(Expression.Constant(Some(true)))
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode = node
|
||||
|
||||
object False extends CompNodeFactory:
|
||||
override val Key: String = "False"
|
||||
val node: BooleanNode = new BooleanNode(Expression.Constant(Some(false)))
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode = node
|
|
@ -0,0 +1,60 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.WritableConfigTrait
|
||||
import gov.irs.factgraph.types.CollectionItem
|
||||
import scala.scalajs.js.annotation.JSExport
|
||||
|
||||
final case class CollectionItemNode(
|
||||
expr: Expression[CollectionItem],
|
||||
alias: Option[Path],
|
||||
) extends CompNode:
|
||||
type Value = CollectionItem
|
||||
override def ValueClass = classOf[CollectionItem]
|
||||
|
||||
@JSExport
|
||||
def getAlias() = alias
|
||||
|
||||
override private[compnodes] def switch(
|
||||
cases: List[(BooleanNode, CompNode)],
|
||||
): CompNode =
|
||||
val aliasesMatch = cases.forall(
|
||||
_._2.asInstanceOf[CollectionItemNode].alias == alias,
|
||||
)
|
||||
|
||||
if (!aliasesMatch)
|
||||
throw new UnsupportedOperationException(
|
||||
"collection items in a <Switch> must reference the same collection",
|
||||
)
|
||||
|
||||
CollectionItemNode(
|
||||
Expression.Switch(
|
||||
cases
|
||||
.asInstanceOf[List[(BooleanNode, CollectionItemNode)]]
|
||||
.map((b, a) => (b.expr, a.expr)),
|
||||
),
|
||||
alias,
|
||||
)
|
||||
|
||||
override private[compnodes] def dependency(path: Path): CompNode =
|
||||
val newAlias = alias match
|
||||
case Some(_) => alias
|
||||
case None => Some(path)
|
||||
|
||||
CollectionItemNode(Expression.Dependency(path), newAlias)
|
||||
|
||||
override private[factgraph] def fromExpression(
|
||||
expr: Expression[CollectionItem],
|
||||
): CompNode =
|
||||
CollectionItemNode(expr, alias)
|
||||
|
||||
object CollectionItemNode extends WritableNodeFactory:
|
||||
override val Key: String = "CollectionItem"
|
||||
|
||||
override def fromWritableConfig(
|
||||
e: WritableConfigTrait,
|
||||
)(using Factual)(using FactDictionary): CompNode =
|
||||
new CollectionItemNode(
|
||||
Expression.Writable(classOf[CollectionItem]),
|
||||
Some(Path(e.collectionItemAlias.get)),
|
||||
)
|
|
@ -0,0 +1,71 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.*
|
||||
import gov.irs.factgraph.definitions.fact.WritableConfigTrait
|
||||
import gov.irs.factgraph.types.{Collection, CollectionItem}
|
||||
|
||||
final case class CollectionNode(
|
||||
expr: Expression[Collection],
|
||||
alias: Option[Path],
|
||||
) extends CompNode:
|
||||
type Value = Collection
|
||||
override def ValueClass = classOf[Collection]
|
||||
|
||||
override private[compnodes] def switch(
|
||||
cases: List[(BooleanNode, CompNode)],
|
||||
): CompNode =
|
||||
val aliasesMatch = cases.forall(
|
||||
_._2.asInstanceOf[CollectionNode].alias == alias,
|
||||
)
|
||||
|
||||
if (!aliasesMatch)
|
||||
throw new UnsupportedOperationException(
|
||||
"collections in a <Switch> must reference the same collection",
|
||||
)
|
||||
|
||||
CollectionNode(
|
||||
Expression.Switch(
|
||||
cases
|
||||
.asInstanceOf[List[(BooleanNode, CollectionNode)]]
|
||||
.map((b, a) => (b.expr, a.expr)),
|
||||
),
|
||||
alias,
|
||||
)
|
||||
|
||||
override private[compnodes] def dependency(path: Path): CompNode =
|
||||
val newAlias = alias match
|
||||
case Some(_) => alias
|
||||
case None => Some(path)
|
||||
|
||||
CollectionNode(Expression.Dependency(path), newAlias)
|
||||
|
||||
override private[factgraph] def fromExpression(
|
||||
expr: Expression[Collection],
|
||||
): CompNode =
|
||||
throw new UnsupportedOperationException(
|
||||
"cannot create a Collection from an expression",
|
||||
)
|
||||
|
||||
override def extract(key: PathItem): Option[CompNode] =
|
||||
key match
|
||||
case PathItem.Member(id) =>
|
||||
Some(
|
||||
CollectionItemNode(
|
||||
Expression.Constant(Some(CollectionItem(id))),
|
||||
None,
|
||||
),
|
||||
)
|
||||
case PathItem.Unknown =>
|
||||
Some(CollectionItemNode(Expression.Constant(None), None))
|
||||
case _ => None
|
||||
|
||||
object CollectionNode extends WritableNodeFactory:
|
||||
override val Key: String = "Collection"
|
||||
|
||||
override def fromWritableConfig(
|
||||
e: WritableConfigTrait,
|
||||
)(using Factual)(using FactDictionary): CompNode =
|
||||
new CollectionNode(
|
||||
Expression.Writable(classOf[Collection]),
|
||||
None,
|
||||
)
|
|
@ -0,0 +1,31 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.{ReduceOperator, UnaryOperator}
|
||||
import gov.irs.factgraph.types.Collection
|
||||
|
||||
object CollectionSize extends CompNodeFactory:
|
||||
override val Key: String = "CollectionSize"
|
||||
private val operator = CollectionSizeOperator()
|
||||
def apply(node: CollectionNode): IntNode =
|
||||
IntNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
operator,
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
CompNode.getConfigChildNode(e) match
|
||||
case x: CollectionNode => this(x)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"invalid child type: ${e.typeName}",
|
||||
)
|
||||
|
||||
private final class CollectionSizeOperator extends UnaryOperator[Int, Collection]:
|
||||
override protected def operation(x: Collection): Int =
|
||||
x.items.length
|
|
@ -0,0 +1,73 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.operators.AggregateOperator
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.types.*
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
// Sum is used across a collection. If you're trying to add
|
||||
// a number of dependencies, use the `Add` node.
|
||||
object CollectionSum extends CompNodeFactory:
|
||||
override val Key: String = "CollectionSum"
|
||||
|
||||
def apply(node: CompNode): CompNode =
|
||||
node match
|
||||
case node: IntNode =>
|
||||
IntNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[SumOperator[Int]],
|
||||
),
|
||||
)
|
||||
case node: DollarNode =>
|
||||
DollarNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[SumOperator[Dollar]],
|
||||
),
|
||||
)
|
||||
case node: RationalNode =>
|
||||
RationalNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[SumOperator[Rational]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot sum a ${node.getClass.getName}",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(CompNode.getConfigChildNode(e))
|
||||
private final class SumOperator[A: Numeric] extends AggregateOperator[A, A]:
|
||||
override def apply(vect: MaybeVector[Thunk[Result[A]]]): Result[A] =
|
||||
accumulator(vect.toList, Result(Numeric[A].zero, vect.complete))
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def accumulator(
|
||||
thunks: List[Thunk[Result[A]]],
|
||||
a: Result[A],
|
||||
): Result[A] = thunks match
|
||||
case thunk :: thunks =>
|
||||
thunk.get match
|
||||
case Result(value, complete) =>
|
||||
val sum = Numeric[A].plus(value, a.get)
|
||||
accumulator(
|
||||
thunks,
|
||||
Result(sum, complete && a.complete),
|
||||
)
|
||||
case _ => Result.Incomplete
|
||||
case Nil => a
|
||||
|
||||
@unused
|
||||
private object SumOperator:
|
||||
implicit val intOperator: SumOperator[Int] = SumOperator[Int]
|
||||
implicit val dollarOperator: SumOperator[Dollar] = SumOperator[Dollar]
|
||||
implicit val rationalOperator: SumOperator[Rational] = SumOperator[Rational]
|
||||
// implicit def numericOperator[A: Numeric]: SumOperator[A] = SumOperator[A]
|
|
@ -0,0 +1,181 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.*
|
||||
import gov.irs.factgraph.definitions.*
|
||||
import gov.irs.factgraph.definitions.fact.{CompNodeConfigTrait, LimitConfigTrait}
|
||||
import gov.irs.factgraph.persisters.Persister
|
||||
import gov.irs.factgraph.util.Seq.itemsHaveSameRuntimeClass
|
||||
|
||||
import scala.collection.mutable
|
||||
import gov.irs.factgraph.limits.Limit
|
||||
import transformations.TruncateNameForMeF
|
||||
|
||||
trait CompNode:
|
||||
type Value
|
||||
|
||||
/** Runtime class of associated value */
|
||||
def ValueClass: Class[Value]
|
||||
|
||||
val expr: Expression[Value]
|
||||
export expr.{get, getThunk, explain, set, delete, isWritable}
|
||||
|
||||
private[compnodes] def fromExpression(expr: Expression[Value]): CompNode
|
||||
private[compnodes] def switch(
|
||||
cases: List[(BooleanNode, CompNode)],
|
||||
): CompNode =
|
||||
if (!itemsHaveSameRuntimeClass(cases.map(_._2)))
|
||||
throw new UnsupportedOperationException(
|
||||
"cannot switch between nodes of different types",
|
||||
)
|
||||
|
||||
fromExpression(
|
||||
Expression.Switch(
|
||||
cases.map((b, a) => (b.expr, a.expr.asInstanceOf[Expression[Value]])),
|
||||
),
|
||||
)
|
||||
private[compnodes] def dependency(path: Path): CompNode = fromExpression(
|
||||
Expression.Dependency(path),
|
||||
)
|
||||
def extract(key: PathItem): Option[CompNode] = None
|
||||
|
||||
def getIntrinsicLimits()(using Factual): Seq[Limit] = Seq.empty
|
||||
|
||||
object CompNode:
|
||||
private val defaultFactories: Seq[CompNodeFactory] = List(
|
||||
// Constant nodes
|
||||
BooleanNode.False,
|
||||
BooleanNode.True,
|
||||
DollarNode,
|
||||
IntNode,
|
||||
DaysNode,
|
||||
RationalNode,
|
||||
DayNode,
|
||||
StringNode,
|
||||
TinNode,
|
||||
EinNode,
|
||||
EmailAddressNode,
|
||||
AddressNode,
|
||||
BankAccountNode,
|
||||
EnumNode,
|
||||
MultiEnumNode,
|
||||
PhoneNumberNode,
|
||||
|
||||
// Operation nodes
|
||||
Add,
|
||||
All,
|
||||
Any,
|
||||
Today,
|
||||
AsString,
|
||||
AsDecimalString,
|
||||
CollectionSize,
|
||||
Count,
|
||||
Dependency,
|
||||
Divide,
|
||||
EnumOptionsContains,
|
||||
EnumOptionsSize,
|
||||
EnumOptionsNode,
|
||||
Equal,
|
||||
Filter,
|
||||
FirstNCollectionItems,
|
||||
Find,
|
||||
GreaterOf,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
IndexOf,
|
||||
IsComplete,
|
||||
Length,
|
||||
LesserOf,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
Maximum,
|
||||
Minimum,
|
||||
Multiply,
|
||||
Not,
|
||||
NotEqual,
|
||||
Paste,
|
||||
Placeholder,
|
||||
Regex,
|
||||
Round,
|
||||
RoundToInt,
|
||||
StepwiseMultiply,
|
||||
StripChars,
|
||||
Subtract,
|
||||
CollectionSum,
|
||||
Switch,
|
||||
Trim,
|
||||
ToUpper,
|
||||
TruncateCents,
|
||||
TruncateNameForMeF,
|
||||
)
|
||||
|
||||
private val factories = mutable.Map(defaultFactories.map(_.asTuple)*)
|
||||
|
||||
def register(f: CompNodeFactory): Unit = factories.addOne(f.asTuple)
|
||||
|
||||
def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val factory = factories.getOrElse(
|
||||
e.typeName,
|
||||
throw new UnsupportedOperationException(
|
||||
s"${e.typeName} is not a registered CompNode",
|
||||
),
|
||||
)
|
||||
|
||||
factory.fromDerivedConfig(e)
|
||||
|
||||
def getConfigChildNode(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val children = e.children
|
||||
.map(fromDerivedConfig(_))
|
||||
|
||||
children match
|
||||
case child :: Nil => child
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(
|
||||
s"<${e.typeName}> must have exactly one child node: $e",
|
||||
)
|
||||
|
||||
def getConfigChildNodes(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): Seq[CompNode] =
|
||||
val children = e.children
|
||||
.map(fromDerivedConfig(_))
|
||||
|
||||
children match
|
||||
case Nil =>
|
||||
throw new IllegalArgumentException(
|
||||
s"<${e.typeName}> must have at least one child node: $e",
|
||||
)
|
||||
case _ => children.toSeq
|
||||
|
||||
def getConfigChildNode(e: CompNodeConfigTrait, label: String)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val children = e.children
|
||||
.filter(x => x.typeName == label)
|
||||
.flatMap(_.children)
|
||||
.map(fromDerivedConfig(_))
|
||||
|
||||
children match
|
||||
case child :: Nil => child
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(
|
||||
s"<${e.typeName}> must have exactly one <$label>: $e",
|
||||
)
|
||||
|
||||
def getConfigChildNodes(e: CompNodeConfigTrait, label: String)(using Factual)(using
|
||||
FactDictionary,
|
||||
): Seq[CompNode] =
|
||||
val children = e.children
|
||||
.filter(x => x.typeName == label)
|
||||
.flatMap(_.children)
|
||||
.map(fromDerivedConfig(_))
|
||||
|
||||
children match
|
||||
case Nil =>
|
||||
throw new IllegalArgumentException(
|
||||
s"<${e.typeName}> must have at least one <$label>: $e",
|
||||
)
|
||||
case _ => children.toSeq
|
|
@ -0,0 +1,11 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
|
||||
trait CompNodeFactory:
|
||||
val Key: String
|
||||
def asTuple: (String, CompNodeFactory) = (Key, this)
|
||||
def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode
|
|
@ -0,0 +1,44 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.operators.AggregateOperator
|
||||
import gov.irs.factgraph.monads.*
|
||||
|
||||
object Count extends CompNodeFactory:
|
||||
override val Key: String = "Count"
|
||||
|
||||
private val operator = CountOperator()
|
||||
|
||||
def apply(bool: BooleanNode): CompNode =
|
||||
IntNode(Expression.Aggregate(bool.expr, operator))
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
CompNode.getConfigChildNode(e) match
|
||||
case x: BooleanNode => this(x)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"invalid child type: $e",
|
||||
)
|
||||
|
||||
private final class CountOperator extends AggregateOperator[Int, Boolean]:
|
||||
override def apply(vect: MaybeVector[Thunk[Result[Boolean]]]): Result[Int] =
|
||||
accumulator(vect.toList, Result(0, vect.complete))
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def accumulator(
|
||||
thunks: List[Thunk[Result[Boolean]]],
|
||||
a: Result[Int],
|
||||
): Result[Int] = thunks match
|
||||
case thunk :: thunks =>
|
||||
thunk.get match
|
||||
case Result(bool, complete) =>
|
||||
val count = if bool then a.get + 1 else a.get
|
||||
accumulator(
|
||||
thunks,
|
||||
Result(count, complete && a.complete),
|
||||
)
|
||||
case _ => Result.Incomplete
|
||||
case Nil => a
|
|
@ -0,0 +1,50 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.monads.{Result}
|
||||
import gov.irs.factgraph.types.{CollectionItem, Day}
|
||||
|
||||
final case class DayNode(expr: Expression[Day]) extends CompNode:
|
||||
type Value = Day
|
||||
override def ValueClass = classOf[Day];
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[Day],
|
||||
): CompNode =
|
||||
DayNode(expr)
|
||||
|
||||
override def extract(key: PathItem): Option[CompNode] =
|
||||
key match
|
||||
case PathItem.Child(Symbol("year")) =>
|
||||
Some(
|
||||
IntNode(Expression.Extract((x: Result[Day]) => x.map(y => y.year))),
|
||||
)
|
||||
case PathItem.Child(Symbol("month")) =>
|
||||
Some(
|
||||
IntNode(Expression.Extract((x: Result[Day]) => x.map(y => y.month))),
|
||||
)
|
||||
case PathItem.Child(Symbol("day")) =>
|
||||
Some(
|
||||
IntNode(Expression.Extract((x: Result[Day]) => x.map(y => y.day))),
|
||||
)
|
||||
case _ => None
|
||||
|
||||
object DayNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "Day"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
new DayNode(
|
||||
Expression.Writable(classOf[Day]),
|
||||
)
|
||||
|
||||
def apply(value: Day): DayNode = new DayNode(
|
||||
Expression.Constant(Some(value)),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(Day(e.getOptionValue(CommonOptionConfigTraits.VALUE).get))
|
|
@ -0,0 +1,33 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.types.Days
|
||||
|
||||
final case class DaysNode(expr: Expression[Days]) extends CompNode:
|
||||
type Value = Days
|
||||
override def ValueClass = classOf[java.lang.Integer].asInstanceOf[Class[Days]]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[Days],
|
||||
): CompNode =
|
||||
DaysNode(expr)
|
||||
|
||||
object DaysNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "Days"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
new DaysNode(
|
||||
Expression.Writable(classOf[java.lang.Integer].asInstanceOf[Class[Days]]),
|
||||
)
|
||||
|
||||
def apply(value: Days): DaysNode = new DaysNode(
|
||||
Expression.Constant(Some(value)),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(Days(e.getOptionValue(CommonOptionConfigTraits.VALUE).get))
|
|
@ -0,0 +1,21 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.monads.Result
|
||||
|
||||
object Dependency extends CompNodeFactory:
|
||||
override val Key: String = "Dependency"
|
||||
|
||||
def apply(path: Path)(using fact: Factual): CompNode =
|
||||
fact(path)(0) match
|
||||
case Result.Complete(fact) => fact.value.dependency(path)
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(
|
||||
s"cannot find fact at path '$path' from '${fact.path}'",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(Path(e.getOptionValue(CommonOptionConfigTraits.PATH).get))
|
|
@ -0,0 +1,171 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.types.{*, given}
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.{BinaryOperator, ReduceOperator}
|
||||
import gov.irs.factgraph.util.Seq.itemsHaveSameRuntimeClass
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object Divide extends CompNodeFactory:
|
||||
override val Key: String = "Divide"
|
||||
|
||||
def apply(nodes: Seq[CompNode]): CompNode =
|
||||
if (itemsHaveSameRuntimeClass(nodes))
|
||||
reduceDivide(nodes)
|
||||
else
|
||||
nodes.reduceLeft(binaryDivide)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val dividend = CompNode.getConfigChildNode(e, "Dividend")
|
||||
val divisors = CompNode.getConfigChildNodes(e, "Divisors")
|
||||
|
||||
this(dividend +: divisors)
|
||||
|
||||
private def reduceDivide(nodes: Seq[CompNode]): CompNode =
|
||||
nodes.head match
|
||||
case node: IntNode =>
|
||||
nodes.reduceLeft(binaryDivide)
|
||||
case node: DollarNode =>
|
||||
DollarNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[DollarNode]].map(_.expr),
|
||||
summon[DivideReduceOperator[Dollar]],
|
||||
),
|
||||
)
|
||||
case node: RationalNode =>
|
||||
RationalNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[RationalNode]].map(_.expr),
|
||||
summon[DivideReduceOperator[Rational]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot Divide a ${nodes.head.getClass.getName}",
|
||||
)
|
||||
|
||||
private def binaryDivide(lhs: CompNode, rhs: CompNode): CompNode =
|
||||
(lhs, rhs).match
|
||||
case (lhs: IntNode, rhs: IntNode) =>
|
||||
RationalNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Rational, Int, Int]],
|
||||
),
|
||||
)
|
||||
case (lhs: DollarNode, rhs: DollarNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Dollar, Dollar, Dollar]],
|
||||
),
|
||||
)
|
||||
case (lhs: RationalNode, rhs: RationalNode) =>
|
||||
RationalNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Rational, Rational, Rational]],
|
||||
),
|
||||
)
|
||||
case (lhs: IntNode, rhs: DollarNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Dollar, Int, Dollar]],
|
||||
),
|
||||
)
|
||||
case (lhs: DollarNode, rhs: IntNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Dollar, Dollar, Int]],
|
||||
),
|
||||
)
|
||||
case (lhs: IntNode, rhs: RationalNode) =>
|
||||
RationalNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Rational, Int, Rational]],
|
||||
),
|
||||
)
|
||||
case (lhs: RationalNode, rhs: IntNode) =>
|
||||
RationalNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Rational, Rational, Int]],
|
||||
),
|
||||
)
|
||||
case (lhs: RationalNode, rhs: DollarNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Dollar, Rational, Dollar]],
|
||||
),
|
||||
)
|
||||
case (lhs: DollarNode, rhs: RationalNode) =>
|
||||
DollarNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[DivideBinaryOperator[Dollar, Dollar, Rational]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot Divide a ${lhs.getClass.getName} and a ${rhs.getClass.getName}",
|
||||
)
|
||||
|
||||
private final class DivideReduceOperator[A: Fractional] extends ReduceOperator[A]:
|
||||
override protected def reduce(x: A, y: A): A = Fractional[A].div(x, y)
|
||||
|
||||
@unused
|
||||
private object DivideReduceOperator:
|
||||
implicit val dollarOperator: DivideReduceOperator[Dollar] =
|
||||
DivideReduceOperator[Dollar]
|
||||
implicit val rationalOperator: DivideReduceOperator[Rational] =
|
||||
DivideReduceOperator[Rational]
|
||||
// implicit def fractionalOperator[A: Fractional]: DivideReduceOperator[A] =
|
||||
// DivideReduceOperator[A]
|
||||
|
||||
private trait DivideBinaryOperator[A, L, R] extends BinaryOperator[A, L, R]
|
||||
|
||||
@unused
|
||||
private object DivideBinaryOperator:
|
||||
implicit val intIntOperator: DivideBinaryOperator[Rational, Int, Int] =
|
||||
(lhs: Int, rhs: Int) => Rational(lhs, rhs)
|
||||
implicit val dollarDollarOperator: DivideBinaryOperator[Dollar, Dollar, Dollar] =
|
||||
(lhs: Dollar, rhs: Dollar) => lhs / rhs
|
||||
implicit val rationalRationalOperator: DivideBinaryOperator[Rational, Rational, Rational] =
|
||||
(lhs: Rational, rhs: Rational) => lhs / rhs
|
||||
implicit val intDollarOperator: DivideBinaryOperator[Dollar, Int, Dollar] =
|
||||
(lhs: Int, rhs: Dollar) => Fractional[Dollar].div(lhs, rhs)
|
||||
implicit val dollarIntOperator: DivideBinaryOperator[Dollar, Dollar, Int] =
|
||||
(lhs: Dollar, rhs: Int) => Fractional[Dollar].div(lhs, rhs)
|
||||
implicit val intRationalOperator: DivideBinaryOperator[Rational, Int, Rational] =
|
||||
(lhs: Int, rhs: Rational) => Fractional[Rational].div(lhs, rhs)
|
||||
implicit val rationalIntOperator: DivideBinaryOperator[Rational, Rational, Int] =
|
||||
(lhs: Rational, rhs: Int) => Fractional[Rational].div(lhs, rhs)
|
||||
implicit val dollarRationalOperator: DivideBinaryOperator[Dollar, Dollar, Rational] =
|
||||
(lhs: Dollar, rhs: Rational) =>
|
||||
Fractional[Dollar].div(
|
||||
Numeric[Dollar].times(lhs, rhs.denominator),
|
||||
rhs.numerator,
|
||||
)
|
||||
implicit val rationalDollarOperator: DivideBinaryOperator[Dollar, Rational, Dollar] =
|
||||
(lhs: Rational, rhs: Dollar) =>
|
||||
Fractional[Dollar].div(
|
||||
lhs.numerator,
|
||||
Numeric[Dollar].times(rhs, lhs.denominator),
|
||||
)
|
|
@ -0,0 +1,33 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.types.Dollar
|
||||
|
||||
final case class DollarNode(expr: Expression[Dollar]) extends CompNode:
|
||||
type Value = Dollar
|
||||
override def ValueClass = classOf[BigDecimal].asInstanceOf[Class[Dollar]]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[Dollar],
|
||||
): CompNode =
|
||||
DollarNode(expr)
|
||||
|
||||
object DollarNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "Dollar"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
new DollarNode(
|
||||
Expression.Writable(classOf[BigDecimal].asInstanceOf[Class[Dollar]]),
|
||||
)
|
||||
|
||||
def apply(value: Dollar): DollarNode = new DollarNode(
|
||||
Expression.Constant(Some(value)),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(Dollar(e.getOptionValue(CommonOptionConfigTraits.VALUE).get))
|
|
@ -0,0 +1,38 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.Expression
|
||||
import gov.irs.factgraph.Factual
|
||||
import gov.irs.factgraph.Path
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.types.Ein
|
||||
import gov.irs.factgraph.FactDictionary
|
||||
import gov.irs.factgraph.PathItem
|
||||
import gov.irs.factgraph.monads.Result
|
||||
|
||||
final case class EinNode(expr: Expression[Ein]) extends CompNode:
|
||||
type Value = Ein
|
||||
override def ValueClass = classOf[Ein]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[Ein],
|
||||
): CompNode =
|
||||
EinNode(expr)
|
||||
|
||||
object EinNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "EIN"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using
|
||||
Factual,
|
||||
)(using FactDictionary): CompNode =
|
||||
new EinNode(
|
||||
Expression.Writable(classOf[Ein]),
|
||||
)
|
||||
|
||||
def apply(value: Ein): EinNode = new EinNode(
|
||||
Expression.Constant(Some(value)),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(Ein(e.getOptionValue(CommonOptionConfigTraits.VALUE).get))
|
|
@ -0,0 +1,33 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.types.EmailAddress
|
||||
|
||||
final case class EmailAddressNode(expr: Expression[EmailAddress]) extends CompNode:
|
||||
type Value = EmailAddress
|
||||
override def ValueClass = classOf[EmailAddress]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[EmailAddress],
|
||||
): CompNode =
|
||||
EmailAddressNode(expr)
|
||||
|
||||
object EmailAddressNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "EmailAddress"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
new EmailAddressNode(
|
||||
Expression.Writable(classOf[EmailAddress]),
|
||||
)
|
||||
|
||||
def apply(value: EmailAddress): EmailAddressNode = new EmailAddressNode(
|
||||
Expression.Constant(Some(value)),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(EmailAddress(e.getOptionValue(CommonOptionConfigTraits.VALUE).get))
|
|
@ -0,0 +1,69 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.limits.Limit
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.limits.ContainsLimit
|
||||
import gov.irs.factgraph.definitions.fact.LimitLevel
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigElement
|
||||
import gov.irs.factgraph.limits.LimitContext
|
||||
|
||||
final case class EnumNode(
|
||||
expr: Expression[gov.irs.factgraph.types.Enum],
|
||||
enumOptionsPath: String,
|
||||
) extends CompNode:
|
||||
type Value = gov.irs.factgraph.types.Enum
|
||||
override def ValueClass = classOf[gov.irs.factgraph.types.Enum]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[gov.irs.factgraph.types.Enum],
|
||||
): CompNode =
|
||||
EnumNode(expr, enumOptionsPath)
|
||||
|
||||
object EnumNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "Enum"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val dictionary = summon[FactDictionary]
|
||||
val enumOptionsPath = e.options.find(x => x.name == "optionsPath")
|
||||
if (enumOptionsPath == None) {
|
||||
throw new IllegalArgumentException(
|
||||
s"Enum must contain ${CommonOptionConfigTraits.ENUM_OPTIONS_PATH}",
|
||||
)
|
||||
}
|
||||
new EnumNode(
|
||||
Expression.Writable(classOf[gov.irs.factgraph.types.Enum]),
|
||||
enumOptionsPath.get.value,
|
||||
)
|
||||
|
||||
def apply(value: gov.irs.factgraph.types.Enum): EnumNode = new EnumNode(
|
||||
Expression.Constant(Some(value)),
|
||||
value.enumOptionsPath,
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val dictionary = summon[FactDictionary]
|
||||
val enumOptionsPath =
|
||||
e.getOptionValue(CommonOptionConfigTraits.ENUM_OPTIONS_PATH)
|
||||
val value = e.getOptionValue(CommonOptionConfigTraits.VALUE)
|
||||
|
||||
if (value == None) {
|
||||
throw new IllegalArgumentException(
|
||||
s"Enum must contain ${CommonOptionConfigTraits.VALUE}",
|
||||
)
|
||||
}
|
||||
|
||||
if (enumOptionsPath == None) {
|
||||
throw new IllegalArgumentException(
|
||||
s"Enum must contain ${CommonOptionConfigTraits.ENUM_OPTIONS_PATH}",
|
||||
)
|
||||
}
|
||||
|
||||
this(
|
||||
gov.irs.factgraph.types.Enum
|
||||
.apply(value.get, enumOptionsPath.get),
|
||||
)
|
|
@ -0,0 +1,67 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.monads.{MaybeVector, Result, Thunk}
|
||||
import gov.irs.factgraph.operators.{AggregateOperator, BinaryOperator, UnaryOperator}
|
||||
|
||||
object EnumOptionsContains extends CompNodeFactory:
|
||||
override val Key: String = "EnumOptionsContains"
|
||||
private val enumOperator = EnumContainsOperator()
|
||||
private val multiEnumOperator = MultiEnumContainsOperator()
|
||||
def apply(node: CompNode, value: EnumNode): BooleanNode =
|
||||
node match
|
||||
case node: EnumOptionsNode =>
|
||||
BooleanNode(
|
||||
Expression.Binary(
|
||||
node.expr,
|
||||
value.expr,
|
||||
enumOperator,
|
||||
),
|
||||
)
|
||||
case node: MultiEnumNode =>
|
||||
BooleanNode(
|
||||
Expression.Binary(
|
||||
node.expr,
|
||||
value.expr,
|
||||
multiEnumOperator,
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val lhs = CompNode.getConfigChildNode(e, "Options")
|
||||
val rhs = CompNode.getConfigChildNode(e, "Value")
|
||||
val lhsIsEnumOptions = lhs.isInstanceOf[EnumOptionsNode]
|
||||
val lhsIsMultiEnum = lhs.isInstanceOf[MultiEnumNode]
|
||||
if ((!lhsIsEnumOptions && !lhsIsMultiEnum) || !rhs.isInstanceOf[EnumNode])
|
||||
throw new IllegalArgumentException(
|
||||
"Options should be EnumOptions or MultiEnum and Value should be Enum node",
|
||||
)
|
||||
if (lhsIsEnumOptions)
|
||||
this(lhs.asInstanceOf[EnumOptionsNode], rhs.asInstanceOf[EnumNode])
|
||||
else this(lhs.asInstanceOf[MultiEnumNode], rhs.asInstanceOf[EnumNode])
|
||||
|
||||
private final class EnumContainsOperator() extends BinaryOperator[Boolean, List[String], gov.irs.factgraph.types.Enum]:
|
||||
override protected def operation(
|
||||
options: List[String],
|
||||
enumm: gov.irs.factgraph.types.Enum,
|
||||
): Boolean =
|
||||
enumm.value match
|
||||
case Some(value) => options.contains(value)
|
||||
case None => true // Not having picked an option is always valid
|
||||
|
||||
private final class MultiEnumContainsOperator()
|
||||
extends BinaryOperator[
|
||||
Boolean,
|
||||
gov.irs.factgraph.types.MultiEnum,
|
||||
gov.irs.factgraph.types.Enum,
|
||||
]:
|
||||
override protected def operation(
|
||||
options: gov.irs.factgraph.types.MultiEnum,
|
||||
enumm: gov.irs.factgraph.types.Enum,
|
||||
): Boolean =
|
||||
enumm.value match
|
||||
case Some(value) => options.values.contains(value)
|
||||
case None => true // Not having picked an option is always valid
|
|
@ -0,0 +1,70 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
import gov.irs.factgraph.FactDictionary
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.operators.AggregateOperator
|
||||
import gov.irs.factgraph.monads.MaybeVector
|
||||
import gov.irs.factgraph.monads.Result
|
||||
import gov.irs.factgraph.monads.Thunk
|
||||
final case class EnumOptionsNode(expr: Expression[List[String]]) extends CompNode:
|
||||
type Value = List[String]
|
||||
// below is equivalent to `Nil.getClass().asInstanceOf[Class[List[String]]]` due to scala List implementation
|
||||
override def ValueClass =
|
||||
List[String]().getClass().asInstanceOf[Class[List[String]]]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[List[String]],
|
||||
): CompNode =
|
||||
EnumOptionsNode(expr)
|
||||
|
||||
object EnumOptionsNode extends CompNodeFactory:
|
||||
override val Key: String = "EnumOptions"
|
||||
def apply(
|
||||
options: List[(BooleanNode, StringNode)],
|
||||
)(using Factual): CompNode =
|
||||
val allowedOpts = Expression.ConditionalList(
|
||||
options.map(o => (o._1.expr, o._2.expr)),
|
||||
)
|
||||
EnumOptionsNode(allowedOpts)
|
||||
|
||||
def apply(options: List[String]): CompNode =
|
||||
EnumOptionsNode(Expression.Constant(Some(options)))
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
try {
|
||||
val conditions = for {
|
||||
c <- e.children.filter(x => (x.typeName == "EnumOption" || x.typeName == "String"))
|
||||
} yield (
|
||||
c.typeName match
|
||||
case "String" =>
|
||||
(
|
||||
BooleanNode(true),
|
||||
StringNode(
|
||||
Expression.Constant(
|
||||
c.getOptionValue(CommonOptionConfigTraits.VALUE),
|
||||
),
|
||||
),
|
||||
)
|
||||
case "EnumOption" =>
|
||||
(
|
||||
CompNode
|
||||
.getConfigChildNode(c, "Condition")
|
||||
.asInstanceOf[BooleanNode],
|
||||
CompNode.getConfigChildNode(c, "Value").asInstanceOf[StringNode],
|
||||
)
|
||||
)
|
||||
|
||||
if (conditions.isEmpty) {
|
||||
throw new IllegalArgumentException(
|
||||
s"EnumOptions must have at least one child node: $e",
|
||||
)
|
||||
}
|
||||
this(conditions.toList)
|
||||
} catch {
|
||||
case e: ClassCastException =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"Condition must be boolean: $e",
|
||||
)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.monads.{MaybeVector, Result, Thunk}
|
||||
import gov.irs.factgraph.operators.{AggregateOperator, BinaryOperator, UnaryOperator}
|
||||
|
||||
object EnumOptionsSize extends CompNodeFactory:
|
||||
override val Key: String = "EnumOptionsSize"
|
||||
private val operator = EnumOptionsSizeOperator()
|
||||
def apply(node: EnumOptionsNode): IntNode =
|
||||
IntNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
operator,
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
CompNode.getConfigChildNode(e) match
|
||||
case x: EnumOptionsNode => this(x)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"invalid child type: ${e.typeName}",
|
||||
)
|
||||
|
||||
private final class EnumOptionsSizeOperator() extends UnaryOperator[Int, List[String]]:
|
||||
override protected def operation(options: List[String]): Int =
|
||||
options.length
|
|
@ -0,0 +1,35 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.BinaryOperator
|
||||
|
||||
object Equal extends CompNodeFactory:
|
||||
override val Key: String = "Equal"
|
||||
|
||||
private val operator = EqualOperator()
|
||||
|
||||
def apply(lhs: CompNode, rhs: CompNode): BooleanNode =
|
||||
if (lhs.getClass != rhs.getClass)
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare a ${lhs.getClass.getName} and a ${rhs.getClass.getName}",
|
||||
)
|
||||
|
||||
BooleanNode(
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
operator,
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val lhs = CompNode.getConfigChildNode(e, "Left")
|
||||
val rhs = CompNode.getConfigChildNode(e, "Right")
|
||||
|
||||
this(lhs, rhs)
|
||||
|
||||
private final class EqualOperator extends BinaryOperator[Boolean, Any, Any]:
|
||||
override protected def operation(x: Any, y: Any): Boolean = x == y
|
|
@ -0,0 +1,56 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.CollectOperator
|
||||
import gov.irs.factgraph.types.{Collection, CollectionItem}
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
|
||||
object Filter extends CompNodeFactory:
|
||||
override val Key: String = "Filter"
|
||||
|
||||
private val operator = FilterOperator()
|
||||
|
||||
def apply(path: Path, cnBuilder: Factual ?=> BooleanNode)(using
|
||||
fact: Factual,
|
||||
): CompNode =
|
||||
fact(path :+ PathItem.Wildcard)(0) match
|
||||
case Result.Complete(collectionItem) =>
|
||||
CollectionNode(
|
||||
Expression
|
||||
.Collect(path, cnBuilder(using collectionItem).expr, operator),
|
||||
Some(path),
|
||||
)
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(
|
||||
s"cannot find fact at path '$path' from '${fact.path}'",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val cnBuilder: Factual ?=> BooleanNode =
|
||||
CompNode.getConfigChildNode(e) match
|
||||
case node: BooleanNode => node
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"invalid child type: $e",
|
||||
)
|
||||
|
||||
this(Path(e.getOptionValue(CommonOptionConfigTraits.PATH).get), cnBuilder)
|
||||
|
||||
private final class FilterOperator extends CollectOperator[Collection, Boolean]:
|
||||
override def apply(
|
||||
vect: MaybeVector[(CollectionItem, Thunk[Result[Boolean]])],
|
||||
): Result[Collection] =
|
||||
val (items, thunks) = vect.toVector.unzip
|
||||
val results = thunks.map(_.get)
|
||||
|
||||
val bools = results.map(_.getOrElse(false))
|
||||
val filteredIds = for {
|
||||
i <- bools.indices if bools(i)
|
||||
} yield items(i).id
|
||||
|
||||
val complete = vect.complete && results.forall(_.complete)
|
||||
|
||||
Result(Collection(filteredIds.toVector), complete)
|
|
@ -0,0 +1,49 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.CollectOperator
|
||||
import gov.irs.factgraph.types.CollectionItem
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
|
||||
object Find extends CompNodeFactory:
|
||||
override val Key: String = "Find"
|
||||
|
||||
private val operator = FindOperator()
|
||||
|
||||
def apply(path: Path, cnBuilder: Factual ?=> BooleanNode)(using
|
||||
fact: Factual,
|
||||
): CompNode =
|
||||
fact(path :+ PathItem.Wildcard)(0) match
|
||||
case Result.Complete(collectionItem) =>
|
||||
CollectionItemNode(
|
||||
Expression
|
||||
.Collect(path, cnBuilder(using collectionItem).expr, operator),
|
||||
Some(path),
|
||||
)
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(
|
||||
s"cannot find fact at path '$path' from '${fact.path}'",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val cnBuilder: Factual ?=> BooleanNode =
|
||||
CompNode.getConfigChildNode(e) match
|
||||
case node: BooleanNode => node
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"invalid child type: $e",
|
||||
)
|
||||
this(Path(e.getOptionValue(CommonOptionConfigTraits.PATH).get), cnBuilder)
|
||||
|
||||
private final class FindOperator extends CollectOperator[CollectionItem, Boolean]:
|
||||
override def apply(
|
||||
vect: MaybeVector[(CollectionItem, Thunk[Result[Boolean]])],
|
||||
): Result[CollectionItem] =
|
||||
val item = vect.toVector.collectFirst {
|
||||
case (item, thunk) if thunk.get.getOrElse(false) => item
|
||||
}
|
||||
|
||||
Result(item)
|
|
@ -0,0 +1,52 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.BinaryOperator
|
||||
import gov.irs.factgraph.types.{Collection, CollectionItem}
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
|
||||
object FirstNCollectionItems extends CompNodeFactory:
|
||||
override val Key: String = "FirstNCollectionItems"
|
||||
|
||||
private val operator = FirstNCollectionItemsOperator()
|
||||
|
||||
def apply(collection: CollectionNode, count: IntNode): CollectionNode =
|
||||
CollectionNode(
|
||||
Expression.Binary(
|
||||
collection.expr,
|
||||
count.expr,
|
||||
operator,
|
||||
),
|
||||
collection.alias,
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val collection = CompNode.getConfigChildNode(e, "Collection")
|
||||
val count = CompNode.getConfigChildNode(e, "Count")
|
||||
if (!collection.isInstanceOf[CollectionNode] || !count.isInstanceOf[IntNode])
|
||||
throw new UnsupportedOperationException(
|
||||
s"required to have a collection node and integer count node specified to use FirstNCollectionItems",
|
||||
)
|
||||
this(collection.asInstanceOf[CollectionNode], count.asInstanceOf[IntNode])
|
||||
|
||||
private final class FirstNCollectionItemsOperator extends BinaryOperator[Collection, Collection, Int]:
|
||||
override def apply(
|
||||
lhs: Result[Collection],
|
||||
rhs: Thunk[Result[Int]],
|
||||
): Result[Collection] =
|
||||
if (lhs == Result.Incomplete || rhs.get == Result.Incomplete)
|
||||
Result.Incomplete
|
||||
else
|
||||
(lhs, rhs.get) match
|
||||
case (Result(collection, collComplete), Result(count, countComplete)) =>
|
||||
Result(
|
||||
Collection(collection.items.slice(0, count)),
|
||||
collComplete && countComplete,
|
||||
)
|
||||
case _ => Result.Incomplete
|
||||
|
||||
override protected def operation(x: Collection, y: Int): Collection =
|
||||
throw new Exception("shouldn't be calling this")
|
|
@ -0,0 +1,74 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.types.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.ReduceOperator
|
||||
import gov.irs.factgraph.util.Seq.itemsHaveSameRuntimeClass
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object GreaterOf extends CompNodeFactory:
|
||||
override val Key: String = "GreaterOf"
|
||||
|
||||
def apply(nodes: Seq[CompNode]): CompNode =
|
||||
if (!itemsHaveSameRuntimeClass(nodes))
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare nodes of different classes",
|
||||
)
|
||||
|
||||
nodes.head match
|
||||
case node: IntNode =>
|
||||
IntNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[IntNode]].map(_.expr),
|
||||
summon[GreaterOfOperator[Int]],
|
||||
),
|
||||
)
|
||||
case node: DollarNode =>
|
||||
DollarNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[DollarNode]].map(_.expr),
|
||||
summon[GreaterOfOperator[Dollar]],
|
||||
),
|
||||
)
|
||||
case node: RationalNode =>
|
||||
RationalNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[RationalNode]].map(_.expr),
|
||||
summon[GreaterOfOperator[Rational]],
|
||||
),
|
||||
)
|
||||
case node: DayNode =>
|
||||
DayNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[DayNode]].map(_.expr),
|
||||
summon[GreaterOfOperator[Day]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare a ${nodes.head.getClass.getName}",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(CompNode.getConfigChildNodes(e))
|
||||
|
||||
private final class GreaterOfOperator[A: Ordering] extends ReduceOperator[A]:
|
||||
override protected def reduce(x: A, y: A): A =
|
||||
if (Ordering[A].gteq(x, y)) x else y
|
||||
|
||||
@unused
|
||||
private object GreaterOfOperator:
|
||||
implicit val intOperator: GreaterOfOperator[Int] =
|
||||
GreaterOfOperator[Int]
|
||||
implicit val dollarOperator: GreaterOfOperator[Dollar] =
|
||||
GreaterOfOperator[Dollar]
|
||||
implicit val rationalOperator: GreaterOfOperator[Rational] =
|
||||
GreaterOfOperator[Rational]
|
||||
implicit val dayOperator: GreaterOfOperator[Day] =
|
||||
GreaterOfOperator[Day]
|
||||
// implicit def numericOperator[A: Numeric]: GreaterOfOperator[A] =
|
||||
// GreaterOfOperator[A]
|
|
@ -0,0 +1,68 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.types.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.BinaryOperator
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object GreaterThan extends CompNodeFactory:
|
||||
override val Key: String = "GreaterThan"
|
||||
|
||||
def apply(lhs: CompNode, rhs: CompNode): BooleanNode =
|
||||
BooleanNode(
|
||||
(lhs, rhs).match
|
||||
case (lhs: IntNode, rhs: IntNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[GreaterThanBinaryOperator[Int]],
|
||||
)
|
||||
case (lhs: DollarNode, rhs: DollarNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[GreaterThanBinaryOperator[Dollar]],
|
||||
)
|
||||
case (lhs: RationalNode, rhs: RationalNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[GreaterThanBinaryOperator[Rational]],
|
||||
)
|
||||
case (lhs: DayNode, rhs: DayNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[GreaterThanBinaryOperator[Day]],
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare a ${lhs.getClass.getName} and a ${rhs.getClass.getName}",
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val lhs = CompNode.getConfigChildNode(e, "Left")
|
||||
val rhs = CompNode.getConfigChildNode(e, "Right")
|
||||
this(lhs, rhs)
|
||||
|
||||
private final class GreaterThanBinaryOperator[A: Ordering] extends BinaryOperator[Boolean, A, A]:
|
||||
override protected def operation(x: A, y: A): Boolean = Ordering[A].gt(x, y)
|
||||
|
||||
@unused
|
||||
private object GreaterThanBinaryOperator:
|
||||
implicit val intOperator: GreaterThanBinaryOperator[Int] =
|
||||
GreaterThanBinaryOperator[Int]
|
||||
implicit val dollarOperator: GreaterThanBinaryOperator[Dollar] =
|
||||
GreaterThanBinaryOperator[Dollar]
|
||||
implicit val rationalOperator: GreaterThanBinaryOperator[Rational] =
|
||||
GreaterThanBinaryOperator[Rational]
|
||||
implicit val dayOperator: GreaterThanBinaryOperator[Day] =
|
||||
GreaterThanBinaryOperator[Day]
|
||||
|
||||
// implicit def numericOperator[A: Numeric]: GreaterThanBinaryOperator[A] =
|
||||
// GreaterThanBinaryOperator[A]
|
|
@ -0,0 +1,69 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.types.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.BinaryOperator
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object GreaterThanOrEqual extends CompNodeFactory:
|
||||
override val Key: String = "GreaterThanOrEqual"
|
||||
|
||||
def apply(lhs: CompNode, rhs: CompNode): BooleanNode =
|
||||
BooleanNode(
|
||||
(lhs, rhs).match
|
||||
case (lhs: IntNode, rhs: IntNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[GreaterThanOrEqualBinaryOperator[Int]],
|
||||
)
|
||||
case (lhs: DollarNode, rhs: DollarNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[GreaterThanOrEqualBinaryOperator[Dollar]],
|
||||
)
|
||||
case (lhs: RationalNode, rhs: RationalNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[GreaterThanOrEqualBinaryOperator[Rational]],
|
||||
)
|
||||
case (lhs: DayNode, rhs: DayNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[GreaterThanOrEqualBinaryOperator[Day]],
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare a ${lhs.getClass.getName} and a ${rhs.getClass.getName}",
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val lhs = CompNode.getConfigChildNode(e, "Left")
|
||||
val rhs = CompNode.getConfigChildNode(e, "Right")
|
||||
this(lhs, rhs)
|
||||
|
||||
private final class GreaterThanOrEqualBinaryOperator[A: Ordering] extends BinaryOperator[Boolean, A, A]:
|
||||
override protected def operation(x: A, y: A): Boolean = Ordering[A].gteq(x, y)
|
||||
|
||||
@unused
|
||||
private object GreaterThanOrEqualBinaryOperator:
|
||||
implicit val intOperator: GreaterThanOrEqualBinaryOperator[Int] =
|
||||
GreaterThanOrEqualBinaryOperator[Int]
|
||||
implicit val dollarOperator: GreaterThanOrEqualBinaryOperator[Dollar] =
|
||||
GreaterThanOrEqualBinaryOperator[Dollar]
|
||||
implicit val rationalOperator: GreaterThanOrEqualBinaryOperator[Rational] =
|
||||
GreaterThanOrEqualBinaryOperator[Rational]
|
||||
implicit val dayOperator: GreaterThanOrEqualBinaryOperator[Day] =
|
||||
GreaterThanOrEqualBinaryOperator[Day]
|
||||
|
||||
// implicit def numericOperator[A: Numeric]
|
||||
// : GreaterThanOrEqualBinaryOperator[A] =
|
||||
// GreaterThanOrEqualBinaryOperator[A]
|
|
@ -0,0 +1,52 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.{BinaryOperator, CollectOperator}
|
||||
import gov.irs.factgraph.types.{Collection, CollectionItem}
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
object IndexOf extends CompNodeFactory:
|
||||
override val Key: String = "IndexOf"
|
||||
|
||||
private val operator = IndexOfOperator()
|
||||
def apply(collection: CollectionNode, index: IntNode): CollectionItemNode =
|
||||
CollectionItemNode(
|
||||
Expression.Binary(
|
||||
collection.expr,
|
||||
index.expr,
|
||||
operator,
|
||||
),
|
||||
collection.alias,
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val collection = CompNode.getConfigChildNode(e, "Collection")
|
||||
val index = CompNode.getConfigChildNode(e, "Index")
|
||||
if (!collection.isInstanceOf[CollectionNode] || !index.isInstanceOf[IntNode])
|
||||
throw new UnsupportedOperationException(
|
||||
s"required to have a collection node and integer index node specified to use indexof",
|
||||
)
|
||||
this(collection.asInstanceOf[CollectionNode], index.asInstanceOf[IntNode])
|
||||
|
||||
private final class IndexOfOperator extends BinaryOperator[CollectionItem, Collection, Int]:
|
||||
override def apply(
|
||||
lhs: Result[Collection],
|
||||
rhs: Thunk[Result[Int]],
|
||||
): Result[CollectionItem] =
|
||||
if (lhs == Result.Incomplete || rhs.get == Result.Incomplete) Result.Incomplete
|
||||
else
|
||||
val index = rhs.get.get
|
||||
val collection = lhs.get
|
||||
var potentialCollectionItem = collection.items.lift(index)
|
||||
potentialCollectionItem match {
|
||||
case Some(x) => Result.Complete(CollectionItem(x))
|
||||
case None => Result.Incomplete
|
||||
}
|
||||
|
||||
override protected def operation(x: Collection, y: Int): CollectionItem =
|
||||
throw new Exception("shouldn't be calling this")
|
|
@ -0,0 +1,30 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
|
||||
final case class IntNode(expr: Expression[Int]) extends CompNode:
|
||||
type Value = Int
|
||||
override def ValueClass = classOf[java.lang.Number].asInstanceOf[Class[Int]]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[Int],
|
||||
): CompNode =
|
||||
IntNode(expr)
|
||||
|
||||
object IntNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "Int"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
new IntNode(
|
||||
Expression.Writable(classOf[java.lang.Integer].asInstanceOf[Class[Int]]),
|
||||
)
|
||||
|
||||
def apply(value: Int): IntNode = new IntNode(Expression.Constant(Some(value)))
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(e.getOptionValue(CommonOptionConfigTraits.VALUE).get.toInt)
|
|
@ -0,0 +1,38 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.Expression
|
||||
import gov.irs.factgraph.Factual
|
||||
import gov.irs.factgraph.Path
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
import gov.irs.factgraph.types.IpPin
|
||||
import gov.irs.factgraph.FactDictionary
|
||||
import gov.irs.factgraph.PathItem
|
||||
import gov.irs.factgraph.monads.Result
|
||||
|
||||
final case class IpPinNode(expr: Expression[IpPin]) extends CompNode:
|
||||
type Value = IpPin
|
||||
override def ValueClass = classOf[IpPin]
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[IpPin],
|
||||
): CompNode =
|
||||
IpPinNode(expr)
|
||||
|
||||
object IpPinNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "IPPIN"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using
|
||||
Factual,
|
||||
)(using FactDictionary): CompNode =
|
||||
new IpPinNode(
|
||||
Expression.Writable(classOf[IpPin]),
|
||||
)
|
||||
|
||||
def apply(value: IpPin): IpPinNode = new IpPinNode(
|
||||
Expression.Constant(Some(value)),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(IpPin(e.getOptionValue(CommonOptionConfigTraits.VALUE).get))
|
|
@ -0,0 +1,30 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.UnaryOperator
|
||||
import gov.irs.factgraph.monads.Result
|
||||
|
||||
object IsComplete extends CompNodeFactory:
|
||||
override val Key: String = "IsComplete"
|
||||
|
||||
private val operator = IsCompleteOperator()
|
||||
|
||||
def apply(x: CompNode): BooleanNode =
|
||||
BooleanNode(
|
||||
Expression.Unary(
|
||||
x.expr,
|
||||
operator,
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(
|
||||
e: CompNodeConfigTrait,
|
||||
)(using Factual)(using FactDictionary): CompNode =
|
||||
this(CompNode.getConfigChildNode(e))
|
||||
|
||||
private final class IsCompleteOperator extends UnaryOperator[Boolean, Any]:
|
||||
override protected def operation(x: Any): Boolean = ???
|
||||
|
||||
override def apply(x: Result[Any]): Result[Boolean] =
|
||||
Result(x.complete, true)
|
|
@ -0,0 +1,30 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.{ReduceOperator, UnaryOperator}
|
||||
|
||||
object Length extends CompNodeFactory:
|
||||
override val Key: String = "Length"
|
||||
private val operator = LengthOperator()
|
||||
def apply(node: StringNode): IntNode =
|
||||
IntNode(
|
||||
Expression.Unary(
|
||||
node.expr,
|
||||
operator,
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
CompNode.getConfigChildNode(e) match
|
||||
case x: StringNode => this(x)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"invalid child type: ${e.typeName}",
|
||||
)
|
||||
|
||||
private final class LengthOperator() extends UnaryOperator[Int, String]:
|
||||
override protected def operation(x: String): Int =
|
||||
x.length
|
|
@ -0,0 +1,68 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.types.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.BinaryOperator
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object LessThan extends CompNodeFactory:
|
||||
override val Key: String = "LessThan"
|
||||
|
||||
def apply(lhs: CompNode, rhs: CompNode): BooleanNode =
|
||||
BooleanNode(
|
||||
(lhs, rhs).match
|
||||
case (lhs: IntNode, rhs: IntNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[LessThanBinaryOperator[Int]],
|
||||
)
|
||||
case (lhs: DollarNode, rhs: DollarNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[LessThanBinaryOperator[Dollar]],
|
||||
)
|
||||
case (lhs: RationalNode, rhs: RationalNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[LessThanBinaryOperator[Rational]],
|
||||
)
|
||||
case (lhs: DayNode, rhs: DayNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[LessThanBinaryOperator[Day]],
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare a ${lhs.getClass.getName} and a ${rhs.getClass.getName}",
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val lhs = CompNode.getConfigChildNode(e, "Left")
|
||||
val rhs = CompNode.getConfigChildNode(e, "Right")
|
||||
this(lhs, rhs)
|
||||
|
||||
private final class LessThanBinaryOperator[A: Ordering] extends BinaryOperator[Boolean, A, A]:
|
||||
override protected def operation(x: A, y: A): Boolean = Ordering[A].lt(x, y)
|
||||
|
||||
@unused
|
||||
private object LessThanBinaryOperator:
|
||||
implicit val intOperator: LessThanBinaryOperator[Int] =
|
||||
LessThanBinaryOperator[Int]
|
||||
implicit val dollarOperator: LessThanBinaryOperator[Dollar] =
|
||||
LessThanBinaryOperator[Dollar]
|
||||
implicit val rationalOperator: LessThanBinaryOperator[Rational] =
|
||||
LessThanBinaryOperator[Rational]
|
||||
implicit val dayOperator: LessThanBinaryOperator[Day] =
|
||||
LessThanBinaryOperator[Day]
|
||||
|
||||
// implicit def numericOperator[A: Numeric]: LessThanBinaryOperator[A] =
|
||||
// LessThanBinaryOperator[A]
|
|
@ -0,0 +1,68 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.types.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.BinaryOperator
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object LessThanOrEqual extends CompNodeFactory:
|
||||
override val Key: String = "LessThanOrEqual"
|
||||
|
||||
def apply(lhs: CompNode, rhs: CompNode): BooleanNode =
|
||||
BooleanNode(
|
||||
(lhs, rhs).match
|
||||
case (lhs: IntNode, rhs: IntNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[LessThanOrEqualBinaryOperator[Int]],
|
||||
)
|
||||
case (lhs: DollarNode, rhs: DollarNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[LessThanOrEqualBinaryOperator[Dollar]],
|
||||
)
|
||||
case (lhs: RationalNode, rhs: RationalNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[LessThanOrEqualBinaryOperator[Rational]],
|
||||
)
|
||||
case (lhs: DayNode, rhs: DayNode) =>
|
||||
Expression.Binary(
|
||||
lhs.expr,
|
||||
rhs.expr,
|
||||
summon[LessThanOrEqualBinaryOperator[Day]],
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare a ${lhs.getClass.getName} and a ${rhs.getClass.getName}",
|
||||
),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val lhs = CompNode.getConfigChildNode(e, "Left")
|
||||
val rhs = CompNode.getConfigChildNode(e, "Right")
|
||||
this(lhs, rhs)
|
||||
|
||||
private final class LessThanOrEqualBinaryOperator[A: Ordering] extends BinaryOperator[Boolean, A, A]:
|
||||
override protected def operation(x: A, y: A): Boolean = Ordering[A].lteq(x, y)
|
||||
|
||||
@unused
|
||||
private object LessThanOrEqualBinaryOperator:
|
||||
implicit val intOperator: LessThanOrEqualBinaryOperator[Int] =
|
||||
LessThanOrEqualBinaryOperator[Int]
|
||||
implicit val dollarOperator: LessThanOrEqualBinaryOperator[Dollar] =
|
||||
LessThanOrEqualBinaryOperator[Dollar]
|
||||
implicit val rationalOperator: LessThanOrEqualBinaryOperator[Rational] =
|
||||
LessThanOrEqualBinaryOperator[Rational]
|
||||
implicit val dayOperator: LessThanOrEqualBinaryOperator[Day] =
|
||||
LessThanOrEqualBinaryOperator[Day]
|
||||
|
||||
// implicit def numericOperator[A: Numeric]: LessThanOrEqualBinaryOperator[A] =
|
||||
// LessThanOrEqualBinaryOperator[A]
|
|
@ -0,0 +1,75 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.types.*
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual}
|
||||
import gov.irs.factgraph.definitions.fact.CompNodeConfigTrait
|
||||
import gov.irs.factgraph.operators.ReduceOperator
|
||||
import gov.irs.factgraph.util.Seq.itemsHaveSameRuntimeClass
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object LesserOf extends CompNodeFactory:
|
||||
override val Key: String = "LesserOf"
|
||||
|
||||
def apply(nodes: Seq[CompNode]): CompNode =
|
||||
if (!itemsHaveSameRuntimeClass(nodes))
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare nodes of different classes",
|
||||
)
|
||||
|
||||
nodes.head match
|
||||
case node: IntNode =>
|
||||
IntNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[IntNode]].map(_.expr),
|
||||
summon[LesserOfOperator[Int]],
|
||||
),
|
||||
)
|
||||
case node: DollarNode =>
|
||||
DollarNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[DollarNode]].map(_.expr),
|
||||
summon[LesserOfOperator[Dollar]],
|
||||
),
|
||||
)
|
||||
case node: RationalNode =>
|
||||
RationalNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[RationalNode]].map(_.expr),
|
||||
summon[LesserOfOperator[Rational]],
|
||||
),
|
||||
)
|
||||
case node: DayNode =>
|
||||
DayNode(
|
||||
Expression.Reduce(
|
||||
nodes.asInstanceOf[List[DayNode]].map(_.expr),
|
||||
summon[LesserOfOperator[Day]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot compare a ${nodes.head.getClass.getName}",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(CompNode.getConfigChildNodes(e))
|
||||
|
||||
private final class LesserOfOperator[A: Ordering] extends ReduceOperator[A]:
|
||||
override protected def reduce(x: A, y: A): A =
|
||||
if (Ordering[A].lteq(x, y)) x else y
|
||||
|
||||
@unused
|
||||
private object LesserOfOperator:
|
||||
implicit val intOperator: LesserOfOperator[Int] =
|
||||
LesserOfOperator[Int]
|
||||
implicit val dollarOperator: LesserOfOperator[Dollar] =
|
||||
LesserOfOperator[Dollar]
|
||||
implicit val rationalOperator: LesserOfOperator[Rational] =
|
||||
LesserOfOperator[Rational]
|
||||
implicit val dayOperator: LesserOfOperator[Day] =
|
||||
LesserOfOperator[Day]
|
||||
|
||||
// implicit def numericOperator[A: Numeric]: LesserOfOperator[A] =
|
||||
// LesserOfOperator[A]
|
|
@ -0,0 +1,87 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.AggregateOperator
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.types.*
|
||||
|
||||
import math.Ordered.orderingToOrdered
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object Maximum extends CompNodeFactory:
|
||||
override val Key: String = "Maximum"
|
||||
def apply(node: CompNode): CompNode =
|
||||
node match
|
||||
case node: IntNode =>
|
||||
IntNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[MaximumOperator[Int]],
|
||||
),
|
||||
)
|
||||
case node: DollarNode =>
|
||||
DollarNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[MaximumOperator[Dollar]],
|
||||
),
|
||||
)
|
||||
case node: RationalNode =>
|
||||
RationalNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[MaximumOperator[Rational]],
|
||||
),
|
||||
)
|
||||
case node: DayNode =>
|
||||
DayNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[MaximumOperator[Day]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot execute minimum on a ${node.getClass.getName}",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(CompNode.getConfigChildNode(e))
|
||||
|
||||
private final class MaximumOperator[A: Ordering] extends AggregateOperator[A, A]:
|
||||
override def apply(vect: MaybeVector[Thunk[Result[A]]]): Result[A] =
|
||||
vect.toList match
|
||||
case Nil =>
|
||||
Result.Incomplete
|
||||
case thunk :: Nil =>
|
||||
thunk.get
|
||||
case thunk :: thunks =>
|
||||
accumulator(thunks, thunk.get)
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def accumulator(
|
||||
thunks: List[Thunk[Result[A]]],
|
||||
a: Result[A],
|
||||
): Result[A] = thunks match
|
||||
case thunk :: thunks =>
|
||||
thunk.get match
|
||||
case Result(value, complete) =>
|
||||
val max = if Ordering[A].gt(value, a.get) then value else a.get
|
||||
accumulator(
|
||||
thunks,
|
||||
Result(max, complete && a.complete),
|
||||
)
|
||||
case _ => Result.Incomplete
|
||||
case Nil => a
|
||||
|
||||
@unused
|
||||
private object MaximumOperator:
|
||||
implicit val intOperator: MaximumOperator[Int] = MaximumOperator[Int]
|
||||
implicit val dollarOperator: MaximumOperator[Dollar] = MaximumOperator[Dollar]
|
||||
implicit val rationalOperator: MaximumOperator[Rational] =
|
||||
MaximumOperator[Rational]
|
||||
implicit val dayOperator: MaximumOperator[Day] = MaximumOperator[Day]
|
|
@ -0,0 +1,92 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait}
|
||||
import gov.irs.factgraph.operators.AggregateOperator
|
||||
import gov.irs.factgraph.monads.*
|
||||
import gov.irs.factgraph.types.*
|
||||
|
||||
import math.Ordered.orderingToOrdered
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path, PathItem}
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object Minimum extends CompNodeFactory:
|
||||
override val Key: String = "Minimum"
|
||||
def apply(node: CompNode): CompNode =
|
||||
node match
|
||||
case node: IntNode =>
|
||||
IntNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[MinimumOperator[Int]],
|
||||
),
|
||||
)
|
||||
case node: DollarNode =>
|
||||
DollarNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[MinimumOperator[Dollar]],
|
||||
),
|
||||
)
|
||||
case node: RationalNode =>
|
||||
RationalNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[MinimumOperator[Rational]],
|
||||
),
|
||||
)
|
||||
case node: DayNode =>
|
||||
DayNode(
|
||||
Expression.Aggregate(
|
||||
node.expr,
|
||||
summon[MinimumOperator[Day]],
|
||||
),
|
||||
)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"cannot execute minimum on a ${node.getClass.getName}",
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
this(CompNode.getConfigChildNode(e))
|
||||
|
||||
private final class MinimumOperator[A: Ordering] extends AggregateOperator[A, A]:
|
||||
override def apply(vect: MaybeVector[Thunk[Result[A]]]): Result[A] =
|
||||
vect.toList match
|
||||
case Nil =>
|
||||
Result.Incomplete
|
||||
case thunk :: Nil =>
|
||||
thunk.get
|
||||
case thunk :: thunks =>
|
||||
accumulator(thunks, thunk.get)
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def accumulator(
|
||||
thunks: List[Thunk[Result[A]]],
|
||||
a: Result[A],
|
||||
): Result[A] = thunks match
|
||||
case thunk :: thunks =>
|
||||
thunk.get match
|
||||
case Result(value, complete) =>
|
||||
val min = if Ordering[A].lt(value, a.get) then value else a.get
|
||||
accumulator(
|
||||
thunks,
|
||||
Result(min, complete && a.complete),
|
||||
)
|
||||
case _ => Result.Incomplete
|
||||
case Nil => a
|
||||
|
||||
//3/23/2023
|
||||
// you might be thinking to yourself...
|
||||
// "it sure is strange that he has this unused code here"
|
||||
// Well... this gives a hint to the compiler and allows the
|
||||
// accumulator operators to build.
|
||||
@unused
|
||||
private object MinimumOperator:
|
||||
implicit val intOperator: MinimumOperator[Int] = MinimumOperator[Int]
|
||||
implicit val dollarOperator: MinimumOperator[Dollar] = MinimumOperator[Dollar]
|
||||
implicit val rationalOperator: MinimumOperator[Rational] =
|
||||
MinimumOperator[Rational]
|
||||
implicit val dayOperator: MinimumOperator[Day] = MinimumOperator[Day]
|
|
@ -0,0 +1,63 @@
|
|||
package gov.irs.factgraph.compnodes
|
||||
|
||||
import gov.irs.factgraph.{Expression, FactDictionary, Factual, Path}
|
||||
import gov.irs.factgraph.definitions.fact.{CommonOptionConfigTraits, CompNodeConfigTrait, WritableConfigTrait}
|
||||
|
||||
final case class MultiEnumNode(
|
||||
expr: Expression[gov.irs.factgraph.types.MultiEnum],
|
||||
enumOptionsPath: Path,
|
||||
) extends CompNode:
|
||||
type Value = gov.irs.factgraph.types.MultiEnum
|
||||
override def ValueClass = classOf[gov.irs.factgraph.types.MultiEnum];
|
||||
|
||||
override private[compnodes] def fromExpression(
|
||||
expr: Expression[gov.irs.factgraph.types.MultiEnum],
|
||||
): CompNode =
|
||||
MultiEnumNode(expr, enumOptionsPath)
|
||||
|
||||
object MultiEnumNode extends CompNodeFactory with WritableNodeFactory:
|
||||
override val Key: String = "MultiEnum"
|
||||
|
||||
override def fromWritableConfig(e: WritableConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val enumOptionsPath = e.options.find(x => x.name == "optionsPath")
|
||||
if (enumOptionsPath == None) {
|
||||
throw new IllegalArgumentException(
|
||||
s"MultiEnum must contain ${CommonOptionConfigTraits.ENUM_OPTIONS_PATH}",
|
||||
)
|
||||
}
|
||||
new MultiEnumNode(
|
||||
Expression.Writable(classOf[gov.irs.factgraph.types.MultiEnum]),
|
||||
Path(enumOptionsPath.get.value),
|
||||
)
|
||||
|
||||
def apply(value: gov.irs.factgraph.types.MultiEnum): MultiEnumNode =
|
||||
new MultiEnumNode(
|
||||
Expression.Constant(Some(value)),
|
||||
Path(value.enumOptionsPath),
|
||||
)
|
||||
|
||||
override def fromDerivedConfig(e: CompNodeConfigTrait)(using Factual)(using
|
||||
FactDictionary,
|
||||
): CompNode =
|
||||
val dictionary = summon[FactDictionary]
|
||||
val enumOptionsPath =
|
||||
e.getOptionValue(CommonOptionConfigTraits.ENUM_OPTIONS_PATH)
|
||||
val values = e
|
||||
.getOptionValue(CommonOptionConfigTraits.VALUE)
|
||||
.get
|
||||
.split(",")
|
||||
.map(_.trim)
|
||||
.toSet
|
||||
|
||||
if (enumOptionsPath == None) {
|
||||
throw new IllegalArgumentException(
|
||||
s"MultiEnum must contain ${CommonOptionConfigTraits.ENUM_OPTIONS_PATH}",
|
||||
)
|
||||
}
|
||||
|
||||
this(
|
||||
gov.irs.factgraph.types.MultiEnum
|
||||
.apply(values, enumOptionsPath.get),
|
||||
)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue