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

Generate scaladoc for @documentation trait #731

Merged
merged 22 commits into from
Jan 30, 2023
Merged

Generate scaladoc for @documentation trait #731

merged 22 commits into from
Jan 30, 2023

Conversation

zetashift
Copy link
Contributor

Attempt at resolving: #256
@kubukoz's feedback loop tips in #496 were very very helpful!
Still a draft, because I am not sure if I am following the logic of Renderer.scala correct everywhere.
However I was quite happy seeing this:
image

Copy link
Member

@kubukoz kubukoz left a comment

Choose a reason for hiding this comment

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

looking good so far, would love to see multi-line examples and a solution for escaping */. :)

@zetashift
Copy link
Contributor Author

So another random question popped up.

Do we expect things like @constructor @tparam for certain members/shapes to also be generated, or does that fall out of scope?

@kubukoz
Copy link
Member

kubukoz commented Jan 15, 2023

I think that's not necessary at this stage... but @param might be easy to add in the future, and for methods corresponding to operations might be quite useful.

@@ -838,6 +856,7 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self =>
val closing = if (recursive) ")" else ""
lines(
deprecationAnnotation(hints),
documentationAnnotation(hints),
obj(name, line"$Newtype_[$tpe]")(
renderId(shapeId),
renderHintsVal(hints),
Copy link
Member

Choose a reason for hiding this comment

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

(not related to this line in particular)

I think we're missing an example (and possibly an impl) for documenting structure members...

I guess this could look better if we do @param inside the structure's Scaladoc, as the lines would get quite lengthy if we just put it on the case class fields themselves. Maybe let's add this in a future PR.

In the meantime, I'd like to see an example of a documented alt case in a union as well. Ideally, every usage of documentationAnnotation(hints) would be checked with an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So should I make a DocumentationService.smithy file that has all these variants of docs on members/shapes/unions?

Copy link
Member

Choose a reason for hiding this comment

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

That works, but I meant that we need these cases to be test covered - not necessarily as part of a new spec, reusing existing ones is fine

@zetashift
Copy link
Contributor Author

zetashift commented Jan 15, 2023

RE: multiline.

I got something working with :

  private def makeDocLines(l: List[String]): List[Line] = {
    def loop(l: List[String], acc: List[Line]): List[Line] = {
      l match {
        case Nil                  => acc
        case _ :: tl if tl == Nil => loop(tl, acc :+ line"  */")
        case hd :: tl             => loop(tl, acc :+ line"  * $hd")
      }
    }
    if (l.length > 1) loop(l.tail, List(line"/** ${l.head}")) else List(line"/** ${l.head} */")

  }
  private def documentationAnnotation(hints: List[Hint]): Lines = {
    hints
      .collectFirst { case h: Hint.Documentation => h }
      .foldMap { doc =>
        val lines = doc.docString.split(System.lineSeparator()).toList
        Lines(makeDocLines(lines))
      }
  }

However, as Baccata said, the number of lines/easyness could be better with a mutable solution, what fits better here? (or is my solution overthinking it?)

EDIT: renders the following:

/** This is the first line
  * Second line of information
  */
object AString extends Newtype[String] {

@Baccata
Copy link
Contributor

Baccata commented Jan 17, 2023

@zetashift great work so far! I'm fine with the logic being implemented in terms of tailrec, it's fine.

Before we merge this in, I have a few asks :

  • I'd like for some multiline documentation to be added to some example specs (see the samples files and the examples project in sbt) so that we can have a look. The "example" project has checked-in code-generation results so that we can have a quick look at what happens when a PR changes the rendering logic.
  • I don't think @tparam or @constructor need to be generated, I do however think that @param should be generated for structures / operations (by aggregating the member documentations). So it'd probably be a good idea to change the Hint.Documentation to hold both the shape's docs and its fields when it has some.
  • Union members documentation should be generated above the Case-suffixed ADT members

@zetashift
Copy link
Contributor Author

@Baccata sounds good!(I've been using the "example" project to check out my changes locally as well, thanks to kubukoz's tips).

As for point 2 and 3, I'll try to get that working.

@kubukoz
Copy link
Member

kubukoz commented Jan 17, 2023

Fields have their own hints, is it really necessary to change the parent's?

@Baccata
Copy link
Contributor

Baccata commented Jan 17, 2023

Fields have their own hints, is it really necessary to change the parent's?

not 100% necessary, but it means def documentationAnnotation needs to be amended with a memberHints: List[List[Hint]] additional parameter and that's not looking too great

@kubukoz
Copy link
Member

kubukoz commented Jan 17, 2023

or we make an extra function that calls this after doing a local transformation. Only some callsites will actually need this

@Baccata
Copy link
Contributor

Baccata commented Jan 17, 2023

Right. I think my point should be more that the purpose of the IR is to facilitate the rendering, by pre-digesting the raw data coming from the model. If the Renderer needs to apply logic to gather some data from several layers of the IR, the IR is not doing his job very well

@kubukoz
Copy link
Member

kubukoz commented Jan 17, 2023

Fair enough.

Comment on lines +22 to +23
ObjectKey.schema.required[GetObjectInput]("key", _.key).addHints(smithy.api.Required(), smithy.api.Documentation("Sent in the URI label named \"key\".\nKey can also be seen as the filename\nIt is always required for a GET operation"), smithy.api.HttpLabel()),
BucketName.schema.required[GetObjectInput]("bucketName", _.bucketName).addHints(smithy.api.Required(), smithy.api.Documentation("Sent in the URI label named \"bucketName\"."), smithy.api.HttpLabel()),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This doesn't seem to get rendered as a scaladoc

Copy link
Member

Choose a reason for hiding this comment

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

that's the aggregating the member documentations part, right? I don't see this having been implemented yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah yes this should be rendered as @constructor arguments to GetObjectInput I believe, and I have not started yet with rendering those type of things

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, so the correct way to go at it would be for Hint.Documentation type to change to :

case class Documentation(docString: String, memberDocs: ListMap[String, String]) extends Hint

so that you'd be capturing the documentation of the members during the creation of the intermediate representation, and using it where it makes sense (ie, documentation on structures for now)

Copy link
Contributor Author

@zetashift zetashift Jan 20, 2023

Choose a reason for hiding this comment

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

Thank you for the hint(pun not intended)! I understand that we want to capture the doc of the members (if there is any) in the IR. But how do I fill memberDocs in the SmithyToIR.scala file?

    case doc: DocumentationTrait =>
      Hint.Documentation(doc.getValue(), )

I've looked around a bit, and I cannot see where DocumentationTrait provides a function to get the member shapes. Do I have to make that logic myself?

Copy link
Member

Choose a reason for hiding this comment

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

that private def hints(shape: Shape) is kinda useless now. I only left it in because it has way too many usages... I guess you could remove it, rename shapeHints to hints and call it as this.hints wherever hints is taken.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I see! I'll get to this weekend! :D, that clears a few things up.
After that I'll work on the escaping of docstrings.
And hopefully then I can undraft this PR.

Copy link
Member

Choose a reason for hiding this comment

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

sounds great. No pressure ;)

Copy link
Contributor Author

@zetashift zetashift Jan 21, 2023

Choose a reason for hiding this comment

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

rename shapeHints to hints and call it as this.hints wherever hints is taken.

Do I rename it this.hints? The function hints isn't part of ShapeVisitor but SmithyIR
So (from other functions) this seems to work:

      private def getDefault(shape: Shape): Option[Decl] = {
        val _hints = hints(shape)

Copy link
Member

Choose a reason for hiding this comment

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

if I'm understanding it right, and ShapeVisitor is nested in SmithyIR, you could still call it as SmithyIR.this.hints... but I'm kinda too lazy to try it out in this given I'd have to repeat that diff above 😂

just push what you have, we can iterate on that :)

Comment on lines +29 to +31
/** this is a comment saying you should be careful for this case
* you never know what lies ahead with Strings like this
*/
Copy link
Member

Choose a reason for hiding this comment

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

I'm cracking up 😂

modules/example/src/smithy4s/example/FooService.scala Outdated Show resolved Hide resolved
Comment on lines 741 to 751
case doc: DocumentationTrait =>
val memberDocs = shape
.members()
.asScala
.map(_.getTarget())
.map(shapeId => (shapeId.name, model.expectShape(shapeId)))
.map({ case (name, shape) =>
(name, shape.getTrait(classOf[DocumentationTrait]).asScala)
})
.collect({ case (name, Some(v)) => (name, v.getValue()) })
.toMap
Copy link
Contributor Author

Choose a reason for hiding this comment

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

memberDocs seems to always come up as an empty Map, and I don't know why exactly :S

Copy link
Member

Choose a reason for hiding this comment

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

I'll take a look

Copy link
Member

Choose a reason for hiding this comment

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

yeah so I think it's just that we were looking at the target type's docs, see https://github.com/disneystreaming/smithy4s/pull/731/files#r1083577898

I pushed a commit to fix that, also managed to squeeze in a couple cleanups (e.g. got rid of the tailrec function in favor of mkString): d3234c7

Copy link
Member

Choose a reason for hiding this comment

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

realized the linesIterator can be empty, so another one... 0b3bf43

.members()
.asScala
.map(_.getTarget())
.map(shapeId => (shapeId.name, model.expectShape(shapeId)))
Copy link
Member

Choose a reason for hiding this comment

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

I think this shapeId.name should be the member name instead. Can't see what this outputs as, but I feel like that's going to be a different thing (e.g. in foo: String you'll see String instead of foo).

Comment on lines 741 to 751
case doc: DocumentationTrait =>
val memberDocs = shape
.members()
.asScala
.map(_.getTarget())
.map(shapeId => (shapeId.name, model.expectShape(shapeId)))
.map({ case (name, shape) =>
(name, shape.getTrait(classOf[DocumentationTrait]).asScala)
})
.collect({ case (name, Some(v)) => (name, v.getValue()) })
.toMap
Copy link
Member

Choose a reason for hiding this comment

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

I'll take a look

@@ -738,19 +738,6 @@ private[codegen] class SmithyToIR(model: Model, namespace: String) {
Hint.PackedInputs
case d: DeprecatedTrait =>
Hint.Deprecated(d.getMessage.asScala, d.getSince.asScala)
case doc: DocumentationTrait =>
Copy link
Member

Choose a reason for hiding this comment

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

Moved this out to a separate function to make it easier to get member docs even if the shape itself has no doc trait.

.collect { case (name, Some(v)) => (name, v.getValue()) }
.toMap

// todo: what if the shape in question doesn't have docs, but members do?
Copy link
Member

Choose a reason for hiding this comment

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

I think we'll have to change the Hint.Documentation class to account for the shape not having docs. Just having two Options in it might be enough, or a Ior[ParentDocs, MemberDocs]

@kubukoz
Copy link
Member

kubukoz commented Jan 23, 2023

@zetashift I may've made a little mess in these commits, if it gets out of hand let me know and I can get this over the finish line. I guess at this point we'll need to:

@zetashift
Copy link
Contributor Author

@kubukoz I'll definitely try to get this over the finish line myself!

Two Options for Hint.Documentation sounds good to me, I'm not familiar with the Ior datatype.

Comment on lines 189 to 197
.flatMap { doc =>
val shapeLinesOpt = doc.docString.map(split(_)).map(_.toList)
val memberLinesOpt = doc.memberDocs.map(m => m.flatMap {
case (memberName, memberDoc) =>
split(memberDoc) match {
case Some(NonEmptyList(head, rest)) =>
s"@param $memberName $head" :: rest
case None => Nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

help, I'm losing the game of aligning types! :P

No but seriously, I'm having trouble getting the docs in a List as List(shapeLinesOpt, memberLinesOpt).flatten.flatten, as in I don't know how to get unnest these options(shapeDocs and memberDocs) using map or flatMap correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

pro-tip: you shouldn't have to : the lines() dsl method is there to flatten stuff for you, no matter the level of nesting. The code should roughly look like :

hints.collectFirst { .... }.foldMap { doc => 
   lines(
      doc.docString.flatMap(split), 
      doc.memberDocs.combineAll.foldMap {
           case (memberName, memberDoc) => //... 
      } 
   ) 
}

@zetashift
Copy link
Contributor Author

I'm trying to apply @Baccata suggestions of pre-splitting, and it was working out fine, but memberDocLines is a Map[String,List[String]] and but I'm still having trouble type checking it.

Ignoring the fact that my fp-fu is failing, I do think it might better to have Options(or something equivalent) in Hint.Documentation for indicating that shape docs or member docs might be absent

@Baccata
Copy link
Contributor

Baccata commented Jan 26, 2023

PR'ing against your PR : https://github.com/zetashift/smithy4s/pull/1

@zetashift
Copy link
Contributor Author

zetashift commented Jan 26, 2023

So I believe that only leaves this point:

  • Union members documentation should be generated above the Case-suffixed ADT members

Which looks good to me, anything I'm missing here :O?

Currently union shapes are rendered like this:
image

EDIT: nope nvm the StrCase doc is duplicated into the Foo shapes docs

@zetashift zetashift marked this pull request as ready for review January 26, 2023 23:41
@@ -701,8 +701,13 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self =>
val caseNamesAndIsUnit =
caseNames.zip(alts.map(_.member == UnionMember.UnitCase))

val shapeDocs = hints
Copy link
Member

Choose a reason for hiding this comment

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

question: wait, how does this change actually help? I don't see anything union-related in the diff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Foo.scala union gets its @param str doc comment removed. It should be removed because the doc comment should be placed on case class StrCase
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(sorry for the screenshot, I don't know how to do a fancy diff block :P)

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with the reasoning, but transformation is something that should happen in SmithyToIR, let's move it over there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've been pondering over how I would do that, without it getting a bit funky.
I think that would require that Documentation#shapeDocs would be a Hint[Documentation] rather than a List[String] no?

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually can't figure out how to transform a Option[DocumentationTrait] into a Hint.Documentation where shapeDocs is a Hint.Documentation as well:

    shape.getTrait(classOf[DocumentationTrait]).asScala.map { doc =>
      val shapeDocs = Hint.Documentation(split(doc.getValue()), Map()) // Can't chuck in a List[String] in shapeDocs anymore
      Hint.Documentation(shapeDocs, memberDocs)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do I just introduce a new type, like Hint.ShapeDocumentation?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've given it some thoughts, and I think the most straightforward way is to keep the types as they are, but change this line to add a condition like if(shape.isUnion) Map.empty else ..., that'll be sufficient

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So simple, love it! :P

Almost makes me think that it's too simple to work.

/** Returns a useful Foo
* No input necessary to find our Foo
* The path for this operation is "/foo"
*/
Copy link
Member

Choose a reason for hiding this comment

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

question: would this also include parameter docs? I don't think we have an example of that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With parameter docs here you mean an input on an operation for example? I could chuck a doc comment on PutObjectin example.smithy

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's defer this to a later PR, as there is a little bit of nuance to it with regards to packed inputs.

Copy link
Contributor

@Baccata Baccata left a comment

Choose a reason for hiding this comment

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

Awesome work @zetashift, thank you so much for this contribution !

@Baccata Baccata dismissed kubukoz’s stale review January 30, 2023 10:04

concern was addressed

@Baccata Baccata merged commit 93122ca into disneystreaming:series/0.17 Jan 30, 2023
@zetashift
Copy link
Contributor Author

Thank you and @kubukoz very much for all the guidance and help! Some of this stuff I would never come up with on my own

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants