Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement #312 #313

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
2024-12

- preliminary partial impl of #312 "Reflect doc comments in generated code"
- only for scala at the moment, and only scaladoc for object, but not `@param`s yet.
- for now, this is an opt-in feature with `--doc-comments` flag,
but this should be the default behavior.

1.1.5

- Fixed #309 "Empty object not reflected in generated wrapper."
Expand Down
6 changes: 6 additions & 0 deletions src/main/scala/tscfg/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ object Main {
| --scala generate scala code (java)
| --scala:bt use backticks (see #30) (false)
| --durations use java.time.Duration (false)
| --doc-comments reflect documentation comments (see #312)
| --all-required assume all properties are required (see #47)
| --tpl <filename> generate config template (no default)
| --tpl.ind <string> template indentation string ("${templateOpts.indent}")
Expand All @@ -57,6 +58,7 @@ object Main {
packageName: String = defaultGenOpts.packageName,
className: String = defaultGenOpts.className,
destDir: String = defaultDestDir,
docComments: Boolean = false,
assumeAllRequired: Boolean = false,
language: String = "java",
useBackticks: Boolean = false,
Expand Down Expand Up @@ -121,6 +123,9 @@ object Main {
traverseList(rest, opts.copy(destDir = destDir))
else None

case "--doc-comments" :: rest =>
traverseList(rest, opts.copy(docComments = true))

case "--all-required" :: rest =>
traverseList(rest, opts.copy(assumeAllRequired = true))

Expand Down Expand Up @@ -197,6 +202,7 @@ object Main {
val genOpts = GenOpts(
opts.packageName,
opts.className,
docComments = opts.docComments,
assumeAllRequired = opts.assumeAllRequired,
useBackticks = opts.useBackticks,
genGetters = opts.genGetters,
Expand Down
5 changes: 4 additions & 1 deletion src/main/scala/tscfg/ModelBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class ModelBuilder(
effOptional,
effDefault,
childStruct.defineCaseOpt,
docComments = childStruct.docComments,
commentsOpt,
parentClassMembers,
)
Expand Down Expand Up @@ -167,6 +168,7 @@ class ModelBuilder(
effOptional: Boolean,
effDefault: Option[String],
defineCase: Option[DefineCase],
docComments: List[String],
commentsOpt: Option[String],
parentClassMembers: Option[Map[String, model.AnnType]]
): AnnType = {
Expand All @@ -192,6 +194,7 @@ class ModelBuilder(
optional = effOptional,
default = effDefault,
defineCase = defineCase,
docComments = docComments,
comments = commentsOpt,
parentClassMembers = parentClassMembers.map(_.toMap),
)
Expand Down Expand Up @@ -369,7 +372,7 @@ object ModelBuilder {
}

/** build model from TS Config object */
def fromConfig(
private def fromConfig(
rootNamespace: NamespaceMan,
config: Config,
assumeAllRequired: Boolean = false
Expand Down
7 changes: 7 additions & 0 deletions src/main/scala/tscfg/Struct.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ case class Struct(
val comments: List[String] =
cv.origin().comments().asScala.toList

val docComments: List[String] =
comments
.map(_.trim)
.filterNot(c =>
c.startsWith("@") || c.startsWith("!") || c.startsWith("GenOpts:")
)

// Non-None when this is a `@define`
val defineCaseOpt: Option[DefineCase] = {
val defineLines = comments.map(_.trim).filter(_.startsWith("@define"))
Expand Down
6 changes: 6 additions & 0 deletions src/main/scala/tscfg/gen4tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ object gen4tests {
case opt @ "--durations" =>
OptFromFile(opt, None)

case opt @ "--doc-comments" =>
OptFromFile(opt, None)

case opt @ "--all-required" =>
OptFromFile(opt, None)

Expand Down Expand Up @@ -122,6 +125,9 @@ object gen4tests {
case "--all-required" =>
genOpts = genOpts.copy(assumeAllRequired = true)

case "--doc-comments" =>
genOpts = genOpts.copy(docComments = true)

case opt =>
warn(s"$confFile: ignoring unrecognized GenOpts argument: `$opt'")
}
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/tscfg/generators/Generator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ abstract class Generator(genOpts: GenOpts) {
case class GenOpts(
packageName: String,
className: String,
docComments: Boolean = false,
assumeAllRequired: Boolean = false,
useBackticks: Boolean = false,
genGetters: Boolean = false,
Expand Down
9 changes: 8 additions & 1 deletion src/main/scala/tscfg/generators/scala/ScalaGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,20 @@ class ScalaGen(

val results = symbols.map { symbol =>
val a = ot.members(symbol)
val res = generate(
var res = generate(
a.t,
classNamesPrefix = className + "." :: classNamesPrefix,
className = getClassName(symbol),
Some(a),
a.parentClassMembers,
)
if (genOpts.docComments & a.t.isObject) {
// TODO Capture the `@param`s
val docComment = ScalaUtil.formatDocComment(a.docComments)
if (docComment.nonEmpty) {
res = res.copy(definition = docComment + res.definition)
}
}
(symbol, res, a, false)
}

Expand Down
9 changes: 9 additions & 0 deletions src/main/scala/tscfg/generators/scala/ScalaUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,13 @@ object ScalaUtil {
"toString",
"wait"
)

def formatDocComment(docComments: List[String]): String = {
if (docComments.isEmpty) ""
else {
// TODO more appropriate formatting; this is simplistic
val lines = docComments.map(_.trim).filter(_.nonEmpty)
lines.mkString("/** ", "\n * ", "\n */\n")
}
}
}
5 changes: 4 additions & 1 deletion src/main/scala/tscfg/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ object model {

}

sealed abstract class Type
sealed abstract class Type {
def isObject: Boolean = this.isInstanceOf[ObjectAbsType]
}

sealed abstract class BasicType extends Type

Expand Down Expand Up @@ -87,6 +89,7 @@ object model {
optional: Boolean = false,
default: Option[String] = None,
defineCase: Option[DefineCase] = None,
docComments: List[String] = Nil,
comments: Option[String] = None,
parentClassMembers: Option[Map[String, model.AnnType]] = None,
) {
Expand Down
21 changes: 21 additions & 0 deletions src/main/tscfg/example/issue312a.spec.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// GenOpts: --doc-comments --scala
#! Comments starting with `!`, `@`, or `GenOpts` are not transferred to output.

# Description of the required endpoint.
endpoint {
# The associated path.
# For example, "/home/foo/bar"
path = "string"

# Port for the endpoint service.
port = "int | 8080"

# Configuration for notifications.
notification {
# Emails to send notifications to.
emails = [{
# Email address.
email = "string"
}]
}
}
100 changes: 100 additions & 0 deletions src/test/scala/tscfg/example/ScalaIssue312aCfg.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package tscfg.example

final case class ScalaIssue312aCfg(
endpoint : ScalaIssue312aCfg.Endpoint
)
object ScalaIssue312aCfg {
/** Description of the required endpoint.
*/
final case class Endpoint(
notification : ScalaIssue312aCfg.Endpoint.Notification,
path : java.lang.String,
port : scala.Int
)
object Endpoint {
/** Configuration for notifications.
*/
final case class Notification(
emails : scala.List[ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm]
)
object Notification {
final case class Emails$Elm(
email : java.lang.String
)
object Emails$Elm {
def apply(c: com.typesafe.config.Config, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm = {
ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm(
email = $_reqStr(parentPath, c, "email", $tsCfgValidator)
)
}
private def $_reqStr(parentPath: java.lang.String, c: com.typesafe.config.Config, path: java.lang.String, $tsCfgValidator: $TsCfgValidator): java.lang.String = {
if (c == null) null
else try c.getString(path)
catch {
case e:com.typesafe.config.ConfigException =>
$tsCfgValidator.addBadPath(parentPath + path, e)
null
}
}

}

def apply(c: com.typesafe.config.Config, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): ScalaIssue312aCfg.Endpoint.Notification = {
ScalaIssue312aCfg.Endpoint.Notification(
emails = $_LScalaIssue312aCfg_Endpoint_Notification_Emails$Elm(c.getList("emails"), parentPath, $tsCfgValidator)
)
}
private def $_LScalaIssue312aCfg_Endpoint_Notification_Emails$Elm(cl:com.typesafe.config.ConfigList, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): scala.List[ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm] = {
import scala.jdk.CollectionConverters._
cl.asScala.map(cv => ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm(cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, parentPath, $tsCfgValidator)).toList
}
}

def apply(c: com.typesafe.config.Config, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): ScalaIssue312aCfg.Endpoint = {
ScalaIssue312aCfg.Endpoint(
notification = ScalaIssue312aCfg.Endpoint.Notification(if(c.hasPathOrNull("notification")) c.getConfig("notification") else com.typesafe.config.ConfigFactory.parseString("notification{}"), parentPath + "notification.", $tsCfgValidator),
path = $_reqStr(parentPath, c, "path", $tsCfgValidator),
port = if(c.hasPathOrNull("port")) c.getInt("port") else 8080
)
}
private def $_reqStr(parentPath: java.lang.String, c: com.typesafe.config.Config, path: java.lang.String, $tsCfgValidator: $TsCfgValidator): java.lang.String = {
if (c == null) null
else try c.getString(path)
catch {
case e:com.typesafe.config.ConfigException =>
$tsCfgValidator.addBadPath(parentPath + path, e)
null
}
}

}

def apply(c: com.typesafe.config.Config): ScalaIssue312aCfg = {
val $tsCfgValidator: $TsCfgValidator = new $TsCfgValidator()
val parentPath: java.lang.String = ""
val $result = ScalaIssue312aCfg(
endpoint = ScalaIssue312aCfg.Endpoint(if(c.hasPathOrNull("endpoint")) c.getConfig("endpoint") else com.typesafe.config.ConfigFactory.parseString("endpoint{}"), parentPath + "endpoint.", $tsCfgValidator)
)
$tsCfgValidator.validate()
$result
}
final class $TsCfgValidator {
private val badPaths = scala.collection.mutable.ArrayBuffer[java.lang.String]()

def addBadPath(path: java.lang.String, e: com.typesafe.config.ConfigException): Unit = {
badPaths += s"'$path': ${e.getClass.getName}(${e.getMessage})"
}

def addInvalidEnumValue(path: java.lang.String, value: java.lang.String, enumName: java.lang.String): Unit = {
badPaths += s"'$path': invalid value $value for enumeration $enumName"
}

def validate(): Unit = {
if (badPaths.nonEmpty) {
throw new com.typesafe.config.ConfigException(
badPaths.mkString("Invalid configuration:\n ", "\n ", "")
){}
}
}
}
}