├── .gitignore ├── ChangeLog ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── Screenshot-TreeLog_UI_v0.1.png ├── build.sbt ├── index.html ├── project └── plugins.sbt └── src ├── main └── scala │ ├── com │ └── oranda │ │ └── treelogui │ │ ├── LogTreeItem.scala │ │ ├── LogTreeItemWithoutAnnotations.scala │ │ ├── LogTreePerson.scala │ │ ├── LogTreeQuadratic.scala │ │ ├── TreeLogUIExamples.scala │ │ └── UIExample.scala │ ├── demo │ └── components │ │ └── CodeHighlight.scala │ └── treelog │ ├── LogTreeLabel.scala │ ├── LogTreeSyntax.scala │ ├── LogTreeSyntaxWithoutAnnotations.scala │ ├── QuadraticRootsExample.scala │ ├── SerializableTree.scala │ └── package.scala └── test └── scala └── com └── oranda └── treelogui ├── LogTreeItemSpec.scala ├── LogTreeItemWithoutAnnotationsSpec.scala └── package.scala /.gitignore: -------------------------------------------------------------------------------- 1 | # Java class files 2 | *.class 3 | 4 | # SBT generated files 5 | target/ 6 | project/target/ 7 | project/project/target 8 | 9 | # IntelliJ files 10 | *.iml 11 | 12 | # npm 13 | npm-debug.log -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | == 0.1 June 21 2016 2 | 3 | * Initial commit with two examples of TreeLog logs in ReactTreeView format: 4 | a success case, and an annotated case. -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 lancewalton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TreeLog-ScalaJS 2 | =============== 3 | 4 | This project brings together two other projects: the excellent 5 | [TreeLog](https://github.com/lancewalton/treelog) from Lance and Channing Walton, 6 | and the pretty ReactTreeView component from Chandra Sekhar Kode in the 7 | [scalajs-react-components](https://github.com/chandu0101/scalajs-react-components) project. 8 | 9 | **TreeLog** is a way of displaying a program's log in a tree structure instead of the hopelessly 10 | verbose sequential format we are usually cursed with. It is based on the Scalaz `Writer` monad. 11 | Although it is oriented towards algorithms with pure functions (like mathematical computations), 12 | it can annotate tree nodes with messages for side-effects. Read more about it 13 | [at typelevel](http://typelevel.org/blog/2013/10/18/treelog.html). 14 | 15 | **ReactTreeView** is the UI part: this component supports expanding nodes, highlighting, and other 16 | nice features. It existed first as a [normal ReactJS component](https://github.com/chenglou/react-treeview), 17 | then was [adapted into ScalaJS]( 18 | https://github.com/chandu0101/scalajs-react-components/blob/master/core/src/main/scala/chandu0101/scalajs/react/components/ReactTreeView.scala), 19 | meaning you can write your tree in Scala and it will be 20 | converted automatically into JavaScript. See the screenshot lower down to see what it looks like. 21 | 22 | Implementation 23 | ============== 24 | 25 | So, two tree projects: a match made in heaven? All TreeLog-ScalaJS really needs to do 26 | is transform a `scalaz.Tree` into a UI `TreeItem`. The main code is in `com.oranda.treelogui`. 27 | There is some code here and in `demo.components` that is copied from the 28 | scalajs-react-components demo. The code in the `treelog` package is copied straight from 29 | the TreeLog project. 30 | 31 | Running 32 | ======= 33 | 34 | 1. Clone this project (or download and unzip). 35 | 2. In the project's root, from the command-line run `sbt fastOptJS`. This will do all the Scala compilation and 36 | ScalaJS transpilation. 37 | 3. In your browser navigate to the project's index.html on the filesystem. (There is no need 38 | to run a server.) You should see: 39 | 40 | ![](https://github.com/oranda/treelog-scalajs/raw/master/Screenshot-TreeLog_UI_v0.1.png) 41 | 42 | Running the tests 43 | ================= 44 | 45 | 1. Download [PhantomJS](http://phantomjs.org/download.html). This can run JavaScript outside a browser. 46 | 2. Make sure the `phantomjs` executable is in your `PATH` environment variable. 47 | 3. In this project's root, from the command-line run `sbt test`. 48 | 49 | License 50 | ======= 51 | 52 | Licensed under 53 | 54 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 55 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 56 | -------------------------------------------------------------------------------- /Screenshot-TreeLog_UI_v0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oranda/treelog-scalajs/fc35d4ed3a91a49e3b915b84fae53dcd7bb1ff4c/Screenshot-TreeLog_UI_v0.1.png -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | 4 | name := "treelog-scalajs" 5 | 6 | version := "0.1" 7 | 8 | enablePlugins(ScalaJSPlugin) 9 | 10 | scalaVersion := "2.11.8" 11 | 12 | scalacOptions ++= Seq("-unchecked", "-deprecation") 13 | 14 | resolvers ++= Seq( 15 | Resolver.sonatypeRepo("releases"), 16 | Resolver.sonatypeRepo("snapshots") 17 | ) 18 | 19 | libraryDependencies ++= Seq( 20 | "com.github.japgolly.fork.scalaz" %%% "scalaz-core" % "7.2.0", 21 | "com.github.japgolly.scalajs-react" %% "core_sjs0.6" % "0.8.3", 22 | "com.github.japgolly.scalajs-react" %% "extra_sjs0.6" % "0.8.3", 23 | "com.github.chandu0101.scalajs-react-components" %% "core_sjs0.6" % "0.4.1", 24 | "org.scalatest" %%% "scalatest" % "3.0.0-M15" % "test" 25 | ) 26 | 27 | scalaJSUseRhino in Global := false 28 | 29 | // React JS itself (Note the filenames, adjust as needed, eg. to remove addons.) 30 | jsDependencies ++= Seq( 31 | "org.webjars.bower" % "react" % "15.0.2" 32 | / "react-with-addons.js" 33 | minified "react-with-addons.min.js" 34 | commonJSName "React", 35 | "org.webjars.bower" % "react" % "15.0.2" 36 | / "react-dom.js" 37 | minified "react-dom.min.js" 38 | dependsOn "react-with-addons.js" 39 | commonJSName "ReactDOM", 40 | "org.webjars.bower" % "react" % "15.0.2" 41 | / "react-dom-server.js" 42 | minified "react-dom-server.min.js" 43 | dependsOn "react-dom.js" 44 | commonJSName "ReactDOMServer") 45 | 46 | // This causes tests to be run with the headless PhantomJS instead of Node 47 | jsDependencies += RuntimeDOM % "test" 48 | 49 | 50 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | scalajs-react components demo 6 | 26 | 27 | 28 | 29 |
30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.9") -------------------------------------------------------------------------------- /src/main/scala/com/oranda/treelogui/LogTreeItem.scala: -------------------------------------------------------------------------------- 1 | package com.oranda.treelogui 2 | 3 | import chandu0101.scalajs.react.components.TreeItem 4 | import treelog.{LogTreeLabel, LogTreeSyntax} 5 | 6 | import scalaz.Show 7 | 8 | /** 9 | * Provides a way of building a [[chandu0101.scalajs.react.components.TreeItem TreeItem]] from a 10 | * [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]] 11 | * (declared in [[treelog.LogTreeSyntax LogTreeSyntax]]). 12 | * 13 | * Classes that extend this trait should override `result` to form the `DescribedComputation`, 14 | * and `annotationShow` to specify what the annotation for a node should display. 15 | */ 16 | trait LogTreeItem[Annotation, Result] extends LogTreeSyntax[Annotation] { 17 | 18 | /** 19 | * Forms the `DescribedComputation` used by this `LogTreeItem`. 20 | */ 21 | protected def result: DescribedComputation[Result] 22 | 23 | /** 24 | * Specifies what the annotation for a node should display for this `LogTreeItem`. 25 | */ 26 | protected def annotationShow: Show[Annotation] 27 | 28 | /** 29 | * Builds the UI `TreeItem` using the `DescribedComputation` and annotation messages specified 30 | * in the trait implementation. 31 | */ 32 | final def treeItem: TreeItem = { 33 | 34 | def showSuccess(success: Boolean, s: String) = if (success) s else "Failed: " + s 35 | 36 | def showDescription(label: LogTreeLabel[Annotation]) = 37 | label.fold(_.description, _ ⇒ "No Description") 38 | 39 | def showAnnotations( 40 | annotations: Set[Annotation], 41 | line: String, 42 | annotationShow: Show[Annotation]) = 43 | if (annotations.isEmpty) line 44 | else line + " - [" + annotations.map(annotationShow.show).mkString(", ") + "]" 45 | 46 | def buildTreeItem(t: LogTree): TreeItem = { 47 | val label = t.rootLabel 48 | val description: String = showDescription(label) 49 | val subtrees: Seq[LogTree] = t.subForest 50 | val reactSubtrees = subtrees.map(buildTreeItem) 51 | val line = showAnnotations(label.annotations, showSuccess(label.success(), description), annotationShow) 52 | 53 | TreeItem(line, reactSubtrees:_*) 54 | } 55 | 56 | buildTreeItem(result.run.written) 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/scala/com/oranda/treelogui/LogTreeItemWithoutAnnotations.scala: -------------------------------------------------------------------------------- 1 | package com.oranda.treelogui 2 | 3 | import scalaz.Show 4 | 5 | /** 6 | * A [[com.oranda.treelogui.LogTreeItem LogTreeItem]] without annotations. 7 | * 8 | * Classes that extend this trait only need to override `result` to form 9 | * the `DescribedComputation`. 10 | */ 11 | trait LogTreeItemWithoutAnnotations[Result] extends LogTreeItem[Nothing, Result] { 12 | override def annotationShow = new Show[Nothing] { 13 | override def shows(n: Nothing): String = "" 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/scala/com/oranda/treelogui/LogTreePerson.scala: -------------------------------------------------------------------------------- 1 | package com.oranda.treelogui 2 | 3 | import java.util.UUID 4 | import scalaz.Show 5 | 6 | case class PersonKey(uuid: UUID = UUID.randomUUID()) 7 | case class Person(key: PersonKey, name: String) 8 | 9 | /** 10 | * An example of a [[com.oranda.treelogui.LogTreeItem LogTreeItem]] with annotations. 11 | * It greets two people. Each message is annotated with the UUID of the person greeted. 12 | */ 13 | object LogTreePerson extends LogTreeItem[PersonKey, List[String]] { 14 | // The '~~' operator annotates the node on the left with the object on the right 15 | private def greet(person: Person) = 16 | s"Hello, ${person.name}" ~> s"Said hello to ${person.name}" ~~ person.key 17 | 18 | private def peopleToGreet() = Person(PersonKey(), "Lance") :: Person(PersonKey(), "Channing") :: Nil 19 | 20 | protected def annotationShow = new Show[PersonKey] { 21 | override def shows(k: PersonKey) = k.uuid.toString 22 | } 23 | 24 | import scalaz.Scalaz._ 25 | override def result: DescribedComputation[List[String]] = 26 | peopleToGreet() ~>* ("Greeting everybody", greet) 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/com/oranda/treelogui/LogTreeQuadratic.scala: -------------------------------------------------------------------------------- 1 | package com.oranda.treelogui 2 | 3 | import treelog.QuadraticRootsExample._ 4 | 5 | /** 6 | * A [[com.oranda.treelogui.LogTreeItem LogTreeItem]] that forms its `DescribedComputation` 7 | * using the algorithm in [[treelog.QuadraticRootsExample QuadraticRootsExample]]. 8 | */ 9 | object LogTreeQuadratic extends LogTreeItemWithoutAnnotations[Double] { 10 | override def result: DescribedComputation[Double] = root(Parameters(2, 5, 3)) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/com/oranda/treelogui/TreeLogUIExamples.scala: -------------------------------------------------------------------------------- 1 | package com.oranda.treelogui 2 | 3 | import chandu0101.scalajs.react.components.ReactTreeView 4 | 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^.{^, _} 7 | import org.scalajs.dom 8 | import org.scalajs.dom.document 9 | 10 | import scala.scalajs.js.annotation.JSExport 11 | 12 | /** 13 | * The ScalaJS layout for a page of [[com.oranda.treelogui.LogTreeItem LogTreeItem]] examples, 14 | * using a [[com.oranda.treelogui.UIExample UIExample]] for each one. 15 | */ 16 | @JSExport 17 | object TreeLogUIExamples { 18 | 19 | object Style { 20 | def pageBody = Seq(^.marginLeft := "40px") 21 | def pageHeader = Seq(^.paddingBottom := "40px") 22 | def treeViewDemo = Seq(^.display := "flex") 23 | 24 | def selectedContent = Seq(^.alignSelf := "center", ^.margin := "0 40px") 25 | } 26 | 27 | case class State(content: String = "") 28 | 29 | class Backend(t: BackendScope[_, _]) { 30 | 31 | val quadraticTreeSidebarId = "quadraticTreeSidebar" 32 | val annotatedTreeSidebarId = "annotatedTreeSidebar" 33 | 34 | def onItemSelectQuadratic(item: String, parent: String, depth: Int): Callback = 35 | onItemSelect(item, parent, depth, quadraticTreeSidebarId) 36 | 37 | def onItemSelectAnnotated(item: String, parent: String, depth: Int): Callback = 38 | onItemSelect(item, parent, depth, annotatedTreeSidebarId) 39 | 40 | def onItemSelect(item: String, parent: String, depth: Int, elementId: String): Callback = { 41 | val content = 42 | s"""Selected Item: $item
43 | |Its Parent : $parent
44 | |Its depth: $depth
45 | """.stripMargin 46 | Callback(dom.document.getElementById(elementId).innerHTML = content) 47 | } 48 | 49 | def render = { 50 | <.div(Style.pageBody)( 51 | <.h2(Style.pageHeader)("TreeLog UI using ReactTreeView"), 52 | UIExample("Quadratic Roots Tree", "The log tree of calculating the roots of 2x² + 5x + 3.")( 53 | <.div(Style.treeViewDemo)( 54 | ReactTreeView( 55 | root = LogTreeQuadratic.treeItem, 56 | openByDefault = true, 57 | onItemSelect = onItemSelectQuadratic _, 58 | showSearchBox = true 59 | ), 60 | <.strong(^.id := quadraticTreeSidebarId, Style.selectedContent) 61 | ) 62 | ), 63 | UIExample("Annotated Tree", "The log tree of sending greetings to two people. " + 64 | "Each log message is annotated with the UUID of the person greeted.")( 65 | <.div(Style.treeViewDemo)( 66 | ReactTreeView( 67 | root = LogTreePerson.treeItem, 68 | openByDefault = true, 69 | onItemSelect = onItemSelectAnnotated _, 70 | showSearchBox = true 71 | ), 72 | <.strong(^.id := annotatedTreeSidebarId, Style.selectedContent) 73 | ) 74 | ) 75 | ) 76 | } 77 | } 78 | 79 | val component = ReactComponentB[Unit]("TreeLogUIExamples") 80 | .initialState(State()) 81 | .renderBackend[Backend] 82 | .buildU 83 | 84 | def apply() = component() 85 | 86 | @JSExport 87 | def main(): Unit = ReactDOM.render(TreeLogUIExamples(), document.getElementById("container")) 88 | } -------------------------------------------------------------------------------- /src/main/scala/com/oranda/treelogui/UIExample.scala: -------------------------------------------------------------------------------- 1 | package com.oranda.treelogui 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | 6 | import scala.scalajs.js 7 | 8 | /** 9 | * The ScalaJS UI layout for a single `LogTreeItem` example, including the tree 10 | * itself, a title, and a description. 11 | */ 12 | object UIExample { 13 | 14 | object Style { 15 | val pageBodyContent = Seq(^.borderRadius := "2px", 16 | ^.boxShadow := "0 1px 4px rgba(223, 228, 228, 0.79)", 17 | ^.maxWidth := "1024px") 18 | 19 | val contentDemo = Seq(^.paddingBottom := "40px") 20 | 21 | val title = Seq(^.paddingBottom := "5px") 22 | 23 | val description = Seq(^.paddingBottom := "10px") 24 | } 25 | 26 | case class Backend($: BackendScope[Props, _]){ 27 | def render(P: Props, C: PropsChildren) = { 28 | <.div( 29 | P.title.nonEmpty ?= <.h3(P.title, Style.title), 30 | P.description.nonEmpty ?= <.div(P.description, Style.description), 31 | <.div(Style.pageBodyContent)( 32 | <.div(Style.contentDemo, ^.key := "dan")( 33 | C 34 | ) 35 | ) 36 | ) 37 | } 38 | } 39 | 40 | val component = ReactComponentB[Props]("treelogexample") 41 | .renderBackend[Backend] 42 | .build 43 | 44 | case class Props(title: String, description: String) 45 | 46 | def apply( 47 | title: String, 48 | description: String, 49 | ref: js.UndefOr[String] = "", 50 | key: js.Any = {}) 51 | (children: ReactNode*) = 52 | component.set(key, ref)(Props(title, description), if (children.size == 1) children.head else children) 53 | } -------------------------------------------------------------------------------- /src/main/scala/demo/components/CodeHighlight.scala: -------------------------------------------------------------------------------- 1 | package demo.components 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | import org.scalajs.dom 6 | import org.scalajs.dom.ext.PimpedNodeList 7 | 8 | object CodeHighlight { 9 | 10 | val component = ReactComponentB[String]("CodeHighLighter") 11 | .render_P(P => <.code(^.`class` := "scala", ^.padding := "20px", P)) 12 | .configure(installSyntaxHighlighting) 13 | .build 14 | 15 | def installSyntaxHighlighting[P, S, B, N <: TopNode] = 16 | (_: ReactComponentB[P, S, B, N]) 17 | .componentDidMount(_ => applySyntaxHighlight) 18 | .componentDidUpdate(_ => applySyntaxHighlight) 19 | 20 | def applySyntaxHighlight = Callback { 21 | import scala.scalajs.js.Dynamic.{global => g} 22 | val nodeList = dom.document.querySelectorAll("code").toArray 23 | nodeList.foreach(n => g.hljs.highlightBlock(n)) 24 | } 25 | 26 | def apply(code: String) = component(code) 27 | } -------------------------------------------------------------------------------- /src/main/scala/treelog/LogTreeLabel.scala: -------------------------------------------------------------------------------- 1 | package treelog 2 | 3 | import scalaz.Equal 4 | 5 | sealed trait LogTreeLabel[Annotation] extends Product with Serializable { 6 | def success(): Boolean 7 | def fold[T](f: DescribedLogTreeLabel[Annotation] ⇒ T, g: UndescribedLogTreeLabel[Annotation] ⇒ T): T 8 | def annotations: Set[Annotation] 9 | } 10 | 11 | final case class DescribedLogTreeLabel[Annotation](description: String, success: Boolean, annotations: Set[Annotation] = Set[Annotation]()) extends LogTreeLabel[Annotation] { 12 | def fold[T](f: DescribedLogTreeLabel[Annotation] ⇒ T, g: UndescribedLogTreeLabel[Annotation] ⇒ T) = f(this) 13 | } 14 | 15 | final case class UndescribedLogTreeLabel[Annotation](success: Boolean, annotations: Set[Annotation] = Set[Annotation]()) extends LogTreeLabel[Annotation] { 16 | def fold[T](f: DescribedLogTreeLabel[Annotation] ⇒ T, g: UndescribedLogTreeLabel[Annotation] ⇒ T) = g(this) 17 | } 18 | 19 | object LogTreeLabel { 20 | implicit def LogTreeLabelEqual[A]: Equal[LogTreeLabel[A]] = new Equal[LogTreeLabel[A]] { 21 | def equal(a1: LogTreeLabel[A], a2: LogTreeLabel[A]): Boolean = a1 == a2 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/treelog/LogTreeSyntax.scala: -------------------------------------------------------------------------------- 1 | package treelog 2 | 3 | import scalaz.Tree.{Leaf, Node} 4 | import scalaz._ 5 | import scalaz.syntax.traverse._ 6 | 7 | /** 8 | * See the [[treelog]] package documentation for a brief introduction to treelog and also, 9 | * [[https://github.com/lancewalton/treelog#using-treelog---examples examples on GitHub]] to get started. 10 | * 11 | * This trait provides syntax for manipulating `DescribedComputations`. Either: 12 | * 16 | */ 17 | trait LogTreeSyntax[Annotation] { 18 | type LogTree = Tree[LogTreeLabel[Annotation]] 19 | type LogTreeWriter[V] = Writer[LogTree, V] 20 | type DescribedComputation[V] = EitherT[LogTreeWriter, String, V] 21 | 22 | private val NilTree: LogTree = Leaf(UndescribedLogTreeLabel(true)) 23 | 24 | implicit val logTreeMonoid = new Monoid[LogTree] { 25 | 26 | val zero = NilTree 27 | 28 | def append(augend: LogTree, addend: ⇒ LogTree): LogTree = 29 | (augend, addend) match { 30 | case (NilTree, r) ⇒ r 31 | 32 | case (l, NilTree) ⇒ l 33 | 34 | case (Node(leftLabel: UndescribedLogTreeLabel[Annotation], leftChildren), Node(rightLabel: UndescribedLogTreeLabel[Annotation], rightChildren)) ⇒ 35 | Node(UndescribedLogTreeLabel(leftLabel.success && rightLabel.success, leftLabel.annotations ++ rightLabel.annotations), leftChildren ++ rightChildren) 36 | 37 | case (Node(leftLabel: UndescribedLogTreeLabel[Annotation], leftChildren), rightNode @ Node(rightLabel, rightChildren)) ⇒ 38 | Node(UndescribedLogTreeLabel(leftLabel.success && rightLabel.success, leftLabel.annotations), leftChildren :+ rightNode) 39 | 40 | case (leftNode @ Node(leftLabel, leftChildren), Node(rightLabel: UndescribedLogTreeLabel[Annotation], rightChildren)) ⇒ 41 | Node(UndescribedLogTreeLabel(leftLabel.success && rightLabel.success, rightLabel.annotations), leftNode #:: rightChildren) 42 | 43 | case (leftNode: Tree[LogTreeLabel[Annotation]], rightNode: Tree[LogTreeLabel[Annotation]]) ⇒ 44 | Node(UndescribedLogTreeLabel(leftNode.rootLabel.success && rightNode.rootLabel.success), Stream(leftNode, rightNode)) 45 | } 46 | } 47 | 48 | private val eitherWriter = EitherT.monadListen[LogTreeWriter, LogTree, String] 49 | 50 | private def failure[V](description: String, tree: LogTree): DescribedComputation[V] = 51 | for { 52 | _ ← eitherWriter.tell(tree) 53 | err ← eitherWriter.left[V](description) 54 | } yield err 55 | 56 | private def success[V](value: V, tree: LogTree): DescribedComputation[V] = 57 | for { 58 | _ ← eitherWriter.tell(tree) 59 | res ← eitherWriter.right[V](value) 60 | } yield res 61 | 62 | def failureLog[V](dc: DescribedComputation[V]): DescribedComputation[V] = { 63 | val logTree = dc.run.written match { 64 | case Node(UndescribedLogTreeLabel(s, a), c) ⇒ Node(UndescribedLogTreeLabel(false, a), c) 65 | case Node(DescribedLogTreeLabel(d, s, a), c) ⇒ Node(DescribedLogTreeLabel(d, false, a), c) 66 | } 67 | dc.run.value match { 68 | case -\/(des) ⇒ failure(des, logTree) 69 | case \/-(a) ⇒ success(a, logTree) 70 | } 71 | } 72 | 73 | /** 74 | * Create a [[treelog.LogTreeSyntax.DescribedComputation]] representing a failure using the given `description` for both the log tree label and as 75 | * the content of the `value`, which will be a [[scalaz.-\/]]. 76 | */ 77 | def failure[V](description: String): DescribedComputation[V] = failure(description, Leaf(DescribedLogTreeLabel(description, false))) 78 | 79 | /** 80 | * Create a [[treelog.LogTreeSyntax.DescribedComputation]] representing a success with the given `value` (lifted into a [[scalaz.\/-]]) and the given 81 | * `description` in the log tree. 82 | */ 83 | def success[V](value: V, description: String): DescribedComputation[V] = 84 | for { 85 | _ ← eitherWriter.tell(Leaf(DescribedLogTreeLabel(description, true, Set[Annotation]()))) 86 | res ← eitherWriter.right[V](value) 87 | } yield res 88 | 89 | /** 90 | * Create a [[treelog.LogTreeSyntax.DescribedComputation]] representing a success with the given `value` (lifted into a [[scalaz.\/-]]) and no 91 | * description. 92 | */ 93 | def success[V](value: V): DescribedComputation[V] = eitherWriter.right(value) 94 | 95 | /** 96 | * Syntax for lifting values into `DescribedComputations` and creating leaf nodes in the log tree. 97 | */ 98 | 99 | implicit class LeafSyntax[V](value: V) { 100 | 101 | /** 102 | * Create a ''success'' [[treelog.LogTreeSyntax.DescribedComputation]] with `\/-(value)` as the value and 103 | * a success [[treelog.LogTreeLabel TreeNode]] with the given `description`. 104 | * 105 | * {{{ 106 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 107 | * import scalaz.syntax.show._ 108 | * 109 | * val leaf = 1 logSuccess "One" 110 | * println(result.run.value) 111 | * // Will print: \/-(1) - note that the 'right' means ''success'' 112 | * 113 | * println(result.run.written.shows) 114 | * // Will print: 115 | * // One 116 | * }}} 117 | */ 118 | def logSuccess(description: String): DescribedComputation[V] = success(value, description) 119 | 120 | /** 121 | * Sugar for [[treelog.LogTreeSyntax.LeafSyntax.logSuccess(String) logSuccess]] 122 | */ 123 | def ~>(description: String): DescribedComputation[V] = logSuccess(description) 124 | 125 | /** 126 | * Create a ''success'' [[treelog.LogTreeSyntax.DescribedComputation]] with `\/-(value)` as the value and 127 | * a success [[treelog.LogTreeSyntax.DescribedComputation]] using the given `description` function to generate 128 | * a description for the tree node's label. 129 | * 130 | * {{{ 131 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 132 | * import scalaz.syntax.show._ 133 | * 134 | * val leaf = 1 logSuccess (x ⇒ s"One: $x") 135 | * println(result.run.value) 136 | * // Will print: \/-(1) - note that the 'right' means ''success'' 137 | * 138 | * println(result.run.written.shows) 139 | * // Will print: 140 | * // One: 1 141 | * }}} 142 | */ 143 | def logSuccess(description: V ⇒ String): DescribedComputation[V] = ~>(description(value)) 144 | 145 | /** 146 | * Sugar for [[treelog.LogTreeSyntax.LeafSyntax.logSuccess(V ⇒ String) logSuccess]] 147 | */ 148 | def ~>(description: V ⇒ String): DescribedComputation[V] = logSuccess(description(value)) 149 | 150 | /** 151 | * Create a ''failure'' [[treelog.LogTreeSyntax.DescribedComputation]] with `-\/(description)` as the value and 152 | * a failure [[treelog.LogTreeSyntax.DescribedComputation]] with the given `description`. 153 | * 154 | * {{{ 155 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 156 | * import scalaz.syntax.show._ 157 | * 158 | * val leaf = 1 ~>! "One" 159 | * println(result.run.value) 160 | * // Will print: -\/("One") - note that the 'left' means ''failure'', and the contained value is the description, not the 1. 161 | * 162 | * println(result.run.written.shows) 163 | * // Will print: 164 | * // Failed: One 165 | * }}} 166 | */ 167 | def logFailure(description: String): DescribedComputation[V] = failure(description) 168 | 169 | /** 170 | * Sugar for [[treelog.LogTreeSyntax.LeafSyntax.logFailure(String) logFailure]] 171 | */ 172 | def ~>!(description: String): DescribedComputation[V] = logFailure(description) 173 | 174 | /** 175 | * Create a ''failure'' [[treelog.LogTreeSyntax.DescribedComputation]] using the given `description` function to 176 | * generate a description for the tree node's label and for the `DescribedComputations` value (i.e. 177 | * the value will be `\/-(description(value))`. 178 | * 179 | * {{{ 180 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 181 | * import scalaz.syntax.show._ 182 | * 183 | * val leaf = 1 logFailure (x ⇒ s"One - $x") 184 | * println(result.run.value) 185 | * // Will print: -\/("One") - note that the 'left' means ''failure'', and the contained value is the description, not the 1. 186 | * 187 | * println(result.run.written.shows) 188 | * // Will print: 189 | * // Failed: One - 1 190 | * }}} 191 | */ 192 | def logFailure(description: V ⇒ String): DescribedComputation[V] = logFailure(description(value)) 193 | 194 | /** 195 | * Sugar for [[treelog.LogTreeSyntax.LeafSyntax.logFailure(V ⇒ String) logFailure]] 196 | */ 197 | def ~>!(description: V ⇒ String): DescribedComputation[V] = logFailure(description) 198 | } 199 | 200 | /** 201 | * Syntax for allowing annotations to be added to log tree nodes. 202 | * 203 | * The best way to see how this syntax works is to take a look at the 204 | * [[https://github.com/lancewalton/treelog#annotations annotations example]] on GitHub. 205 | * 206 | * Here is a short example: 207 | * 208 | * {{{ 209 | * import scalaz.syntax.show._ 210 | * 211 | * val syntax = new LogTreeSyntax[String] {} 212 | * import syntax._ 213 | * 214 | * val result = 1 ~> "One" ~~ Set("Annotating with a string", "And another") 215 | * println(result.run.value) 216 | * // Will print: \/-(1) - note that the 'right' means ''success'' 217 | * 218 | * println(result.run.written.shows) 219 | * // Will print: 220 | * // One - [Annotating with a string, And another] 221 | * }}} 222 | */ 223 | implicit class AnnotationsSyntax[V](w: DescribedComputation[V]) { 224 | 225 | /** 226 | * Annotate a [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]]. 227 | */ 228 | def annotateWith(annotations: Set[Annotation]): DescribedComputation[V] = { 229 | val newTree = w.run.written match { 230 | case Node(l: DescribedLogTreeLabel[Annotation], c) ⇒ Node(l.copy(annotations = l.annotations ++ annotations), c) 231 | case Node(l: UndescribedLogTreeLabel[Annotation], c) ⇒ Node(l.copy(annotations = l.annotations ++ annotations), c) 232 | } 233 | 234 | w.run.value match { 235 | case -\/(error) ⇒ failure(error, newTree) 236 | case \/-(value) ⇒ success(value, newTree) 237 | } 238 | } 239 | 240 | /** 241 | * Sugar for [[treelog.LogTreeSyntax.AnnotationsSyntax.annotateWith(Set[Annotation]) annotateWith(annotations)]] 242 | */ 243 | def ~~(annotations: Set[Annotation]): DescribedComputation[V] = annotateWith(annotations) 244 | 245 | /** 246 | * Equivalent to `~~ annotation` 247 | */ 248 | def annotateWith(annotation: Annotation): DescribedComputation[V] = ~~(Set(annotation)) 249 | 250 | /** 251 | * Syntactic sugar equivalent to [[treelog.LogTreeSyntax.AnnotationsSyntax.annotateWith(Annotation) annotateWith(annotation)]] 252 | */ 253 | def ~~(annotation: Annotation): DescribedComputation[V] = annotateWith(annotation) 254 | 255 | 256 | /** 257 | * Get the union of all annotations in the log tree of the [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]]. 258 | */ 259 | def allAnnotations: Set[Annotation] = { 260 | def recurse(tree: LogTree, accumulator: Set[Annotation]): Set[Annotation] = tree.subForest.foldLeft(accumulator ++ tree.rootLabel.annotations)((acc, child) ⇒ recurse(child, acc)) 261 | recurse(w.run.written, Set()) 262 | } 263 | } 264 | 265 | /** 266 | * Syntax for treating booleans as indicators of success or failure in a computation. 267 | * 268 | * The simplest usage is something like: `myBoolean ~>? "Is my boolean true?"`. The 'value' 269 | * and log tree of the returned [[treelog.LogTreeSyntax.DescribedComputation]] will indicate success or failure 270 | * depending on the value of `myBoolean`. 271 | */ 272 | implicit class BooleanSyntax(b: Boolean) { 273 | 274 | /** 275 | * Use the same description whether the boolean is `true` or `false`. 276 | * Equivalent to `~>?(description, description)` 277 | */ 278 | def ~>?(description: String): DescribedComputation[Boolean] = ~>?(description, description) 279 | 280 | /** 281 | * Use different descriptions for the `true` and `false` cases. Note that unlike `'if'` 282 | * the `false` / failure description is the first parameter and the `true` / success 283 | * description is the second parameter. This is to maintain consistency with [[treelog.LogTreeSyntax.OptionSyntax OptionSyntax]] 284 | * and [[treelog.LogTreeSyntax.EitherSyntax EitherSyntax]]. 285 | * 286 | * If the boolean is `true` the 'value' of the returned DescribedComputation will be `\/-(true)`, 287 | * otherwise, the 'value' will be `-\/(description)`. 288 | */ 289 | def ~>?(failureDescription: ⇒ String, successDescription: ⇒ String): DescribedComputation[Boolean] = 290 | if (b) success(true, successDescription) else failure(failureDescription) 291 | } 292 | 293 | /** 294 | * Syntax for treating `Options` as indicators of success or failure in a computation. 295 | * 296 | * The simplest usage is something like: `myOption ~>? "Do I have Some?"`. The 'value' 297 | * and log tree of the returned [[treelog.LogTreeSyntax.DescribedComputation]] will indicate success or failure 298 | * depending on the value of `myOption`. 299 | */ 300 | implicit class OptionSyntax[V](option: Option[V]) { 301 | 302 | /** 303 | * Use the same description whether the Option is `Some` or `None`. 304 | * Equivalent to `log(description, description)` 305 | */ 306 | def log(description: String): DescribedComputation[V] = log(description, description) 307 | 308 | /** 309 | * Sugar for [[treelog.LogTreeSyntax.OptionSyntax.log(String) log(String)]] 310 | */ 311 | def ~>?(description: String): DescribedComputation[V] = log(description) 312 | 313 | /** 314 | * Use different descriptions for the `Some` and `None` cases. 315 | * 316 | * If the option is `Some(x)` the 'value' of the returned DescribedComputation will be `\/-(x)`, 317 | * otherwise, the 'value' will be `-\/(noneDescription)`. 318 | */ 319 | def log(noneDescription: ⇒ String, someDescription: ⇒ String): DescribedComputation[V] = ~>?(noneDescription, _ ⇒ someDescription) 320 | 321 | /** 322 | * Sugar for [[treelog.LogTreeSyntax.OptionSyntax.log(String, String) log(String, String)]] 323 | */ 324 | def ~>?(noneDescription: ⇒ String, someDescription: ⇒ String): DescribedComputation[V] = log(noneDescription, someDescription) 325 | 326 | /** 327 | * Use different descriptions for the `Some` and `None` cases, providing the boxed `Some` 328 | * value to the function used to produce the description for the `Some` case, so that it can be included in the 329 | * description if you wish. 330 | * 331 | * If the option is `Some(x)` the 'value' of the returned DescribedComputation will be `\/-(x)`, 332 | * otherwise, the 'value' will be `-\/(noneDescription)`. 333 | */ 334 | def log(noneDescription: ⇒ String, someDescription: V ⇒ String): DescribedComputation[V] = 335 | option map { a ⇒ success(a, someDescription(a)) } getOrElse failure(noneDescription) 336 | 337 | /** 338 | * Sugar for [[treelog.LogTreeSyntax.OptionSyntax.log() log(String, String)]] 339 | */ 340 | def ~>?(noneDescription: ⇒ String, someDescription: V ⇒ String): DescribedComputation[V] = log(noneDescription, someDescription) 341 | 342 | /** 343 | * Return a default [[treelog.LogTreeSyntax.DescribedComputation]] if `option` is a `None`. 344 | * 345 | * If the option is `Some(x)` the 'value' of the returned DescribedComputation will be `\/-(Some(x))`, 346 | * otherwise, the returned [[treelog.LogTreeSyntax.DescribedComputation]] will be `dflt`. 347 | */ 348 | def ~>|[B](f: V ⇒ DescribedComputation[B], dflt: ⇒ DescribedComputation[Option[B]]): DescribedComputation[Option[B]] = 349 | option.map(f).map((v: DescribedComputation[B]) ⇒ v.map(w ⇒ Option(w))) getOrElse dflt 350 | } 351 | 352 | /** 353 | * Syntax for treating `scalaz.\/` as signifiers of success or failure in a computation. 354 | * 355 | * The simplest usage is something like: `myEither ~>? "Do I have the right?"`. The 'value' 356 | * and log tree of the returned [[treelog.LogTreeSyntax.DescribedComputation]] will indicate success or failure 357 | * depending on the value of `myEither`. 358 | */ 359 | implicit class EitherSyntax[V](either: \/[String, V]) { 360 | 361 | /** 362 | * Use different descriptions depending on whether `either` is a `\/-` or a `-\/`. 363 | */ 364 | def ~>?(leftDescription: String ⇒ String, rightDescription: ⇒ String): DescribedComputation[V] = 365 | ~>?(leftDescription, _ ⇒ rightDescription) 366 | 367 | /** 368 | * Use the same description regardless of whether `either` is a `\/-` or a `-\/`. 369 | * Equivalent to: `~>?((error: String) ⇒ s"$description - $error", description)` 370 | */ 371 | def ~>?(description: String): DescribedComputation[V] = ~>?((error: String) ⇒ s"$description - $error", description) 372 | 373 | /** 374 | * Use the given description if `either` is a `\/-`. If `either` is 375 | * `-\/(message)`, use `message` as the description. 376 | */ 377 | def ~>?(description: V ⇒ String): DescribedComputation[V] = ~>?((error: String) ⇒ error, description) 378 | 379 | /** 380 | * Use the given functions to provide descriptions depending on whether `either` is a 381 | * `\/-` or `-\/` 382 | */ 383 | def ~>?(leftDescription: String ⇒ String, rightDescription: V ⇒ String): DescribedComputation[V] = 384 | either.fold(error ⇒ failure(leftDescription(error)), a ⇒ success(a, rightDescription(a))) 385 | } 386 | 387 | /** 388 | * Syntax for labeling or creating new branches in a log tree given a description. 389 | */ 390 | implicit class BranchLabelingSyntax(description: String) { 391 | 392 | /** 393 | * Create a new branch given a monadic, traversable 'container' `F[DescribedComputation[Value]]`, 'sequence' it 394 | * to create a `DescribedComputation[F[Value]]`, and give the new `DescribedComputation's` log tree a 395 | * new root node, with the given `description` and whose children are the trees in the 396 | * `F[DescribedComputation[Value]]`. 397 | * 398 | * For example, if we evaluate this method with `F` instantiated as `List`, we would turn a 399 | * `List[DescribedComputation[Value]]` into a `DescribedComputation[List[Value]]`, such that the 400 | * `List[Value]` which is the result's 'value' is obtained by extracting the 'value' from each 401 | * [[treelog.LogTreeSyntax.DescribedComputation]] in the `describedComputations` parameter. Likewise, the child nodes 402 | * of the returned log tree root node are obtained by extracting the log tree from each of the `describedComputations`. 403 | * 404 | * The ''success'' status of the returned `DescribedComputations` log tree is `true` if all of the children 405 | * are successful. It is `false` otherwise. 406 | */ 407 | def ~<[F[_] : Monad : Traverse, V](describedComputations: F[DescribedComputation[V]]): DescribedComputation[F[V]] = 408 | ~<+(describedComputations, (x: F[V]) ⇒ x) 409 | 410 | /** 411 | * As ~< but folding over the resulting F[Value] to yield R and return a DescribedComputation[R] with all the logs. 412 | * 413 | * For example, given l = List[DescribedComputation[Int]], and f = List[Int] ⇒ Int (say summing the list), then 414 | * `"Sum" ~<+(l, f)` would return a DescribedComputation containing the sum of the elements of the list. 415 | */ 416 | def ~<+[F[_] : Monad : Traverse, V, R](describedComputations: F[DescribedComputation[V]], f: F[V] ⇒ R): DescribedComputation[R] = { 417 | val monad = implicitly[Monad[F]] 418 | val parts = monad.map(describedComputations)(m ⇒ (m.run.value, m.run.written)) 419 | 420 | val children = monad.map(parts)(_._2).toList 421 | val branch = Node( 422 | DescribedLogTreeLabel( 423 | description, 424 | allSuccessful(children), 425 | Set[Annotation]()), 426 | children.toStream) 427 | 428 | describedComputations.sequence.run.value match { 429 | case -\/(_) ⇒ failure(description, branch) 430 | case \/-(v) ⇒ success(f(v), branch) 431 | } 432 | } 433 | 434 | /** 435 | * If `dc` has a log tree with an undescribed root node, give the root node the `description` but otherwise 436 | * leave it unchanged. If the log tree has a described root node, create a new root node above the existing one and give the 437 | * new root node the `description`. In both cases preserve the `value` and success/failure status. 438 | */ 439 | def ~<[V](dc: DescribedComputation[V]): DescribedComputation[V] = 440 | dc.run.value match { 441 | case -\/(_) ⇒ failure(description, branchHoister(dc.run.written, description)) 442 | case \/-(value) ⇒ success(value, branchHoister(dc.run.written, description)) 443 | } 444 | 445 | private def branchHoister(tree: LogTree, description: String): LogTree = tree match { 446 | case Node(l: UndescribedLogTreeLabel[Annotation], children) ⇒ Node(DescribedLogTreeLabel(description, allSuccessful(children), l.annotations), children) 447 | case Node(l: DescribedLogTreeLabel[Annotation], children) ⇒ Node(DescribedLogTreeLabel(description, allSuccessful(List(tree))), Stream(tree)) 448 | } 449 | 450 | private def allSuccessful(trees: Iterable[LogTree]) = trees.forall(_.rootLabel.success()) 451 | } 452 | 453 | implicit class FoldSyntax[V](values: Iterable[V]) { 454 | 455 | /** 456 | * Starting with a given value and description, foldleft over an Iterable of values and 'add' them, describing each 'addition'. 457 | */ 458 | def ~>/[R](description: String, initial: DescribedComputation[R], f: (R, V) ⇒ DescribedComputation[R]): DescribedComputation[R] = { 459 | @scala.annotation.tailrec 460 | def recurse(remainingValues: Iterable[V], partialResult: DescribedComputation[R]): DescribedComputation[R] = 461 | if (remainingValues.isEmpty) partialResult 462 | else 463 | partialResult.run.value match { 464 | case -\/(m) ⇒ partialResult 465 | case \/-(_) ⇒ recurse(remainingValues.tail, partialResult.flatMap(p ⇒ f(p, remainingValues.head))) 466 | } 467 | 468 | description ~< recurse(values, initial) 469 | } 470 | } 471 | 472 | /** 473 | * Syntax for dealing with traversable monads 474 | */ 475 | implicit class TraversableMonadSyntax[F[_]: Monad: Traverse, V](values: F[V]) { 476 | 477 | /** 478 | * This method is syntactic sugar for `description ~< monad.map(values)(f)` 479 | */ 480 | def ~>*[B](description: String, f: V ⇒ DescribedComputation[B]): DescribedComputation[F[B]] = description ~< implicitly[Monad[F]].map(values)(f) 481 | } 482 | 483 | /** 484 | * Syntax for labeling root nodes of trees in `DescribedComputions` 485 | */ 486 | implicit class LabellingSyntax[V](w: DescribedComputation[V]) { 487 | 488 | /** 489 | * This method is syntactic sugar for `description ~< w` 490 | */ 491 | def ~>(description: String) = description ~< w 492 | } 493 | 494 | implicit def logTreeShow(implicit annotationShow: Show[Annotation]) = new Show[LogTree] { 495 | 496 | override def shows(t: LogTree) = toList(t).map(line ⇒ " " * line._1 + line._2).mkString(System.getProperty("line.separator")) 497 | 498 | private def toList(tree: LogTree, depth: Int = 0): List[(Int, String)] = 499 | line(depth, tree.rootLabel) :: tree.subForest.flatMap(toList(_, depth + 1)).toList 500 | 501 | private def line(depth: Int, label: LogTreeLabel[Annotation]) = (depth, showAnnotations(label.annotations, showSuccess(label.success(), showDescription(label)))) 502 | 503 | private def showAnnotations(annotations: Set[Annotation], line: String) = 504 | if (annotations.isEmpty) line else line + " - [" + annotations.map(annotationShow.show).mkString(", ") + "]" 505 | 506 | private def showDescription(label: LogTreeLabel[Annotation]) = label.fold(_.description, _ ⇒ "No Description") 507 | 508 | private def showSuccess(success: Boolean, s: String) = if (success) s else "Failed: " + s 509 | } 510 | 511 | type SerializableDescribedComputation[V] = (\/[String, V], SerializableTree[Annotation]) 512 | 513 | def toSerializableForm[V](dc: DescribedComputation[V]): SerializableDescribedComputation[V] = { 514 | def transform(tree: LogTree): SerializableTree[Annotation] = SerializableTree(tree.rootLabel, tree.subForest.map(transform).toList) 515 | Tuple2(dc.run.value, transform(dc.run.written)) 516 | } 517 | 518 | def fromSerializableForm[V](sdc: SerializableDescribedComputation[V]): DescribedComputation[V] = { 519 | def transform(tree: SerializableTree[Annotation]): LogTree = Node(tree.label, tree.children.map(transform).toStream) 520 | sdc._1.fold(m ⇒ failure(m, transform(sdc._2)), v ⇒ success(v, transform(sdc._2))) 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /src/main/scala/treelog/LogTreeSyntaxWithoutAnnotations.scala: -------------------------------------------------------------------------------- 1 | package treelog 2 | 3 | import scalaz.Show 4 | 5 | object LogTreeSyntaxWithoutAnnotations extends LogTreeSyntax[Nothing] { 6 | implicit object NothingShow extends Show[Nothing] { 7 | override def shows(n: Nothing): String = "" 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/scala/treelog/QuadraticRootsExample.scala: -------------------------------------------------------------------------------- 1 | package treelog 2 | 3 | import treelog.LogTreeSyntaxWithoutAnnotations._ 4 | import scalaz._ 5 | import Scalaz._ 6 | 7 | object QuadraticRootsExample extends App { 8 | 9 | private def showDescription(label: LogTreeLabel[Nothing]) = label.fold(_.description, _ ⇒ "No Description") 10 | 11 | case class Parameters(a: Double, b: Double, c: Double) 12 | 13 | // Roots are real 14 | println("Success case:") 15 | 16 | // Roots are complex 17 | println("Failure case:") 18 | 19 | def root(parameters: Parameters) = { 20 | "Extracting root" ~< { 21 | for { 22 | num ← numerator(parameters) ~> "Calculating Numerator" 23 | den ← denominator(parameters) ~> "Calculating Denominator" 24 | root ← (num / den) ~> ("Got root = numerator / denominator: " + _) 25 | } yield root 26 | } 27 | } 28 | 29 | private def numerator(parameters: Parameters) = 30 | for { 31 | det ← determinant(parameters) 32 | sqrtDet ← sqrtDeterminant(det) 33 | b ← parameters.b ~> ("Got b: " + _) 34 | minusB ← -b ~> ("Got -b: " + _) 35 | sum ← (minusB + sqrtDet) ~> ("Got -b + sqrt(determinant): " + _) 36 | } yield sum 37 | 38 | private def sqrtDeterminant(det: Double) = 39 | "Calculating sqrt(determinant)" ~< { 40 | for { 41 | _ ← if (det >= 0) det ~> (d ⇒ s"Determinant ($d) is >= 0") else det ~>! (d ⇒ s"Determinant ($d) is < 0") 42 | sqrtDet ← Math.sqrt(det) ~> ("Got sqrt(determinant): " + _) 43 | } yield sqrtDet 44 | } 45 | 46 | private def denominator(parameters: Parameters) = 47 | for { 48 | a ← parameters.a ~> ("Got a: " + _) 49 | twoA ← (2 * a) ~> ("Got 2a: " + _) 50 | } yield twoA 51 | 52 | private def determinant(parameters: Parameters) = 53 | "Calculating Determinant" ~< { 54 | for { 55 | bSquared ← bSquared(parameters) 56 | fourac ← fourac(parameters) 57 | determinant ← (bSquared - fourac) ~> ("Got b^2 - 4ac: " + _) 58 | } yield determinant 59 | } 60 | 61 | private def bSquared(parameters: Parameters) = 62 | "Calculating b^2" ~< { 63 | for { 64 | b ← parameters.b ~> ("Got b: " + _) 65 | bSquared ← (b * b) ~> ("Got b^2: " + _) 66 | } yield bSquared 67 | } 68 | 69 | private def fourac(parameters: Parameters) = 70 | "Calculating 4ac" ~< { 71 | for { 72 | a ← parameters.a ~> ("Got a: " + _) 73 | c ← parameters.c ~> ("Got c: " + _) 74 | fourac ← (4 * a * c) ~> ("Got 4ac: " + _) 75 | } yield fourac 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/scala/treelog/SerializableTree.scala: -------------------------------------------------------------------------------- 1 | package treelog 2 | 3 | final case class SerializableTree[Annotation](label: LogTreeLabel[Annotation], children: List[SerializableTree[Annotation]]) 4 | -------------------------------------------------------------------------------- /src/main/scala/treelog/package.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * =Introduction= 3 | * 4 | * TreeLog enables logging as a tree structure so that comprehensive logging does not become incomprehensible. 5 | * 6 | * It is often necessary to understand exactly what happened in a computation, not just that it succeeded or failed, but what was actually done 7 | * and with what data. TreeLog produces a description of a computation (along with a result) as a hierarchical log of 8 | * computations that led to the result. The tree could be logged as text or stored in a database so that users can see 9 | * a detailed audit trail of the processing that has occurred for particular entities. 10 | * 11 | * Note that in the remainder of this document, results of producing log trees will be shown by rendering 12 | * the tree textually, but that is only one possible way. 13 | * 14 | * Nodes in the log tree can be annotated with important information for your program to use later. This is useful, for example, when you want to audit 15 | * a process that affects multiple entities, and you want to ensure that the audit trail is associated with each of the modified entities. 16 | * 17 | * =DescribedComputation= 18 | * 19 | * All of this works by ''fting'' the intermediate and final results of computations and the description of the steps into a type called [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]] 20 | * (declared in [[treelog.LogTreeSyntax LogTreeSyntax]]). 21 | 22 | * ==Some Simple Lifting== 23 | * 24 | * You can produce a [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]] very simply with many of the methods in [[treelog.LogTreeSyntax LogTreeSyntax]]. The simplest few are: 25 | * 26 | * {{{ 27 | * // This is a concrete implementation of LogTreeSyntax that is provided for you 28 | * // to use if you don't need to use annotations (see later) 29 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 30 | * 31 | * val result1 = success(2 * 3, "Calculated product") 32 | * // result1 is now a DescribedComputation and carries the successful result and 33 | * // a single node tree telling us that the product was calculated. See below for how to 34 | * // extract these things. 35 | * 36 | * val result2 = failure("It's all wrong") 37 | * // result2 is now a DescribedComputation whose value and tree both tell us that things 38 | * // went wrong 39 | * 40 | * val result3 = (2 * 3) ~> "Calculated product" 41 | * // The same as result1 42 | * 43 | * val result4 = (2 * 3) ~> (p ⇒ "Calculated product: " + p) 44 | * // The same as result1, except the description in the tree node will be "Calculated product: 6" 45 | * }}} 46 | * 47 | * ''result3'' and ''result4'' above introduce the first pieces of syntax related to producing 48 | * ''DescribedComputations''. In this case it lifts the value into the [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]] 49 | * and creates a leaf node in the associated log tree. See [[treelog.LogTreeSyntax.LeafSyntax LeafSyntax]] for 50 | * related simple syntax for leaves. 51 | * 52 | * == Extracting the Result and Log == 53 | * 54 | * When a computation result is ''lifted'' into a [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]] by one of the many methods 55 | * in the [[treelog.LogTreeSyntax LogTreeSyntax]] trait, it is possible to retrieve the ''value'' of the computation like this: 56 | * 57 | * {{{ 58 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 59 | * val foo = 1 ~> "Here's one" 60 | * val value = foo.run.value 61 | * // value will now be equal to scalaz.\/-(1), which represents a successful computation. 62 | * }}} 63 | * 64 | * The ''value'' is a scalaz ''Either'' (scalaz.\/). Following the usual convention: 65 | * 69 | * 70 | * Likewise, it is possible to retrieve the log tree like this: 71 | * 72 | * {{{ 73 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 74 | * val foo = 1 ~> "Here's one" 75 | * val logtree = foo.run.written 76 | * // logtree will now be an instance of LogTree which is a type alias which in this case expands to: 77 | * // Tree[LogTreeLabel[Nothing]](DescribedLogTreeLabel[Nothing]("Here's one", true, Set[Nothing]()) 78 | * // Where: 79 | * // - "Here's one" is the description provided in the declaration of foo 80 | * // - true indicates that the computation represented by the node was successful 81 | * // - the empty set represents the annotations specified for this node 82 | * }}} 83 | * 84 | * It may seem strange that both the ''value'' and the log tree provide indications of success and failure (the ''value'' 85 | * through the use of ''scalaz.\/'', and the log tree with a ''boolean'' property in the [[treelog.LogTreeLabel LogTreeLabel]] label). 86 | * The reason for this is that part of a computation may fail (which we want to indicate in the log tree), but then a different strategy 87 | * is tried which succeeds leading to a successful overall result. 88 | * 89 | * == More Comprehensive Computations == 90 | * 91 | * (An extended example of this kind of thing is the 92 | * [[https://github.com/lancewalton/treelog/blob/master/src/test/scala/QuadraticRootsExample.scala quadratic roots example on GitHub]]) 93 | * 94 | * Generally, once a value has been lifted, it is a good idea to keep working with it in that form for as long 95 | * as possible before accessing the ''value'' and ''written'' properties. Think monadically! 96 | * The examples above show a value being lifted into the DescribedComputation. To continue to work monadically, 97 | * for-comprehensions come into play: 98 | * 99 | * {{{ 100 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 101 | * import scalaz.syntax.show._ 102 | * 103 | * val result = for { 104 | * foo ← 1 ~> ("foo = " + _) // Using the overload of ~> that gives us the ''value'' 105 | * bar ← 2 ~> ("bar = " + _) // so that we can include it in the log messages 106 | * foobar ← (foo + bar) ~> ("foobar = " + _) 107 | * } yield foobar 108 | * 109 | * println(result.run.value) 110 | * // Will print \/-(3) (i.e. a successful computation of 1 + 2) 111 | * 112 | * println(result.run.written.shows) 113 | * // Will print: 114 | * // No description 115 | * // foo = 1 116 | * // bar = 2 117 | * // foobar = 3 118 | * }}} 119 | * 120 | * (For those struggling with the full power of for-comprehensions, I suggest turning the above example into its unsugared flatmap/map form 121 | * to see what is going on. The central point is that ''foo'' will have the value 1, ''bar'' will have the value 2, and 122 | * ''foobar'' will have the value 3; the monadic stuff all happens in the background.) 123 | * 124 | * == Non-Leaf Nodes == 125 | * 126 | * Non-leaf nodes (branches) are created explicitly by the developer or implicitly by the [[treelog.LogTreeSyntax LogTreeSyntax]] under 127 | * various conditions. 128 | * 129 | * The log tree above has a root node with 'No description' and three child (leaf) nodes with descriptions obviously obtained from the 130 | * arguments to the right of the ''~>'' operators in the for-comprehension. This is because the three leaf nodes explicitly 131 | * created in that for-comprehension need to be placed somewhere while the log tree is produced. An obvious thing to do was to make them 132 | * child nodes of a branch, which [[treelog.LogTreeSyntax LogTreeSyntax]] does, using some rules for when to create a new branch to 133 | * contain existing children and when to just add new children to an existing branch. 134 | * 135 | * However, at the time the branch is created there is no ready description available for it, hence the "No description" text when the tree is 136 | * shown using the ''scalaz.Show'' defined for it. Producing a hierarchical log isn't much use if we 137 | * can't describe the non-leaf elements. We can provide a description in two ways (this looks ugly, but read and it will get more elegant…): 138 | * 139 | * {{{ 140 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 141 | * import scalaz.syntax.show._ 142 | * 143 | * val result = for { 144 | * foo ← 1 ~> ("foo = " + _) // Using the overload of ~> that gives us the ''value'' 145 | * bar ← 2 ~> ("bar = " + _) // so that we can include it in the log messages 146 | * foobar ← (foo + bar) ~> ("foobar = " + _) 147 | * } yield foobar 148 | * 149 | * 150 | * val resultWithDescription1 = result ~> "Adding up" 151 | * println(resultWithDescription1.run.written.shows) 152 | * // Will print: 153 | * // Adding up 154 | * // foo = 1 155 | * // bar = 2 156 | * // foobar = 3 157 | * 158 | * val resultWithDescription2 = "Adding up" ~< result 159 | * println(resultWithDescription2.run.written.shows) 160 | * // Will also print: 161 | * // Adding up 162 | * // foo = 1 163 | * // bar = 2 164 | * // foobar = 3 165 | * }}} 166 | * 167 | * The first approach (''resultWithDescription1'' using ''~gt;'') will generally be used when a method/function used to provide an intermediate result 168 | * in the middle of a for-comprehension returns an undescribed root node. Then the code flows quite nicely thus: 169 | * 170 | * {{{ 171 | * val result = for { 172 | * something ← doSomething() ~> "Something has been done" 173 | * more ← doMore(something) ~> "More has been done" 174 | * } yield more 175 | * }}} 176 | * 177 | * Here, ''doSomething()'' and ''doMore(...)'' return DescribedComputations carrying a log tree with an undescribed root node. 178 | * They have been given descriptions in the above for-comprehension. 179 | * 180 | * The second approach (''resultWithDescription2'' using ''~<'') will generally be used when a for-comprehension yields a 181 | * [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]] (which will always have a log tree with an undescribed root node if the for-comprehension has more than 182 | * one generator), and you want to immediately give the root node a description. In this case, it is more natural to write: 183 | * 184 | * {{{ 185 | * val result = "Adding up" ~< { 186 | * for { 187 | * foo ← 1 ~> ("foo = " + _) 188 | * bar ← 2 ~> ("bar = " + _) 189 | * foobar ← (foo + bar) ~> ("foobar = " + _) 190 | * } yield foobar 191 | * } 192 | * }}} 193 | * 194 | * Both of these approaches are demonstrated in the 195 | * [[https://github.com/lancewalton/treelog/blob/master/src/test/scala/QuadraticRootsExample.scala quadratic roots example]]. There is no good 196 | * reason for mixing the two approaches in that example, other than for purposes of demonstration. 197 | * 198 | * ''~<'' works not only for [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]], but for any ''F[DescribedComputation]'' as long as 199 | * ''F'' has a ''scalaz.Monad'' and a ''scalaz.Traverse'' defined and available in implicit scope. 200 | * See [[treelog.LogTreeSyntax.BranchLabelingSyntax BranchLabelingSyntax]]. 201 | * 202 | * = Special Lifting = 203 | * 204 | * == Boolean, Option and \/ == 205 | * 206 | * ''Boolean'', ''Option'' and ''\/'' (scalaz's ''Either''), have some special syntax ''~>?'' associated with 207 | * them to allow ''true'', ''Some(.)'' and ''\/-(.)'' to be treated as successful computational outcomes, and 208 | * ''false'', ''None'' and ''-\/(.)'' to be treated as failure conditions. For example: 209 | * 210 | * {{{ 211 | * val result = false ~>? "Doing a thing with a Boolean" 212 | * println(result.run.value) 213 | * // Will print -\/(Doing a thing with a Boolean) (note that it's a ''left'') 214 | * 215 | * println(result.run.written.shows) 216 | * // Will print: 217 | * // Failed: Doing a thing with a Boolean 218 | * }}} 219 | * 220 | * ''~>?'' is overloaded for each of the three types above to allow either a simple description to be given (as in the example above) 221 | * or for different descriptions to be given in the success versus failure case. Also, in the case of ''Option'' and ''\/'', 222 | * overloads are provided to pass the values contained. See [[treelog.LogTreeSyntax.BooleanSyntax BooleanSyntax]], 223 | * [[treelog.LogTreeSyntax.OptionSyntax OptionSyntax]], [[treelog.LogTreeSyntax.EitherSyntax EitherSyntax]]. 224 | * 225 | * Note that it is easy to get drawn into always using this syntax for these three types. But sometimes, for example, a ''Boolean'' 226 | * ''false'' does not indicate a failure in a computation and so ''~>?'' is not appropriate. Keep in mind that ''failure'' 227 | * means that the computation will stop, whereas ''success'' will mean that the computation will continue. 228 | * 229 | * == Traversable Monads == 230 | * 231 | * Suppose you have a ''List[A]'' and a function ''f: A ⇒ DescribedComputation[B]'', and you want to apply ''f(.)'' 232 | * to each element of the list to produce ''DescribedComputations'' for each element. That's easy enough. But suppose you now want 233 | * to take all of the 'values' (''vs'') contained in the list of ''DescribedComputations'' thus produced, and create a new 234 | * [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]] whose value is ''vs'' and whose log tree is a branch with a description and whose children 235 | * are the log trees resulting from each application of ''f(.)''. 236 | * 237 | * We needed to do precisely that very often, so we wrote some syntax for it: 238 | * 239 | * {{{ 240 | * import treelog.LogTreeSyntaxWithoutAnnotations._ 241 | * import scalaz.syntax.show._ 242 | * import scalaz.std.list._ 243 | * 244 | * val result = List(1, 2, 3) ~>* ("Double the values", x ⇒ (x * 2) ~> (y ⇒ s"Double $x = $y")) 245 | * 246 | * println(result.run.value) 247 | * // Will print \/-(List(2, 4, 6)) 248 | * 249 | * println(result.run.written.shows) 250 | * // Will print: 251 | * // Double the values 252 | * // Double 1 = 2 253 | * // Double 2 = 4 254 | * // Double 3 = 6 255 | * }}} 256 | * 257 | * This is particularly useful if there is a possibility that ''f(.)'' can produce ''DescribedComputations'' that represent failures, 258 | * because hoisting children into a branch of a log tree gives the branch a successful status only if all of the children are successful (this is true 259 | * of all syntax that does this). Hence: 260 | * 261 | * {{{ 262 | * val result = List(1, 2, 3) ~>* ("All even", x ⇒ (x % 2 == 0) ~>? s"Testing if $x is even") 263 | * 264 | * println(result.run.value) 265 | * // Will print -\/(All even) - Notice that it's a 'left', meaning failure 266 | * 267 | * println(result.run.written.shows) 268 | * // Will print: 269 | * // Failed: All even 270 | * // Failed: Testing if 1 is even 271 | * // Testing if 2 is even 272 | * // Failed: Testing if 3 is even 273 | * }}} 274 | * 275 | * ''~>*'' works not only for ''List'', but for all kinds that have a ''scalaz.Monad'' and a ''scalaz.Traverse'' 276 | * defined and available in implicit scope. See [[treelog.LogTreeSyntax.TraversableMonadSyntax TraversableMonadSyntax]]. 277 | * 278 | * Another common thing you might want to do with a collection of DescribedComputations is retain their logs as children of a parent whose value 279 | * is some function of the child values. Here is an example of summing the result of several computations: 280 | * 281 | * {{{ 282 | * val parts = List(1 ~> "One", 2 ~> "Two") 283 | * val summed = "Sum" ~<+ (parts, (bits: List[Int]) ⇒ bits.sum) 284 | * 285 | * println(summed.run.written.shows) 286 | * // Will print: 287 | * Sum 288 | * One 289 | * Two 290 | * 291 | * println(summed.run.value) 292 | * // Will print \/-(3) 293 | * }}} 294 | * 295 | * == Annotations == 296 | * 297 | * Nodes in the log tree can be annotated with important information for your program to use later. This is useful, for example, when you want to audit 298 | * a process that affects multiple entities, and you want to make sure that the audit trail is associated with each of the modified entities. You can use 299 | * the annotation facility to carry the key (or something richer) for each modified entity. 300 | * 301 | * The ''~~'' operator (see [[treelog.LogTreeSyntax.AnnotationsSyntax AnnotationsSyntax]]) is provided for this purpose. It can be applied to 302 | * any [[treelog.LogTreeSyntax.DescribedComputation DescribedComputation]] and it will add the given annotation to the set of annotations at the current root node of the log 303 | * tree. Annotations can be of any type, but must all be of the same type for a particular log tree. You choose the type of annotations by instantiating 304 | * the 'Annotation' type parameter of [[treelog.LogTreeSyntax LogTreeSyntax]] 305 | * 306 | * Here is a simple example using ''Strings'' as the annotations type: 307 | * 308 | * {{{ 309 | * val stringAnnotateableLogTreeSyntax = new treelog.LogTreeSyntax[String] {} 310 | * import stringAnnotateableLogTreeSyntax._ 311 | * import scalaz.syntax.show._ 312 | * import scalaz.std.string._ 313 | * 314 | * val result = 1 ~> "This is the description" ~~ "This is the annotation" 315 | * 316 | * println(result.run.value) 317 | * // Will print \/-(1) 318 | * 319 | * println(result.run.written.shows) 320 | * // Will print: 321 | * // This is the description - ["This is the annotation"] 322 | * }}} 323 | * 324 | * See the [[https://github.com/lancewalton/treelog/blob/master/src/test/scala/AnnotationsExample.scala annotations example]] 325 | * for a more comprehensive example. 326 | */ 327 | package object treelog -------------------------------------------------------------------------------- /src/test/scala/com/oranda/treelogui/LogTreeItemSpec.scala: -------------------------------------------------------------------------------- 1 | package com.oranda.treelogui 2 | 3 | 4 | import chandu0101.scalajs.react.components.TreeItem 5 | import org.scalatest._ 6 | import scalaz._ 7 | import Scalaz._ 8 | 9 | import org.scalatest.FlatSpec 10 | 11 | class LogTreeItemSpec extends FlatSpec { 12 | 13 | object LogTreeSimpleSum extends LogTreeItem[String, Int] { 14 | override def result: DescribedComputation[Int] = simpleSum 15 | 16 | protected def annotationShow = new Show[String] { 17 | override def shows(text: String) = text 18 | } 19 | 20 | def simpleSum: DescribedComputation[Int] = 21 | "Calculating sum" ~< { 22 | for { 23 | x ← 11 ~> ("x = " + _) ~~ "assignment of x" 24 | y ← 2 ~> ("y = " + _) ~~ "assignment of y" 25 | sum ← (x + y) ~> ("Sum is " + _) ~~ "summing x and y" 26 | } yield sum 27 | } 28 | } 29 | 30 | "A LogTreeItem with annotations" should "produce the expected TreeItem when treeItem is called" in { 31 | val expectedTreeItem = 32 | TreeItem("Calculating sum", 33 | TreeItem("x = 11 - [assignment of x]", 34 | TreeItem("y = 2 - [assignment of y]", 35 | TreeItem("Sum is 13 - [summing x and y]")))) 36 | assert(treesAreEqual(LogTreeSimpleSum.treeItem, expectedTreeItem)) 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/test/scala/com/oranda/treelogui/LogTreeItemWithoutAnnotationsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.oranda.treelogui 2 | 3 | import chandu0101.scalajs.react.components.TreeItem 4 | import org.scalatest._ 5 | import scalaz._ 6 | import treelog._ 7 | 8 | 9 | class LogTreeItemWithoutAnnotationsSpec extends FlatSpec { 10 | 11 | object LogTreeSimpleSum extends LogTreeItemWithoutAnnotations[Int] { 12 | override def result: DescribedComputation[Int] = simpleSum 13 | 14 | def simpleSum: DescribedComputation[Int] = 15 | "Calculating sum" ~< { 16 | for { 17 | x ← 11 ~> ("x = " + _) 18 | y ← 2 ~> ("y = " + _) 19 | sum ← (x + y) ~> ("Sum is " + _) 20 | } yield sum 21 | } 22 | } 23 | 24 | "A LogTreeItem without annotations" should "produce the expected TreeItem when treeItem is called" in { 25 | val expectedTreeItem = 26 | TreeItem("Calculating sum", 27 | TreeItem("x = 11", 28 | TreeItem("y = 2", 29 | TreeItem("Sum is 13")))) 30 | assert(treesAreEqual(LogTreeSimpleSum.treeItem, expectedTreeItem)) 31 | } 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/test/scala/com/oranda/treelogui/package.scala: -------------------------------------------------------------------------------- 1 | package com.oranda 2 | 3 | import chandu0101.scalajs.react.components.TreeItem 4 | 5 | package object treelogui { 6 | def treesAreEqual(tree1: TreeItem, tree2: TreeItem): Boolean = 7 | tree1.item == tree2.item && 8 | (tree1.children.zip(tree2.children)).forall(cpair => treesAreEqual(cpair._1, cpair._2)) 9 | } 10 | --------------------------------------------------------------------------------