diff --git a/core/src/main/scala/flatgraph/help/DocSearchPackages.scala b/core/src/main/scala/flatgraph/help/DocSearchPackages.scala index a7f5b5ee..c0daadb2 100644 --- a/core/src/main/scala/flatgraph/help/DocSearchPackages.scala +++ b/core/src/main/scala/flatgraph/help/DocSearchPackages.scala @@ -3,12 +3,17 @@ package flatgraph.help /** defines where we should search for @Traversal/@TraversalSource/@Doc annotations */ trait DocSearchPackages { def apply(): Seq[String] + + def withAdditionalPackage(packageName: String): DocSearchPackages = { + val combinedPackages = this.apply() :+ packageName + () => combinedPackages + } } object DocSearchPackages { - /** default implicit, for domains that don't have custom steps: no additional packages to search */ - implicit val defaultDocSearchPackage: DocSearchPackages = () => Nil + /** don't scan anywhere other than flatgraph */ + val default: DocSearchPackages = () => List("flatgraph") def apply(searchPackages: String*): DocSearchPackages = () => searchPackages diff --git a/core/src/main/scala/flatgraph/help/TraversalHelp.scala b/core/src/main/scala/flatgraph/help/TraversalHelp.scala index 4b1d2c49..e741d1da 100644 --- a/core/src/main/scala/flatgraph/help/TraversalHelp.scala +++ b/core/src/main/scala/flatgraph/help/TraversalHelp.scala @@ -18,7 +18,7 @@ import scala.jdk.CollectionConverters.* * @param searchPackages: * The base packages that we scan for - we're not scanning the entire classpath */ -class TraversalHelp(searchPackages: DocSearchPackages) { +class TraversalHelp(packageNamesToSearch: DocSearchPackages) { import TraversalHelp._ def forElementSpecificSteps(elementClass: Class[_], verbose: Boolean): String = { @@ -38,10 +38,10 @@ class TraversalHelp(searchPackages: DocSearchPackages) { } val table = Table( - columnNames = if (verbose) ColumnNamesVerbose else ColumnNames, + columnNames = if (verbose) ColumnNames ++ Seq("implemented in", "more details") else ColumnNames, rows = stepDocs.sortBy(_.methodName).map { stepDoc => val baseColumns = List(s".${stepDoc.methodName}", stepDoc.doc.info) - if (verbose) baseColumns :+ stepDoc.traversalClassName + if (verbose) baseColumns ++ Seq(stepDoc.traversalClassName, stepDoc.doc.longInfo) else baseColumns } ) @@ -51,17 +51,19 @@ class TraversalHelp(searchPackages: DocSearchPackages) { |""".stripMargin } - lazy val forTraversalSources: String = { + def forTraversalSources(verbose: Boolean): String = { val stepDocs = for { - packageName <- packageNamesToSearch + packageName <- packageNamesToSearch() traversal <- findClassesAnnotatedWith(packageName, classOf[help.TraversalSource]) stepDoc <- findStepDocs(traversal) } yield stepDoc val table = Table( - columnNames = ColumnNames, + columnNames = if (verbose) ColumnNames :+ "more details" else ColumnNames, rows = stepDocs.distinct.sortBy(_.methodName).map { stepDoc => - List(s".${stepDoc.methodName}", stepDoc.doc.info) + val baseColumns = List(s".${stepDoc.methodName}", stepDoc.doc.info) + if (verbose) baseColumns :+ stepDoc.doc.longInfo + else baseColumns } ) @@ -75,7 +77,7 @@ class TraversalHelp(searchPackages: DocSearchPackages) { */ lazy val stepDocsByElementType: Map[Class[_], List[StepDoc]] = { for { - packageName <- packageNamesToSearch + packageName <- packageNamesToSearch() traversal <- findClassesAnnotatedWith(packageName, classOf[help.Traversal]) annotation <- Option(traversal.getAnnotation(classOf[help.Traversal])).iterator stepDoc <- findStepDocs(traversal) @@ -101,11 +103,8 @@ class TraversalHelp(searchPackages: DocSearchPackages) { .filterNot(_.methodName.endsWith("$extension")) } - private def packageNamesToSearch: Seq[String] = - searchPackages() :+ "flatgraph" } object TraversalHelp { - private val ColumnNames = Array("step", "description") - private val ColumnNamesVerbose = ColumnNames :+ "implemented in" + private val ColumnNames = Array("step", "description") } diff --git a/core/src/main/scala/flatgraph/traversal/Language.scala b/core/src/main/scala/flatgraph/traversal/Language.scala index b9f5f8d8..eb74ee23 100644 --- a/core/src/main/scala/flatgraph/traversal/Language.scala +++ b/core/src/main/scala/flatgraph/traversal/Language.scala @@ -3,6 +3,7 @@ package flatgraph.traversal import flatgraph.help.{Doc, DocSearchPackages, TraversalHelp} import flatgraph.{Accessors, Edge, GNode, MultiPropertyKey, OptionalPropertyKey, PropertyKey, Schema, SinglePropertyKey} +import scala.annotation.implicitNotFound import scala.collection.immutable.ArraySeq import scala.collection.{Iterator, mutable} import scala.reflect.ClassTag @@ -113,10 +114,20 @@ trait GenericLanguage { * the classpath */ @Doc(info = "print help/documentation based on the current elementType `A`.") + @implicitNotFound("""If you're using flatgraph purely without a schema and associated generated domain classes, you can + |start with `given DocSearchPackages = DocSearchPackages.default`. + |If you have generated domain classes, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage`. + |If you have additional custom extension steps that specify help texts via @Doc annotations, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage.withAdditionalPackage("my.custom.package)"` + |""".stripMargin) def help[B >: A](implicit elementType: ClassTag[B], searchPackages: DocSearchPackages): String = new TraversalHelp(searchPackages).forElementSpecificSteps(elementType.runtimeClass, verbose = false) @Doc(info = "print verbose help/documentation based on the current elementType `A`.") + @implicitNotFound("""If you're using flatgraph purely without a schema and associated generated domain classes, you can + |start with `given DocSearchPackages = DocSearchPackages.default`. + |If you have generated domain classes, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage`. + |If you have additional custom extension steps that specify help texts via @Doc annotations, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage.withAdditionalPackage("my.custom.package)"` + |""".stripMargin) def helpVerbose[B >: A](implicit elementType: ClassTag[B], searchPackages: DocSearchPackages): String = new TraversalHelp(searchPackages).forElementSpecificSteps(elementType.runtimeClass, verbose = true) diff --git a/core/src/test/scala/flatgraph/traversal/TraversalTests.scala b/core/src/test/scala/flatgraph/traversal/TraversalTests.scala index 1c459926..1916cfe1 100644 --- a/core/src/test/scala/flatgraph/traversal/TraversalTests.scala +++ b/core/src/test/scala/flatgraph/traversal/TraversalTests.scala @@ -87,6 +87,8 @@ class TraversalTests extends AnyWordSpec with ExampleGraphSetup { } ".help step" should { + // a specific domain would provide it's own DocSearchPackage implementation, to specify where we're supposed to scan for @Doc annotations + given DocSearchPackages = DocSearchPackages.default "generic help for `int`" in { val helpText = Iterator(1, 2, 3, 4).help helpText should include(".cast") diff --git a/core/src/test/scala/flatgraph/traversal/testdomains/simple/ExampleGraphSetup.scala b/core/src/test/scala/flatgraph/traversal/testdomains/simple/ExampleGraphSetup.scala index a3dcd27d..20a72bdd 100644 --- a/core/src/test/scala/flatgraph/traversal/testdomains/simple/ExampleGraphSetup.scala +++ b/core/src/test/scala/flatgraph/traversal/testdomains/simple/ExampleGraphSetup.scala @@ -58,7 +58,8 @@ object SimpleDomain { } val defaultDocSearchPackage: DocSearchPackages = DocSearchPackages(getClass.getPackage.getName) - lazy val help = TraversalHelp(defaultDocSearchPackage).forTraversalSources + lazy val help = TraversalHelp(defaultDocSearchPackage).forTraversalSources(verbose = false) + lazy val helpVerbose = TraversalHelp(defaultDocSearchPackage).forTraversalSources(verbose = true) def newGraph: Graph = { val schema = TestSchema.make(1, 1) diff --git a/domain-classes-generator/src/main/scala/flatgraph/codegen/DomainClassesGenerator.scala b/domain-classes-generator/src/main/scala/flatgraph/codegen/DomainClassesGenerator.scala index fb7466cc..5b8aa394 100644 --- a/domain-classes-generator/src/main/scala/flatgraph/codegen/DomainClassesGenerator.scala +++ b/domain-classes-generator/src/main/scala/flatgraph/codegen/DomainClassesGenerator.scala @@ -737,23 +737,14 @@ class DomainClassesGenerator(schema: Schema) { // domain object and starters: start // TODO: extract into separate method val sanitizeReservedNames = Map("return" -> "ret", "type" -> "typ", "import" -> "imports").withDefault(identity) - def docAnnotationMaybe(nodeType: AbstractNodeType) = - nodeType.comment.map(comment => s"""@flatgraph.help.Doc(info = "$comment")""").getOrElse("") - val starters = mutable.ArrayBuffer[String]() + val starters = mutable.ArrayBuffer[String]() nodeTypes.zipWithIndex.collect { case (typ, idx) => typ.starterName.foreach { starterName => // starter for this concrete node type - val docText = { - val typCommentMaybe = typ.comment - .map { comment => - s" and documentation: ${typ.comment}" - } - .getOrElse("") - s"All nodes of type ${typ.className}, i.e. with label ${typ.name} $typCommentMaybe" - } + val comment = typ.comment.getOrElse("").trim starters.append( - s"""// TODO reimplement help/doc... @overflowdb.traversal.help.Doc(info = "$docText") - |/** $docText */ + s"""/** $comment */ + |@flatgraph.help.Doc(info = \"\"\"$comment\"\"\") |def $starterName: Iterator[nodes.${typ.className}] = wrappedCpg.graph._nodes($idx).asInstanceOf[Iterator[nodes.${typ.className}]]""".stripMargin ) @@ -763,8 +754,7 @@ class DomainClassesGenerator(schema: Schema) { val propertyNameCamelCase = camelCase(property.name) val docText = s"Shorthand for $starterName.$propertyNameCamelCase" starters.append( - s"""// TODO reimplement help/doc... @overflowdb.traversal.help.Doc(info = "$docText") - |/** $docText */ + s"""/** $docText */ |def $starterName($propertyNameCamelCase: ${typeFor( property )}): Iterator[nodes.${typ.className}] = $starterName.$propertyNameCamelCase($propertyNameCamelCase)""".stripMargin @@ -775,15 +765,20 @@ class DomainClassesGenerator(schema: Schema) { schema.nodeBaseTypes.foreach { baseType => baseType.starterName.foreach { starterName => - val types = schema.nodeTypes.filter { _.extendzRecursively.contains(baseType) } - val docText = s"""All nodes of type ${baseType.className}, i.e. with label in ${types.map { _.name }.sorted.mkString(", ")}""" + val docTextInfo = baseType.comment.getOrElse("").trim + val subTypes = schema.nodeTypes.filter(_.extendzRecursively.contains(baseType)).map(_.name).sorted.mkString(", ") + val docTextVerbose = s"""subtypes: $subTypes""" val concreteSubTypeStarters = nodeTypes.collect { case typ if typ.extendzRecursively.contains(baseType) => "this." + sanitizeReservedNames(camelCase(typ.name)) } - starters.append(s"""// TODO reimplement help/doc... @overflowdb.traversal.help.Doc(info = "$docText") - /** $docText */ - def $starterName: Iterator[nodes.${baseType.className}] = Iterator(${concreteSubTypeStarters.mkString(", ")}).flatten""") + starters.append(s""" + |/** $docTextInfo + | * $docTextVerbose + | */ + |@flatgraph.help.Doc(info = \"\"\"$docTextInfo\"\"\", longInfo = \"\"\"$docTextVerbose\"\"\") + |def $starterName: Iterator[nodes.${baseType.className}] = Iterator(${concreteSubTypeStarters.mkString(", ")}).flatten + |""".stripMargin) } } @@ -794,8 +789,25 @@ class DomainClassesGenerator(schema: Schema) { |import Language.* | |object $domainShortName { - | val defaultDocSearchPackage: flatgraph.help.DocSearchPackages = flatgraph.help.DocSearchPackages(getClass.getPackage.getName) - | lazy val help = flatgraph.help.TraversalHelp(defaultDocSearchPackage).forTraversalSources + | val defaultDocSearchPackage = flatgraph.help.DocSearchPackages.default.withAdditionalPackage(getClass.getPackage.getName) + | + |@scala.annotation.implicitNotFound( + | \"\"\"If you're using flatgraph purely without a schema and associated generated domain classes, you can + | |start with `given DocSearchPackages = DocSearchPackages.default`. + | |If you have generated domain classes, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage`. + | |If you have additional custom extension steps that specify help texts via @Doc annotations, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage.withAdditionalPackage("my.custom.package)"` + | |\"\"\".stripMargin) + | def help(implicit searchPackageNames: flatgraph.help.DocSearchPackages) = + | flatgraph.help.TraversalHelp(searchPackageNames).forTraversalSources(verbose = false) + | + |@scala.annotation.implicitNotFound( + | \"\"\"If you're using flatgraph purely without a schema and associated generated domain classes, you can + | |start with `given DocSearchPackages = DocSearchPackages.default`. + | |If you have generated domain classes, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage`. + | |If you have additional custom extension steps that specify help texts via @Doc annotations, use `given DocSearchPackages = MyDomain.defaultDocSearchPackage.withAdditionalPackage("my.custom.package)"` + | |\"\"\".stripMargin) + | def helpVerbose(implicit searchPackageNames: flatgraph.help.DocSearchPackages) = + | flatgraph.help.TraversalHelp(searchPackageNames).forTraversalSources(verbose = true) | | def empty: $domainShortName = new $domainShortName(new flatgraph.Graph(GraphSchema)) | @@ -813,6 +825,11 @@ class DomainClassesGenerator(schema: Schema) { |class $domainShortName(private val _graph: flatgraph.Graph = new flatgraph.Graph(GraphSchema)) extends AutoCloseable { | def graph: flatgraph.Graph = _graph | + | def help(implicit searchPackageNames: flatgraph.help.DocSearchPackages) = + | $domainShortName.help(searchPackageNames) + | def helpVerbose(implicit searchPackageNames: flatgraph.help.DocSearchPackages) = + | $domainShortName.helpVerbose(searchPackageNames) + | | override def close(): Unit = | _graph.close() |}