Repository: FHIR/sushi
Pull request #725 (2021-01-12 21:50:10)

Puns and punting prettiness
This PR has two main components:

1. It exports the puns so they can be re-used in other apps (GoFSH and FSH Online)
2. It attemps to detect when SUSHI is called from Java and use less pretty characters in the summary

To test the less pretty characters, you'll need to install this version of SUSHI globally and then run it from the publisher.  Step by step instructions:
1. `npm remove -g fsh-sushi`
2. `npm pack`
3. `npm install -g ./fsh-sushi-1.0.2.tgz` (adjust as necessary for Windows)
4. `sushi --init` (accept all defaults)
5. `cd ExampleIG`
6. `sushi .` 
7. Confirm that summary uses pretty boxes
8. `./_updatePublisher.sh` (adjust as necessary for Windows)
9. `./_genonce.sh` (adjust as necessary for Windows)
10. Confirm that summary uses ugly boxes

@mint-thompson -- I'd really like to confirm this works on Windows, so can you give it a try?

cmoesel: Now that I'm working on GoFSH, I can see that many of these puns actually wouldn't work there.  For example, when there is an error in GoFSH, it doesn't make sense to shame the user or tell them to check the docs -- because most likely it is not the user's fault (and FSH docs won't help anyway).  So... these will still be useful to FSH Online, but probably not GoFSH.

Patch:
diff --git a/src/app.ts b/src/app.ts
index 1072d36..1066cd2 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -4,15 +4,19 @@ import path from 'path';
 import fs from 'fs-extra';
 import program from 'commander';
 import chalk from 'chalk';
-import { pad, padStart, sample, padEnd } from 'lodash';
+import process from 'process';
+import { pad, padStart, padEnd } from 'lodash';
 import { FSHTank, RawFSH } from './import';
 import { exportFHIR, Package } from './export';
 import { IGExporter } from './ig';
-import { logger, stats, isSupportedFHIRVersion, Type } from './utils';
 import { loadCustomResources } from './fhirdefs';
 import { FHIRDefinitions } from './fhirdefs';
 import { Configuration } from './fshtypes';
 import {
+  logger,
+  stats,
+  isSupportedFHIRVersion,
+  Type,
   ensureInputDir,
   findInputDir,
   ensureOutputDir,
@@ -21,8 +25,9 @@ import {
   fillTank,
   writeFHIRResources,
   getRawFSHes,
-  init
-} from './utils/Processing';
+  init,
+  getRandomPun
+} from './utils';
 
 const FSH_VERSION = '1.0.0';
 
@@ -213,16 +218,10 @@ function printResults(pkg: Package, isIG: boolean) {
   const insNum = pad(pkg.instances.length.toString(), 9);
   const errorNumMsg = pad(`${stats.numError} Error${stats.numError !== 1 ? 's' : ''}`, 13);
   const wrNumMsg = padStart(`${stats.numWarn} Warning${stats.numWarn !== 1 ? 's' : ''}`, 12);
-  let resultStatus: ResultStatus;
-  if (stats.numError === 0 && stats.numWarn === 0) {
-    resultStatus = 'clean';
-  } else if (stats.numError > 0) {
-    resultStatus = 'errors';
-  } else {
-    resultStatus = 'warnings';
-  }
-  const aWittyMessageInvolvingABadFishPun = padEnd(sample(MESSAGE_MAP[resultStatus]), 36);
-  const clr = COLOR_MAP[resultStatus];
+
+  const aWittyMessageInvolvingABadFishPun = padEnd(getRandomPun(stats.numError, stats.numWarn), 36);
+  const clr =
+    stats.numError > 0 ? chalk.red : stats.numWarn > 0 ? chalk.rgb(179, 98, 0) : chalk.green;
 
   // NOTE: Doing some funky things w/ strings on some lines to keep overall alignment in the code
   const results = [
@@ -240,59 +239,35 @@ function printResults(pkg: Package, isIG: boolean) {
   if (!isIG) {
     results.splice(7, 1);
   }
-  results.forEach(r => console.log(r));
-}
 
-type ResultStatus = 'clean' | 'warnings' | 'errors';
+  const convertChars = !supportsFancyCharacters();
+  results.forEach(r => {
+    if (convertChars) {
+      r = r
+        .replace(/[╔╝╚╗╠╣═]/g, '=')
+        .replace(/[╭╯╰╮]/g, ' ')
+        .replace(/[─┬┼┴]/g, '-')
+        .replace(/[║│├┤]/g, '|');
+    }
+    console.log(r);
+  });
+}
 
-const MESSAGE_MAP: { [key in ResultStatus]: string[] } = {
-  clean: [
-    'That went swimmingly!',
-    'O-fish-ally error free!',
-    "Nice! You're totally krilling it!",
-    'Cool and So-fish-ticated!',
-    'Well hooked and landed!',
-    'You earned a PhD in Ichthyology!',
-    'You rock, lobster!',
-    'Everything is ship-shape!',
-    'Ex-clam-ation point!',
-    'Ac-clam-ations!',
-    'Fin-tastic job!',
-    'You are dolphinitely doing great!',
-    'It doesn’t get any betta than this',
-    'You’re piranha roll now!'
-  ],
-  warnings: [
-    'Not bad, but you cod do batter!',
-    'Something smells fishy...',
-    'Warnings... Water those about?',
-    'Looks like you are casting about.',
-    'A bit pitchy, but tuna-ble.',
-    'Do you sea the problem?',
-    'You are skating on fin ice',
-    'You should mullet over'
-  ],
-  errors: [
-    'Ick! Errors!',
-    'Some-fin went wrong...',
-    'Unfor-tuna-tely, there are errors.',
-    'That really smelt.',
-    'You spawned some errors.',
-    'Just keep swimming, Dory.',
-    'This is the one that got away.',
-    'The docs might be bene-fish-al.',
-    'This was a turtle disaster.',
-    'Something went eely wrong there.',
-    'Documentation may be kelp-ful.',
-    'You should do some sole searching.',
-    'Call a FSH sturgeon!',
-    'This is giving me a haddock.',
-    'You whaley need to turn this around'
-  ]
-};
+function supportsFancyCharacters(): boolean {
+  // There is no sure-fire way, but we know that most problems are when running in the IG Publisher,
+  // so try to detect that situation (which is still actually pretty tricky and not guaranteed).
 
-const COLOR_MAP: { [key in ResultStatus]: chalk.Chalk } = {
-  clean: chalk.green,
-  warnings: chalk.rgb(179, 98, 0),
-  errors: chalk.red
-};
+  // 1. Many JVM will insert an environment variable indicating the main Java class being run.
+  //      E.g., JAVA_MAIN_CLASS_25538=org.hl7.fhir.igtools.publisher.Publisher
+  //    We won't check the actual class; we'll just assume that if it's run in Java, best not take chances.
+  if (Object.keys(process.env).some(k => /^JAVA_MAIN_CLASS/.test(k))) {
+    return false;
+  }
+  // 2. It appears that in a Java-launched process, certain aspects of stdout aren't available, so
+  //    use that to test if it's likely the fancy chars will be supported.
+  if (process.stdout.hasColors === undefined) {
+    return false;
+  }
+  // Otherwise, I guess (?) we're OK.  Worst case scenario: user gets rubbish characters in the summary
+  return true;
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 8ceca34..f3a90d7 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -5,3 +5,4 @@ export * from './Mixin';
 export * from './InstanceDefinitionUtils';
 export * from './Processing';
 export * from './PathUtils';
+export * from './puns';
diff --git a/src/utils/puns.ts b/src/utils/puns.ts
new file mode 100644
index 0000000..c7be46e
--- /dev/null
+++ b/src/utils/puns.ts
@@ -0,0 +1,70 @@
+import { sample } from 'lodash';
+
+const CLEAN_RUN_PUNS = [
+  'That went swimmingly!',
+  'O-fish-ally error free!',
+  "Nice! You're totally krilling it!",
+  'Cool and So-fish-ticated!',
+  'Well hooked and landed!',
+  'You earned a PhD in Ichthyology!',
+  'You rock, lobster!',
+  'Everything is ship-shape!',
+  'Ex-clam-ation point!',
+  'Ac-clam-ations!',
+  'Fin-tastic job!',
+  'You are dolphinitely doing great!',
+  'It doesn’t get any betta than this!',
+  'You’re piranha roll now!'
+];
+
+const WARNING_PUNS = [
+  'Not bad, but you cod do batter!',
+  'Something smells fishy...',
+  'Warnings... Water those about?',
+  'Looks like you are casting about.',
+  'A bit pitchy, but tuna-ble.',
+  'Do you sea the problem?',
+  'You are skating on fin ice.',
+  'You should mullet over.'
+];
+
+const ERROR_PUNS = [
+  'Ick! Errors!',
+  'Some-fin went wrong...',
+  'Unfor-tuna-tely, there are errors.',
+  'That really smelt.',
+  'You spawned some errors.',
+  'Just keep swimming, Dory.',
+  'This is the one that got away.',
+  'The docs might be bene-fish-al.',
+  'This was a turtle disaster.',
+  'Something went eely wrong there.',
+  'Documentation may be kelp-ful.',
+  'You should do some sole searching.',
+  'Call a FSH sturgeon!',
+  'This is giving me a haddock.',
+  'You whaley need to turn this around.'
+];
+
+export function getRandomPun(numErrors = 0, numWarnings = 0): string {
+  const puns = numErrors > 0 ? ERROR_PUNS : numWarnings > 0 ? WARNING_PUNS : CLEAN_RUN_PUNS;
+  return sample(puns);
+}
+
+export function getPuns(
+  includeErrors = true,
+  includeWarnings = true,
+  includeClean = true
+): string[] {
+  const puns: string[] = [];
+  if (includeErrors) {
+    puns.push(...ERROR_PUNS);
+  }
+  if (includeWarnings) {
+    puns.push(...WARNING_PUNS);
+  }
+  if (includeClean) {
+    puns.push(...CLEAN_RUN_PUNS);
+  }
+  return puns;
+}


Test patch:
diff --git a/test/utils/puns.test.ts b/test/utils/puns.test.ts
new file mode 100644
index 0000000..a92d461
--- /dev/null
+++ b/test/utils/puns.test.ts
@@ -0,0 +1,62 @@
+import { getPuns, getRandomPun } from '../../src/utils';
+
+const CLEAN_PUN = 'You are dolphinitely doing great!';
+const WARN_PUN = 'Not bad, but you cod do batter!';
+const ERR_PUN = 'This was a turtle disaster.';
+
+describe('puns', () => {
+  describe('#getPuns', () => {
+    it('should get all puns when no arguments are provided', () => {
+      const puns = getPuns();
+      expect(puns).toContain(CLEAN_PUN);
+      expect(puns).toContain(WARN_PUN);
+      expect(puns).toContain(ERR_PUN);
+    });
+
+    it('should get only clean puns when asked for', () => {
+      const puns = getPuns(false, false, true);
+      expect(puns).toContain(CLEAN_PUN);
+      expect(puns).not.toContain(WARN_PUN);
+      expect(puns).not.toContain(ERR_PUN);
+    });
+
+    it('should get only warning puns when asked for', () => {
+      const puns = getPuns(false, true, false);
+      expect(puns).not.toContain(CLEAN_PUN);
+      expect(puns).toContain(WARN_PUN);
+      expect(puns).not.toContain(ERR_PUN);
+    });
+
+    it('should get only error puns when asked for', () => {
+      const puns = getPuns(true, false, false);
+      expect(puns).not.toContain(CLEAN_PUN);
+      expect(puns).not.toContain(WARN_PUN);
+      expect(puns).toContain(ERR_PUN);
+    });
+
+    it('should get no puns when none are asked for', () => {
+      const puns = getPuns(false, false, false);
+      expect(puns).toHaveLength(0);
+    });
+  });
+
+  describe('#getRandomPun', () => {
+    it('should get a clean run pun when there are no errors or warnings', () => {
+      const pun = getRandomPun(0, 0);
+      // a little fishy since it uses getPuns, but it still seems the best way
+      expect(getPuns(false, false, true)).toContain(pun);
+    });
+
+    it('should get a warning pun when there are no errors but there are warnings', () => {
+      const pun = getRandomPun(0, 1);
+      // a little fishy since it uses getPuns, but it still seems the best way
+      expect(getPuns(false, true, false)).toContain(pun);
+    });
+
+    it('should get an error pun when there are errors', () => {
+      const pun = getRandomPun(1, 1);
+      // a little fishy since it uses getPuns, but it still seems the best way
+      expect(getPuns(true, false, false)).toContain(pun);
+    });
+  });
+});