-
Notifications
You must be signed in to change notification settings - Fork 69
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
Generate scaladoc for @documentation
trait
#731
Conversation
There was a problem hiding this 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 */
. :)
So another random question popped up. Do we expect things like |
I think that's not necessary at this stage... but |
@@ -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), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
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] { |
@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 :
|
@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. |
Fields have their own hints, is it really necessary to change the parent's? |
not 100% necessary, but it means |
or we make an extra function that calls this after doing a local transformation. Only some callsites will actually need this |
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 |
Fair enough. |
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()), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds great. No pressure ;)
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 :)
/** this is a comment saying you should be careful for this case | ||
* you never know what lies ahead with Strings like this | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm cracking up 😂
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
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
).
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 |
There was a problem hiding this comment.
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 => |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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 Option
s in it might be enough, or a Ior[ParentDocs, MemberDocs]
@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:
|
@kubukoz I'll definitely try to get this over the finish line myself! Two |
.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 | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) => //...
}
)
}
I'm trying to apply @Baccata suggestions of pre-splitting, and it was working out fine, but Ignoring the fact that my fp-fu is failing, I do think it might better to have |
PR'ing against your PR : https://github.com/zetashift/smithy4s/pull/1 |
Fix compilation and tweak rendering
@@ -701,8 +701,13 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => | |||
val caseNamesAndIsUnit = | |||
caseNames.zip(alts.map(_.member == UnionMember.UnitCase)) | |||
|
|||
val shapeDocs = hints |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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" | ||
*/ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 PutObject
in example.smithy
There was a problem hiding this comment.
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.
There was a problem hiding this 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 !
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 |
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: