Monday, August 2, 2021

Leaving a Legacy

“The large print giveth, and the small print taketh away.” 

Tom Waits

META: This essay is accompanied by a GitHub Repo, containing a sample Xcode project. We will link to tags in that repo, to denote various steps in our discussion.

We will also have a directory containing generated documentation. It, too, will have multiple revisions.

Additionally, this story is also published on Medium.

Why Document?

There is a definite school of thought, that says “If I document my code, I’ll be out of a job.”

I don’t know about anyone else, but I have no intentions of spending the rest of my life, working on a jalopy. I want to learn new stuff, and get myself into trouble, exploring new horizons.

If I deliberately make it difficult for others; especially those with less experience than I, to take over and understand my code, I can look forward to unclogging carburetors, when the world has moved on to fuel injection or electric cars.

Also, I feel a certain amount of pride in my work. I think I do a fairly good job, and I WANT people to see what I do, and understand how I do it.

In The Stone Age

I didn’t start off as a software developer. My first experiences with computers were first, as an electronic technician, then, as an electronic engineer.

Because of this, I was pretty insecure, starting off with this new-fangled “software” stuff. I spent a lot of time, looking around, and seeing how other people did it.

One thing that I very clearly remember, was the amount of stress placed on clearly documented code.

Back in those days, the code was pretty primitive. We had languages like BASIC, FORTRAN, COBOL and Assembly. C was just beginning to show signs of being the massive success that it eventually became.

I started with Machine Code, Assembly and BASIC.

We can’t really document Machine Code; except in the development stage, as there isn’t actual source code. It’s a bunch of numbers that we enter into a processor’s execution heap. We can document the pad of paper, where the codes are calculated (Back then, we would work out machine code on a pad of paper; not a text editor).

Documenting Every Line

Assembly, on the other hand, is painstakingly documented.

Since an Assembly program is really just a linear series of simple steps (for example, we can’t just say “Set the value of Variable B to the value of Variable A.” We need to say “Move the value pointed to by the address of Variable A into Register A, then move the value in register A into the memory location addressed by the pointer for Variable B”), it means that we can attach a simple, one-sentence comment to every line of ASM code, like so:

0045    3A943E          LDA     Byte5           ; Get the comm. lamp byte of the map
0048    3E01            ANI     Hpibon          ; See if the light is already on
004A    C25700          JNZ     Go_0            ; Skip the rest if it's on

004D    21943E          LXI     H,Byte5         ; Get the address of the bit map byte 6
0050    3E01            MVI     A,Hpibon        ; Get the mask to turn the HPIB light on
0052    B6              ORA     M               ; OR it with the contents of the byte
0053    77              MOV     M,A             ; Re-store the result

                    •
                    •
                    •

020A    21943E  Fploff  LXI     H,Byte5         ; Get the address of the bit map byte 6
020D    3E03            MVI     A,Serlon+Hpibon ; Get masks for lights on/off
020F    2F              CMA                     ; Complement it
0210    A6              ANA     M               ; AND it back with what's already there
0211    77              MOV     M,A             ; Put it back where it came from

That code comes directly from my very first engineering project. It was Intel 8085 code, from 1987.

All of that code was there to turn on, and then off again, an indicator light, if there was any interaction on an IEE-488 interface. This was how we got a little blinking light, as data flowed to and from the device. When the data processing would start, we’d turn on the light (the first block of code), and after the data was processed, we’d call a routine to force the light off (the second block of code).

My BASIC, and early C code, was almost as “granular,” so my earlier programs tended to have pedantic, each-line-has-a-comment documentation.

Lessons Learned

One of my first software projects was as a maintenance programmer for over 100,000 lines of 1970s-vintage FORTRAN IV code that had been stepped on by just about every new programmer since it was written, and had ZERO documentation.

Pretty much the Platonic ideal of “spaghetti code.” That was a lesson that stayed with me to this day. I never want to subject anyone else to that.

Today’s Tools

These days, a number of factors influence how I do my documentation, and that’s what I’m really going to cover in this post. I just wanted to do a brief “I had to walk to school, uphill, in the snow, in July” sidebar, to establish some context.

Before we get into matters of procedure, I’d like to talk about the tools that I use, when creating project documentation for my work.

Markdown READMEs

The type of documentation that most people are familiar with, these days, is the Markdown README file.

The most well-known implementation of the Markdown README, is the GitHub implementation, although pretty much every repository system now does it.

The way this works, is that if we have a file in a directory of source code, called “README,” or “README.md,” the site engine will render that as Markdown. This allows us to have a fairly nicely-formatted display, while also having very understandable “source code” of the markdown file.

As a basic rule of thumb, every project that I do has a README.md file in its root directory. If there are subdirectories that I think need extra documentation, I will toss a README.md into them, as well. Any directory that has a README.md file in it will be rendered by GitHub (or other repo systems).

Here are some examples.

When we go to the GitHub page, and scroll down, we will see the README.md file, rendered:

Here is a project that is available in GitHub, and as a CocoaPod:

Note that the README.md renders in both repos. Also note that CocoaPods renders a CHANGELOG.md file into a separate page. That’s useful.

The README can be quite robust; including images and code samples. If I am including images, it will reference images relative to the README, in the same directory. However, I may need to do absolute URIs for images, when I start getting fancier (more on that, in a bit).

Commit Comments

This may seem like a “no-brainer,” but with version control systems, we have another place that we can add documentation: the comment that we supply when committing a new code change.

I have seen people add such helpful comments as “asdf,” or “qwerty,” but this is a place that we can add some highly relevant and timely documentation. I’ll get into that more, in a bit.

Doxygen

Doxygen is a mature, well-maintained “autodoc” generator. It is maintained by a Dutch programmer, and is very powerful.

It uses “Javadoc”-style header comments. Lately, it has also started to support Markdown.

I’ll get into the particulars of how I support it, in my coding.

Apple Inline Markdown

HeaderDoc was Apple’s implementation of JavaDoc. Note the “was.” It has been retired, in favor of a Markdown-based documentation system. I’ll get into that, in a bit. It’s pretty cool. I like it a lot more than HeaderDoc.

Jazzy

Jazzy is a very cool Ruby utility, written by the folks at realm.io. I use it for all of my Apple documentation. It works in conjunction with Apple Inline Markdown.

There are more tools, but these are the ones that I use.

What Needs to be Documented, and How

As I said, in the early days, I’d document every line of code, but that’s not only overkill; it risks having documentation go out of date, which is worse.

In the following discussions, I will use examples from the Swift Programming Language, as that is the language that I work with the most. It’s also a modern, powerful language that has a number of features and capabilities that I can exercise.

Why Vs. What

I’ve come to realize that the most important inline documentation concerns WHY we are doing something; not WHAT we are doing.

For example, no one wants to read “// Set the value of b to 3,” for a line of code that looks like let b = 3. That’s just dumb.

let b = 3 // Set the value of b to 3

However, they may want to know “// Set the value of b to the number of iterations we'll be making.”

let b = 3 // Set the value of b to the number of iterations we'll be making

Clear Naming

Having a consistent naming convention is important. Most corporations that have style guides, include naming conventions in that guide.

For example, here is Apple’s naming guide.

Here’s one of Google’s.

They have a “philosophical underpinning” to their names. I try to do the same, for my projects.

Let’s start with property/variable names. First, this is not really too helpful:

let b = 3 // Set the value of b to the number of iterations we'll be making

b” doesn’t make any sense; especially since we are talking about an iteration maximum index. This would be better named “iterationCount” or “maxIterations“:

let iterationCount = 3 // This is the number of iterations we'll be making

With a good name, we could probably do away with the comment entirely:

let numberOfIterations = 3

Nowadays, I want to reduce the number of comments, and I can do that with a combination of clear naming conventions, and straightforward programming.

Let’s look at an example of a block of code that iterates through a collection of integers, and adds a value to them. This is a fairly basic (and common) type of operation:

Example.1

let ofst = 5

let incArInt = [1, 2, 3, 4, 5].map { $0 + ofst }

let valAr = incArInt

Now that is what is referred to as “idiomatic Swift,” or Swift that is expressed in a “native” manner.

To an experienced Swift programmer, it’s immediately apparent what’s happening.

  1. An offset value of 5 is being set to an Int constant, using a numeric literal.
  2. An Array literal of Int is being mapped (using the map() higher-order function), and a new Array of Int is being returned.
  3. This new Array will be the Array literal value, with the offset added to each of its members.

If we wanted to be REAL shorthand, we could make the offset a literal, as well:

let incArInt = [1, 2, 3, 4, 5].map { $0 + 5 }

But then, why don’t we just do this?

let incArInt = [6, 7, 8, 9, 10]

Which, of course, defeats the entire purpose of this discussion. Let’s just pretend that we’ll be using the offset in the future. I’m leaving the Array literal in there to help me make a point.

Making the Code Accessible

Now, one thing that we need to keep in mind, is who will be cleaning up our mess?

If we know that whoever comes after us will be an experienced Swift programmer, then using idiomatic Swift is a no-brainer. It’s more efficient, and they will understand it quickly.

That said, my experience is that maintenance programmers tend to be some of the more junior staffers. They may well be a JavaScript programmer that just learned the basics of Swift, two months ago.

In that case, idiomatic Swift may be good for teaching them, but maybe not so good, if we want them to find and fix bugs, or add features to an existing codebase.

One way to deal with that, is rewrite the idiomatic Swift into a form they may find more familiar:

Example.2

let integerOffset: Int = 5

let currentValuesIntegerArray: [Int] = [1, 2, 3, 4, 5]

var workingArray: [Int] = []

for value in currentValuesIntegerArray {
    let tempValue: Int = value + integerOffset
    workingArray.append(tempValue)
}

let newIntegerArray: [Int] = workingArray

That is, quite literally, self-documenting code, but it’s not very good, if our goal is to help train the maintenance programmer to become a more advanced Swift programmer. It’s basically simple, lowest-common-denominator JavaScript, written with Swift.

Code Comments to the Rescue

So a “middle ground,” might be to make the naming a bit less opaque, and add some comments to help the programmer understand and learn:

Example.3

// The current index table of lines.
var textLineIndexes = [1, 2, 3, 4, 5]

// The number of lines we will be inserting into the text.
let insertLineCount = 5

let addOffsetFunction = {(currentValue) in
    return currentValue + insertLineCount
}

// We use the Array.map() function to add the offset, as it's the most effective Swift procedure.
textLineIndexes = textLineIndexes.map(addOffsetFunction)

In the above example, I made it a bit more “real world,” by indicating that the Array is an Array of text lines in a file, and that we will be adding some text lines to the file (I’ll be expanding upon this).

Note that the comments are fairly terse. I could have said “This is a table of indexes into the text file.", but instead, I said "The current index table of lines.".

Also take note that I mention why we use the map() function to add the offset. This is a nod towards the training. An experienced Swift programmer would not need to be told this, but a novice one, maybe without the knowledge of Swift’s implementation of Higher-Order Functions, might need it.

If we wanted to be a bit more “idiomatic,” we could write that last line, like so:

Example.4

textLineIndexes = textLineIndexes.map { $0 + insertLineCount }

Which implements Swift’s trailing closure shorthand.

Commit Comments

When we submit code to a version control repository, we can add some extremely relevant documentation in the code commit.

Commit Often

One thing that I try to do, is commit often, so there aren’t a whole ton of changes in each commit, and the comment can be quite focused, like “Fixes Bug #1234. The Jupiter rings were out of alignment. I banged on them a couple of times with a monkey wrench, and they went back into synchronization.

Ozymandias

In the following examples, I’ll be riffing off the whole “leave a legacy” trope, and use the famous Shelley poem “Ozymandias” as a dataset.

We’ll set up a simple function that just appends one Array of String to another, using linefeeds, and we’ll establish a couple of simple Arrays of String to hold the poem:

Example.5

func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String {
    return  (to + inNewTextLineArray).joined(separator: "\n")
}

let originalVersionPartOne = ["I met a traveller from an antique land",
"Who said: Two vast and trunkless legs of stone",
"Stand in the desert. Near them, on the sand,",
"Half sunk, a shattered visage lies, whose frown,",
"And wrinkled lip, and sneer of cold command,",
"Tell that its sculptor well those passions read",
"Which yet survive, stamped on these lifeless things,",
"The hand that mocked them and the heart that fed:",
"And on the pedestal these words appear:",
"\"My name is Ozymandias, king of kings:",
"Look on my works, ye Mighty, and despair!\""]

let originalVersionPartTwo = [
"Nothing beside remains. Round the decay",
"Of that colossal wreck, boundless and bare",
"The lone and level sands stretch far away."]

var fullPoem = appendTextLines(originalVersionPartTwo, to: originalVersionPartOne)

print(fullPoem)

This will print the following:

I met a traveller from an antique land
Who said: Two vast and trunkless legs of stone
Stand in the desert. Near them, on the sand,
Half sunk, a shattered visage lies, whose frown,
And wrinkled lip, and sneer of cold command,
Tell that its sculptor well those passions read
Which yet survive, stamped on these lifeless things,
The hand that mocked them and the heart that fed:
And on the pedestal these words appear:
"My name is Ozymandias, king of kings:
Look on my works, ye Mighty, and despair!"
Nothing beside remains. Round the decay
Of that colossal wreck, boundless and bare
The lone and level sands stretch far away.

This is all fairly straightforward, and we can easily tell what’s happening by just looking at the code, but let’s pretend that we can’t.

First, Document the Function

I use the Doxygen/Apple Markdown syntax for documenting my functions, and I ALWAYS DOCUMENT MY FUNCTIONS.

We will often see comment blocks in code documentation that look like this:

/// This is a Function.
/// Functions do stuff.
/// This function does stuff.
func functionThatDoesStuff() { }

Or like this:

/**
    This is a Function.
    Functions do stuff.
    This function does stuff.
 */
func functionThatDoesStuff() { }

What we should note, is the use of “///” or “/**“. The third forward-slash, or second asterisk, indicate that the following comment should be parsed for auto-documentation. If these are not present, Doxygen, Jazzy or the Apple Markdown Parser will ignore the comment.

I take it one step farther. I also like to add a “visual demarkation” key. Namely, a line of characters that provide a visual queue, telling a fast-scrolling reader that a function (or data structure) is there. I tend to use rows of hash signs or asterisks, like so:

/* #################################################### */

/**
    This is a Function.
    Functions do stuff.
    This function does stuff.
*/
func functionThatDoesStuff() { }

That line has no practical purpose. It is not parsed by the doc generator, and adds yet another line to the file.

But it provides a visual queue, saying “Herein lies a function/method.” As I often scroll through files fairly quickly, these are invaluable. They help me to avoid searching around a page of code and bracketed contexts, looking for the function declaration.

So, with that in mind, let’s add a function header to our simple append function:

Example.6

/* #################################################### */
/**
 This function will append one Array of String to another, then
 will return the entire composite Array as a single String, joined by linefeeds.
 */
func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String {
    return  (to + inNewTextLineArray).joined(separator: "\n")
}

This gives us the “visual break,” and also explains what the function does.

Adding Autodoc Header Elements

With Doxygen and the Apple Markdown Syntax, we can format that function header documentation to key a parser.

It’s a bit beyond this essay to describe the exact syntax of these queues, but here is the Doxygen manual, and here is a great description of the Apple system.

In my examples, I will be using Swift, and the Apple Markdown syntax (which can also be parsed by Doxygen).

Documenting Function Arguments/Parameters

A nice thing to do, is to document each parameter, so the autodoc system will display them as individual blocks.

The Apple-prescribed way, is to create Markdown for lists, and to use the “parameter” keyword to preface each description, like so:

Example.7

/* #################################################### */
/**
 This function will append one Array of String to another, then
 will return the entire composite Array as a single String, joined by linefeeds.
 - parameter inNewTextLineArray: The Array of String that will be appended to the end of the Array.
 - parameter to: The Array of String that will precede the previous Array.
 */
func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String {
    return  (to + inNewTextLineArray).joined(separator: "\n")
}

If we then select the function name, while the Quick Help Inspector is open in the right sidebar, we will see:

In a bit, I’ll show how Jazzy will document that (It’s pretty cool).

But we’re not done, yet…

Document the Function Return

We can also have some markdown that will define what the function returns, and will also allow us to refine the basic “blurb,” as well:

Example.8

/* #################################################### */
/**
 This function will append one Array of String to another, returning a simple, joined String.
 
 - parameter inNewTextLineArray: The Array of String that will be appended to the end of the Array.
 - parameter to: The Array of String that will precede the previous Array.
 
 - returns: The entire composite Array as a single String, joined by linefeeds.
 */
func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String {
    return  (to + inNewTextLineArray).joined(separator: "\n")
}

Which gives us:

And finally, just to make ourselves consistent with Apple’s recommendation (which I sometimes follow, sometimes, not):

Example.9

/* #################################################### */
/**
 This function will append one Array of String to another, returning a simple, joined String.
 
 - parameters:
    - inNewTextLineArray: The Array of String that will be appended to the end of the Array.
    - to: The Array of String that will precede the previous Array.
 
 - returns: The entire composite Array as a single String, joined by linefeeds.
 */
func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String {
    return  (to + inNewTextLineArray).joined(separator: "\n")
}

That’s a great way to leave a legacy for programmers to follow.

I have been doing this for so long, that it’s basically second nature. When I create a function/method, I immediately add an empty header, like so:

/* #################################################### */
/**
 */
func someFunc() { }

At minimum, even if I never go back and fill out the header, it gives that “visual queue,” so I will know there’s a function declaration there.

Making Our Mark

Most languages have some variation of the “MARK” comment. This is a queue for text editors (and autodoc generators) to insert some kind of “break.”

I tend to just use the “MARK” comment in my Swift work, as it’s the most flexible and robust (I also run all warnings as errors, so I don’t like code comments that generate warnings, but that’s another story).

To show how we might use the MARK comment, let’s add another version of the poem:

Example.10

/* #################################################### */
/**
 This function will append one Array of String to another, returning a simple, joined String.
 
 - parameters:
    - inNewTextLineArray: The Array of String that will be appended to the end of the Array.
    - to: The Array of String that will precede the previous Array.
 
 - returns: The entire composite Array as a single String, joined by linefeeds.
 */
func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String {
    return  (to + inNewTextLineArray).joined(separator: "\n")
}

let originalVersionPartOne = ["I met a traveller from an antique land",
"Who said: Two vast and trunkless legs of stone",
"Stand in the desert. Near them, on the sand,",
"Half sunk, a shattered visage lies, whose frown,",
"And wrinkled lip, and sneer of cold command,",
"Tell that its sculptor well those passions read",
"Which yet survive, stamped on these lifeless things,",
"The hand that mocked them and the heart that fed:",
"And on the pedestal these words appear:",
"\"My name is Ozymandias, king of kings:",
"Look on my works, ye Mighty, and despair!\""]

let originalVersionPartTwo = ["Nothing beside remains. Round the decay",
"Of that colossal wreck, boundless and bare",
"The lone and level sands stretch far away."]

var fullPoem = appendTextLines(originalVersionPartTwo, to: originalVersionPartOne)

print(fullPoem + "\n")

let horaceSmithVersionPartOne = ["In Egypt's sandy silence, all alone,",
"Stands a gigantic Leg, which far off throws",
"The only shadow that the Desert knows:—",
"\"I am great OZYMANDIAS,\" saith the stone,",
"\"The King of Kings; this mighty City shows",
"The wonders of my hand.\"— The City's gone,—",
"Naught but the Leg remaining to disclose",
"The site of this forgotten Babylon."]

let horaceSmithVersionPartTwo = ["We wonder,—and some Hunter may express",
"Wonder like ours, when thro' the wilderness",
"Where London stood, holding the Wolf in chace,",
"He meets some fragment huge, and stops to guess",
"What powerful but unrecorded race",
"Once dwelt in that annihilated place."]

fullPoem = appendTextLinesappendTextLines(horaceSmithVersionPartTwo, to: horaceSmithVersionPartOne)

print(fullPoem)

Which prints:

I met a traveller from an antique land
Who said: Two vast and trunkless legs of stone
Stand in the desert. Near them, on the sand,
Half sunk, a shattered visage lies, whose frown,
And wrinkled lip, and sneer of cold command,
Tell that its sculptor well those passions read
Which yet survive, stamped on these lifeless things,
The hand that mocked them and the heart that fed:
And on the pedestal these words appear:
"My name is Ozymandias, king of kings:
Look on my works, ye Mighty, and despair!"
Nothing beside remains. Round the decay
Of that colossal wreck, boundless and bare
The lone and level sands stretch far away.

In Egypt's sandy silence, all alone,
Stands a gigantic Leg, which far off throws
The only shadow that the Desert knows:—
"I am great OZYMANDIAS," saith the stone,
"The King of Kings; this mighty City shows
The wonders of my hand."— The City's gone,—
Naught but the Leg remaining to disclose
The site of this forgotten Babylon.
We wonder,—and some Hunter may express
Wonder like ours, when thro' the wilderness
Where London stood, holding the Wolf in chace,
He meets some fragment huge, and stops to guess
What powerful but unrecorded race
Once dwelt in that annihilated place.

This looks like an opening to mark the two different versions:

Example.11

/* #################################################### */
/**
 This function will append one Array of String to another, returning a simple, joined String.
 
 - parameters:
    - inNewTextLineArray: The Array of String that will be appended to the end of the Array.
    - to: The Array of String that will precede the previous Array.
 
 - returns: The entire composite Array as a single String, joined by linefeeds.
 */
func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String {
    return  (to + inNewTextLineArray).joined(separator: "\n")
}

// MARK: Shelley Version

let originalVersionPartOne = ["I met a traveller from an antique land",
"Who said: Two vast and trunkless legs of stone",
"Stand in the desert. Near them, on the sand,",
"Half sunk, a shattered visage lies, whose frown,",
"And wrinkled lip, and sneer of cold command,",
"Tell that its sculptor well those passions read",
"Which yet survive, stamped on these lifeless things,",
"The hand that mocked them and the heart that fed:",
"And on the pedestal these words appear:",
"\"My name is Ozymandias, king of kings:",
"Look on my works, ye Mighty, and despair!\""]

let originalVersionPartTwo = ["Nothing beside remains. Round the decay",
"Of that colossal wreck, boundless and bare",
"The lone and level sands stretch far away."]

var fullPoem = appendTextLines(originalVersionPartTwo, to: originalVersionPartOne)

print(fullPoem + "\n")

// MARK: Smith Version

let horaceSmithVersionPartOne = ["In Egypt's sandy silence, all alone,",
"Stands a gigantic Leg, which far off throws",
"The only shadow that the Desert knows:—",
"\"I am great OZYMANDIAS,\" saith the stone,",
"\"The King of Kings; this mighty City shows",
"The wonders of my hand.\"— The City's gone,—",
"Naught but the Leg remaining to disclose",
"The site of this forgotten Babylon."]

let horaceSmithVersionPartTwo = ["We wonder,—and some Hunter may express",
"Wonder like ours, when thro' the wilderness",
"Where London stood, holding the Wolf in chace,",
"He meets some fragment huge, and stops to guess",
"What powerful but unrecorded race",
"Once dwelt in that annihilated place."]

fullPoem = appendTextLines(horaceSmithVersionPartTwo, to: horaceSmithVersionPartOne)

print(fullPoem)

This results in the code menu at the top (of the Xcode editor) displaying the two MARKs:

These use the text defined after the colon in each MARK comment:

// MARK: Shelley Version
                   ·
                   ·
                   ·
// MARK: Smith Version

If we select one of these, we will be taken to that line in the source file:

I like to add dashes, which result in visible separator lines in the menu:

Example.12

// MARK: - Shelley Version
                   ·
                   ·
                   ·
// MARK: - Smith Version

Getting Jazzy

OK. All that build-up was to get us to this point.

Basic Jazz

I use Jazzy to auto-generate my documents.

Jazzy is a Ruby gem that we need to first install on our machine. The instructions for doing this are here. It’s pretty straightforward (For now. I understand that Apple will stop bundling Ruby soon, which means that we may need to install Ruby, as well as Jazzy).

Once installed, we need to invoke Jazzy on the command line. We do this by opening Terminal, navigating (cd command) to the project directory, and then simply invoking “jazzy“, like so:

$ cd ~/Desktop/Ozymandias 
$ jazzy

And then we invoke Jazzy on the command line, like so:

Running xcodebuild
Parsing main.swift (1/1)
0% documentation coverage with 0 undocumented symbols
skipped 6 private, fileprivate, or internal symbols (use `--min-acl` to specify a different minimum ACL)
building site
building search index
jam out ♪♫ to your fresh new docs in `docs`

It will churn away, and then result in a new directory at the same level as the project file, called “docs.” This will be an HTML directory, and will have a file, called “index.html“. That file will open as a Web page, which won’t have anything useful to say for itself.

That’s because of this line:

skipped 6 private, fileprivate, or internal symbols (use `--min-acl` to specify a different minimum ACL)

The default permission scheme for Jazzy, is that it only documents public or open declarations, and everything in our example is considered internal.

We have two ways to deal with this:

  1. We declare stuff public
  2. We use “--min-acl internal” as an argument (“jazzy --min-acl internal“).

Let’s start with #2:

$ jazzy --min-acl internal
Running xcodebuild
Checking xcodebuild -showBuildSettings
Assuming New Build System is used.
Parsing main.swift (1/1)
16% documentation coverage with 5 undocumented symbols
included 6 internal, public, or open symbols
building site
building search index
jam out ♪♫ to your fresh new docs in `docs`

This gives us a not-particularly-useful doc dump.

Note this line:

16% documentation coverage with 5 undocumented symbols

That says that we only have 16% of our potential fulfilled.

If we look in the “docs” directory, we will see a file called “undocumented.json“. This file tells us what we’re missing out on (Ignore the paths. They are for my personal machine, and won’t be there in anyone else’s implementation):

{
  "warnings": [
    {
      "file": "/Users/chrismarshall/Desktop/Ozymandias/Ozymandias/main.swift",
      "line": 39,
      "symbol": "originalVersionPartOne",
      "symbol_kind": "source.lang.swift.decl.var.global",
      "warning": "undocumented"
    },
    {
      "file": "/Users/chrismarshall/Desktop/Ozymandias/Ozymandias/main.swift",
      "line": 51,
      "symbol": "originalVersionPartTwo",
      "symbol_kind": "source.lang.swift.decl.var.global",
      "warning": "undocumented"
    },
    {
      "file": "/Users/chrismarshall/Desktop/Ozymandias/Ozymandias/main.swift",
      "line": 55,
      "symbol": "fullPoem",
      "symbol_kind": "source.lang.swift.decl.var.global",
      "warning": "undocumented"
    },
    {
      "file": "/Users/chrismarshall/Desktop/Ozymandias/Ozymandias/main.swift",
      "line": 61,
      "symbol": "horaceSmithVersionPartOne",
      "symbol_kind": "source.lang.swift.decl.var.global",
      "warning": "undocumented"
    },
    {
      "file": "/Users/chrismarshall/Desktop/Ozymandias/Ozymandias/main.swift",
      "line": 70,
      "symbol": "horaceSmithVersionPartTwo",
      "symbol_kind": "source.lang.swift.decl.var.global",
      "warning": "undocumented"
    }
  ],
  "source_directory": "/Users/chrismarshall/Desktop/Ozymandias"
}

That is telling us that the following lines can be documented, but are not:

let originalVersionPartOne = ["I met a traveller from an antique land",
"Who said: Two vast and trunkless legs of stone",
"Stand in the desert. Near them, on the sand,",
"Half sunk, a shattered visage lies, whose frown,",
"And wrinkled lip, and sneer of cold command,",
"Tell that its sculptor well those passions read",
"Which yet survive, stamped on these lifeless things,",
"The hand that mocked them and the heart that fed:",
"And on the pedestal these words appear:",
"\"My name is Ozymandias, king of kings:",
"Look on my works, ye Mighty, and despair!\""]

let originalVersionPartTwo = ["Nothing beside remains. Round the decay",
"Of that colossal wreck, boundless and bare",
"The lone and level sands stretch far away."]

var fullPoem = appendTextLines(originalVersionPartTwo, to: originalVersionPartOne)

print(fullPoem + "\n")

// MARK: - Smith Version

let horaceSmithVersionPartOne = ["In Egypt's sandy silence, all alone,",
"Stands a gigantic Leg, which far off throws",
"The only shadow that the Desert knows:—",
"\"I am great OZYMANDIAS,\" saith the stone,",
"\"The King of Kings; this mighty City shows",
"The wonders of my hand.\"— The City's gone,—",
"Naught but the Leg remaining to disclose",
"The site of this forgotten Babylon."]

let horaceSmithVersionPartTwo = ["We wonder,—and some Hunter may express",

Fortunately, documenting them is quite easy. We simply precede each line with a triple-forward-slash, and a comment, like so:

Example.14

/// This is the first part of Ozymandias, by Percy Bysshe Shelley
let originalVersionPartOne = ["I met a traveller from an antique land",
                   ·
                   ·
                   ·
/// This is the last part of Ozymandias, by Percy Bysshe Shelley
let originalVersionPartTwo = ["Nothing beside remains. Round the decay",
                   ·
                   ·
                   ·
/// This is a String that we use to consolidate the poem before printing.
var fullPoem = appendTextLines(originalVersionPartTwo, to: originalVersionPartOne)
                   ·
                   ·
                   ·
/// This is the first part of Ozymandias, by Horace Smith
let horaceSmithVersionPartOne = ["In Egypt's sandy silence, all alone,",
                   ·
                   ·
                   ·
/// This is the last part of Ozymandias, by Horace Smith
let horaceSmithVersionPartTwo = ["We wonder,—and some Hunter may express",

Now, let’s try running Jazzy again:

$ jazzy --min-acl internal
Running xcodebuild
Parsing main.swift (1/1)
100% documentation coverage with 0 undocumented symbols
included 6 internal, public, or open symbols
building site
building search index
jam out ♪♫ to your fresh new docs in `docs`

It says we documented “100%” of the code. That means that it was able to generate docs for all the places possible.

It looks more complete, now.

In particular, take a look at the documentation for the appending function:

Yeah, I misspelled Shelley’s middle name. See “Errata,” below.

See how nicely it presents the function arguments and return value?

Another thing to look at, is how the MARK comments are used to create groupings:

We now know how to make a basic Jazzy-generated document for our project, but we need to think about refining it.

Cool Jazz

The main thing that we’d like, is to have a “prettier” main page. Right now, it’s quite boring:

The sorting on the left can’t be helped. Jazzy always sorts alphabetically.

Otherwise, we may want to do the following:

  1. Change “Docs” to something a bit more descriptive.
  2. Change “Reference” to something a bit more descriptive.
  3. Add an author.
  4. Add a GitHub Repo URI.
  5. Remove the “100% documented” blurb. It’s not useful to readers.
  6. Filter out the “fullPoem” listing. It’s really just an internal variable.
  7. Add a good “front page” to the project.

Most of these can be done via Jazzy command-line options, which we get by invoking “jazzy --help” on the command line:

$ jazzy --help
Usage: jazzy

Options
    -o, --output FOLDER              Folder to output the HTML docs to
    -c, --[no-]clean                 Delete contents of output directory before running. 
                                     WARNING: If --output is set to ~/Desktop, this will delete the ~/Desktop directory.
        --[no-]objc                  Generate docs for Objective-C.
        --umbrella-header PATH       Umbrella header for your Objective-C framework.
        --framework-root PATH        The root path to your Objective-C framework.
        --sdk [iphone|watch|appletv][os|simulator]|macosx
                                     The SDK for which your code should be built.
        --hide-declarations [objc|swift] 
                                     Hide declarations in the specified language. Given that generating Swift docs only generates Swift declarations, this is useful for hiding a specific interface for either Objective-C or mixed Objective-C and Swift projects.
        --config PATH                Configuration file (.yaml or .json)
                                     Default: .jazzy.yaml in source directory or ancestor
    -b arg1,arg2,…argN,              Arguments to forward to xcodebuild, swift build, or sourcekitten.
        --build-tool-arguments
    -x arg1,arg2,…argN,              Back-compatibility alias for build_tool_arguments.
        --xcodebuild-arguments
    -s filepath1,…filepathN,         File(s) generated from sourcekitten output to parse
        --sourcekitten-sourcefile
        --source-directory DIRPATH   The directory that contains the source to be documented
    -e filepath1,filepath2,…filepathN,
        --exclude                    Source file pathnames to be excluded from documentation. Supports wildcards.
    -i filepath1,filepath2,…filepathN,
        --include                    Source file pathnames to be included in documentation. Supports wildcards.
        --swift-version VERSION
        --swift-build-tool spm | xcodebuild
                                     Control whether Jazzy uses Swift Package Manager or xcodebuild to build the module to be documented.  By default it uses xcodebuild if there is a .xcodeproj file in the source directory.
    -a, --author AUTHOR_NAME         Name of author to attribute in docs (e.g. Realm)
    -u, --author_url URL             Author URL of this project (e.g. https://realm.io)
    -m, --module MODULE_NAME         Name of module being documented. (e.g. RealmSwift)
        --module-version VERSION     Version string to use as part of the the default docs title and inside the docset.
        --title TITLE                Title to display at the top of each page, overriding the default generated from module name and version.
        --copyright COPYRIGHT_MARKDOWN
                                     copyright markdown rendered at the bottom of the docs pages
        --readme FILEPATH            The path to a markdown README file
        --documentation GLOB         Glob that matches available documentation
        --abstract GLOB              Glob that matches available abstracts for categories
        --podspec FILEPATH           A CocoaPods Podspec that describes the Swift library to document
        --docset-icon FILEPATH
        --docset-path DIRPATH        The relative path for the generated docset
    -r, --root-url URL               Absolute URL root where these docs will be stored
    -d, --dash_url URL               Location of the dash XML feed e.g. https://realm.io/docsets/realm.xml)
    -g, --github_url URL             GitHub URL of this project (e.g. https://github.com/realm/realm-cocoa)
        --github-file-prefix PREFIX  GitHub URL file prefix of this project (e.g. https://github.com/realm/realm-cocoa/tree/v0.87.1)
        --disable-search             Avoid generating a search index. Search is available in some themes.
        --skip-documentation         Will skip the documentation generation phase.
        --min-acl [private | fileprivate | internal | public | open]
                                     minimum access control level to document
        --[no-]skip-undocumented     Don't document declarations that have no documentation comments.
        --[no-]hide-documentation-coverage
                                     Hide "(X% documented)" from the generated documents
        --head HTML                  Custom HTML to inject into .
        --theme [apple | fullwidth | jony | DIRPATH]
                                     Which theme to use. Specify either 'apple' (default), one of the other built-in theme names, or the path to your mustache templates and other assets for a custom theme.
        --use-safe-filenames         Replace unsafe characters in filenames with an encoded representation. This will reduce human readability of some URLs, but may be necessary for projects that expose filename-unfriendly functions such as /(_:_:)
    -t, --template-directory DIRPATH DEPRECATED: Use --theme instead.
        --assets-directory DIRPATH   DEPRECATED: Use --theme instead.
        --undocumented-text UNDOCUMENTED_TEXT
                                     Default text for undocumented symbols. The default is "Undocumented", put "" if no text is required
    -v, --version                    Print version number
    -h, --help [TOPIC]               Available topics:
                                       usage   Command line options (this help message)
                                       config  Configuration file options
                                     ...or an option keyword, e.g. "dash"

We are not going to get into a comprehensive tutorial on these options (as you can see, it’s quite a rabbit-hole). This is really just a primer on how to figure out what to do.

Looking at the list of requirements, and the help output, we can assume that these commands may be what we want:

  1. Change “Docs” to something a bit more descriptive.
    --title TITLE Title to display at the top of each page, overriding the default generated from module name and version.
  2. Change “Reference” to something a bit more descriptive.
    -m, --module MODULE_NAME Name of module being documented. (e.g. RealmSwift)
    I should mention here, that the “module name” needs to be the exact target/module name (case-sensitive). We can’t get creative.
  3. Add an author.
    -a, --author AUTHOR_NAME Name of author to attribute in docs (e.g. Realm)
    -u, --author_url URL Author URL of this project (e.g. https://realm.io)

  4. Add a GitHub Repo URI.
    -g, --github_url URL GitHub URL of this project (e.g. https://github.com/realm/realm-cocoa)
  5. Remove the “100% documented” blurb. It’s not useful to readers.
    --[no-]hide-documentation-coverage Hide "(X% documented)" from the generated documents

That’s not all of them, but it covers most of what we want. So, if we then make the Jazzy command line look like this:

jazzy --min-acl \
    internal --hide-documentation-coverage \
    --title Ozymandias \
    -m Ozymandias \
    -a ChrisMarshallNY \
    -u https://github.com/ChrisMarshallNY \
    -g https://github.com/LittleGreenViper/Ozymandias

We should get a big improvement in our output:

So this leaves us with just two more items:

  1. Filter out the “fullPoem” listing. It’s really just an internal variable.
  2. Add a good “front page” to the project.

Filter Out the “fullPoem” Listing

There’s two ways to filter this out:

  1. Make the variable private, so it is not documented.
  2. Don’t document it, and invoke the “--skip-undocumented” command-line option.

First, let’s make the variable private:

/// This is a String that we use to consolidate the poem before printing.
private var fullPoem = appendTextLines(originalVersionPartTwo, to: originalVersionPartOne)

When we run jazzy, we will see a slight change to the output:

$ jazzy --min-acl     internal --hide-documentation-coverage     --title Ozymandias     -m Ozymandias     -a ChrisMarshallNY     -u https://github.com/ChrisMarshallNY     -g https://github.com/LittleGreenViper/Ozymandias
Running xcodebuild
Parsing main.swift (1/1)
100% documentation coverage with 0 undocumented symbols
included 5 internal, public, or open symbols
skipped 1 private or fileprivate symbol (use `--min-acl` to specify a different minimum ACL)
building site
building search index
jam out ♪♫ to your fresh new docs in `docs`

And when we look at it now, we see that the declaration is now gone:

Let’s try the other method, and see how that works.

var fullPoem = appendTextLines(originalVersionPartTwo, to: originalVersionPartOne)
jazzy --min-acl \
    internal --hide-documentation-coverage \
    --title Ozymandias \
    -m Ozymandias \
    -a ChrisMarshallNY \
    -u https://github.com/ChrisMarshallNY \
    -g https://github.com/LittleGreenViper/Ozymandias \
    --skip-undocumented

When we run it this time, we see that we are no longer 100% documented:

$ jazzy --min-acl \
>     internal --hide-documentation-coverage \
>     --title Ozymandias \
>     -m Ozymandias \
>     -a ChrisMarshallNY \
>     -u https://github.com/ChrisMarshallNY \
>     -g https://github.com/LittleGreenViper/Ozymandias \
>     --skip-undocumented
Running xcodebuild
Checking xcodebuild -showBuildSettings
Assuming New Build System is used.
Parsing main.swift (1/1)
83% documentation coverage with 1 undocumented symbol
included 6 internal, public, or open symbols
building site
building search index
jam out ♪♫ to your fresh new docs in `docs`

However, the results are exactly the same as in Example 16.

This time, though, we have something in the “undocumented.json” file:

{
  "warnings": [
    {
      "file": "/Users/chrismarshall/Desktop/Ozymandias/Ozymandias/main.swift",
      "line": 57,
      "symbol": "fullPoem",
      "symbol_kind": "source.lang.swift.decl.var.global",
      "warning": "undocumented"
    }
  ],
  "source_directory": "/Users/chrismarshall/Desktop/Ozymandias"
}

My preference is to use Method 1, as that leaves us the option of getting that documentation by simply increasing the min-acl. However, we may not be able to do that (maybe we are already at private), so Method 2 is also a possibility.

Fronting

The last item is to improve the “front page” of the documentation.

We will take this opportunity to kill two birds with one stone. We’ll create a nice front page, and we’ll also create a Markdown README for the project.

Remember, there was this line in the --help dump:

--readme FILEPATH            The path to a markdown README file

What this means, is that we can specify a README file, in Markdown format, that Jazzy will use as a “front page.”

If this option is not specified, Jazzy will automatically look for a file called “README.md,” and use that.

So let’s start by creating a VERY simple README file:

# OZYMANDIAS

We will run our Jazzy command line again (NOTE: I am reverting to the Method #1 for hiding the “fullPoem” variable):

jazzy --min-acl \
    internal --hide-documentation-coverage \
    --title Ozymandias \
    -m Ozymandias \
    -a ChrisMarshallNY \
    -u https://github.com/ChrisMarshallNY \
    -g https://github.com/LittleGreenViper/Ozymandias

Which will result in this:

OK. We see that this works. Let’s make the front page a little nicer.

We’ll create an icon, and maybe put the poem on the front page:

![Egyptian Bust](icon.png)
# OZYMANDIAS

I met a traveller from an antique land

Who said: Two vast and trunkless legs of stone

Stand in the desert. Near them, on the sand,

Half sunk, a shattered visage lies, whose frown,

And wrinkled lip, and sneer of cold command,

Tell that its sculptor well those passions read

Which yet survive, stamped on these lifeless things,

The hand that mocked them and the heart that fed:

And on the pedestal these words appear:

"My name is Ozymandias, king of kings:

Look on my works, ye Mighty, and despair!"

Nothing beside remains. Round the decay

Of that colossal wreck, boundless and bare

The lone and level sands stretch far away.

Now, if we submit the project now, we get a cool new README front page for our repo:

But there’s one, last issue…

If we look at the docs, we see that the icon is not displayed:

That’s because the README looks for an “icon.png” file in the same directory as the file. As long as the README and the icon file are together (like at the repo root), then there’s no issue. However, when we create the docs, we move the rendered README (which is now index.html) into a new directory, and did not move the icon file.

There’s a couple of ways that we can address this:

  1. Make a copy of the icon.png file, and move that copy into the docs directory.
  2. Have the README reference the image from a consistent place (like an Internet URI).

I tend to use Method #1, but that involves writing a script, and this post is long enough, without getting into that.

So let’s use Method #2:

![Egyptian Bust](https://github.com/LittleGreenViper/Ozymandias/raw/master/icon.png)
# OZYMANDIAS

This is a sample repo that accompanies the [Leaving A Legacy](https://littlegreenviper.com/miscellany/leaving-a-legacy/) essay.
-

I met a traveller from an antique land

Who said: Two vast and trunkless legs of stone

Stand in the desert. Near them, on the sand,

Half sunk, a shattered visage lies, whose frown,

And wrinkled lip, and sneer of cold command,

Tell that its sculptor well those passions read

Which yet survive, stamped on these lifeless things,

The hand that mocked them and the heart that fed:

And on the pedestal these words appear:

"My name is Ozymandias, king of kings:

Look on my works, ye Mighty, and despair!"

Nothing beside remains. Round the decay

Of that colossal wreck, boundless and bare

The lone and level sands stretch far away.

Note that I also added some text, referencing the essay.

Now, everything looks OK.

Extra Credit

There’s another thing that we can do.

First, the Jazzy command line is awkward, so we can move everything into a separate config file.

As is the case with most of this stuff, the config file is a YAML file. The default name is “.jazzy.yaml(Note the leading “dot” -that means the file is “invisible”). If we have a file with that name in the same directory as Jazzy is running, then Jazzy will automatically use that.

We can move the command line arguments into that file:

title: Ozymandias
module: Ozymandias
min_acl: internal
author: ChrisMarshallNY
author_url: https://github.com/ChrisMarshallNY
github_url: https://github.com/LittleGreenViper/Ozymandias
hide_documentation_coverage: true
sdk: macosx
theme: fullwidth

Note the two highlighted lines. I chose to make the theme the “full width” theme, and specify the Mac OS X SDK, to speed up the build.

Now, when we go to the directory, we can simply use “jazzy” as our command:

$ jazzy
Using config file /Users/chrismarshall/Desktop/Ozymandias/.jazzy.yaml
Running xcodebuild
Checking xcodebuild -showBuildSettings
Assuming New Build System is used.
Parsing main.swift (1/1)
100% documentation coverage with 0 undocumented symbols
included 5 internal, public, or open symbols
skipped 1 private or fileprivate symbol (use `--min-acl` to specify a different minimum ACL)
building site
building search index
jam out ♪♫ to your fresh new docs in `docs`

Note the “Using config file” report. It picked up the “.jazzy.yaml” file.

Here is the final rendering.

A nice side-effect of this process, is that if we check in the “docs” directory, then we can ask GitHub to create a GitHub Pages site for us, and it automatically has a documentation link.

Conclusion

Whew! That was a long read, eh?

Well, now that we’ve made it here, let’s review what we learned:

  1. Documentation is an important part of leaving code that we never have to worry about again.
  2. Chris is a caveman.
  3. We have a number of tools at our disposal to use:
    1. We have Markdown READMEs
    2. We have Checkin comments
    3. We have Doxygen
    4. We have Jazzy
    5. We have Apple’s Inline Markdown Quick View Documentation
  4. We should document WHY something is done, and not WHAT it is doing.
  5. We should follow naming conventions, when appropriate.
  6. We should name our variables, properties, functions, methods, classes, etc. in a manner that helps them to “document themselves.”
  7. Commits to code repositories should be frequent and well-documented.
  8. If we are using idiomatic language constructs, we should leave some basic documentation, explaining their use.
  9. We learned how to create Doxygen/Jazzy/Apple Markdown comment blocks and lines.
  10. We should always keep in mind that people that follow us may not be as expert in the language and algorithms as we are, and document accordingly.
  11. “Ozymandias” is a cool poem.
  12. Shelley had competition (but his version was better).
  13. We learned the basics of using Jazzy to generate project documentation.
  14. We learned how to create a Markdown README.
  15. We learned how to create a GitHub Pages directory.

Errata

  • In some of the earlier commits, Percy Bysshe Shelley is spelled “Percy BLYTHE Shelley.” This is an old mistake that I constantly make. What can I say? I’m a slow learner.
  • In the Example 16 commit, I mistakenly say that the commit is “Example 15.”


from Hacker News https://ift.tt/3icdtnE

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.