├── feeds ├── .DS_Store ├── rss.xml.template └── atom.xml.template ├── static ├── avatar.jpg ├── entries │ ├── remote-work.jpg │ ├── white-rose.png │ ├── leaving-python.jpg │ ├── nuxt-elementui.png │ └── nuxt-elementui-fmx.gif ├── mobile.css ├── atom.xml └── rss.xml ├── entries ├── remote-work │ ├── remote-work.jpg │ ├── white-rose.png │ └── remote-work.entry ├── leaving-python │ ├── leaving-python.jpg │ └── leaving-python.entry ├── nuxt-elementui │ ├── nuxt-elementui.png │ ├── nuxt-elementui-fmx.gif │ └── nuxt-elementui.entry ├── invisible-labor.entry ├── atom-w3c-ietf.entry ├── py-aggregator.entry ├── nuxt-boilerplate.entry ├── dont-need-rest.entry ├── choose-vue.entry └── schema-testing.entry ├── assets └── icons │ ├── linkedin.svg │ ├── github.svg │ ├── feed.svg │ └── twitter.svg ├── README.md ├── pages ├── index.js ├── index.vue ├── page.vue ├── archive.vue ├── entry.vue ├── right.css └── influences.md ├── head.js ├── layouts └── default.vue ├── package.json ├── LICENSE ├── .gitignore ├── plugins └── nuxpress.js ├── middleware └── nuxpress.js ├── components └── LeftColumn.vue ├── entries.json ├── nuxt.config.js └── README-archived.md /feeds/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/feeds/.DS_Store -------------------------------------------------------------------------------- /static/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/static/avatar.jpg -------------------------------------------------------------------------------- /static/entries/remote-work.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/static/entries/remote-work.jpg -------------------------------------------------------------------------------- /static/entries/white-rose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/static/entries/white-rose.png -------------------------------------------------------------------------------- /static/entries/leaving-python.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/static/entries/leaving-python.jpg -------------------------------------------------------------------------------- /static/entries/nuxt-elementui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/static/entries/nuxt-elementui.png -------------------------------------------------------------------------------- /entries/remote-work/remote-work.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/entries/remote-work/remote-work.jpg -------------------------------------------------------------------------------- /entries/remote-work/white-rose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/entries/remote-work/white-rose.png -------------------------------------------------------------------------------- /static/entries/nuxt-elementui-fmx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/static/entries/nuxt-elementui-fmx.gif -------------------------------------------------------------------------------- /entries/leaving-python/leaving-python.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/entries/leaving-python/leaving-python.jpg -------------------------------------------------------------------------------- /entries/nuxt-elementui/nuxt-elementui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/entries/nuxt-elementui/nuxt-elementui.png -------------------------------------------------------------------------------- /entries/nuxt-elementui/nuxt-elementui-fmx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galvez/nuxpress/HEAD/entries/nuxt-elementui/nuxt-elementui-fmx.gif -------------------------------------------------------------------------------- /assets/icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # The successor, [NuxtPress](https://nuxt.press/) is available now 3 | 4 | **The code in this repository should not be used any longer**. 5 | 6 | It is in the process of being replaced by an official [Nuxt](https://github.com/nuxt/nuxt.js) module. 7 | 8 | This `README` will be updated with details about the module migration once it's released. 9 | 10 | The old [`README`](https://github.com/galvez/nuxpress/blob/master/README-archived.md) is still available for reference. 11 | 12 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = [ 3 | { 4 | name: 'index', 5 | path: '/', 6 | component: 'pages/index.vue' 7 | }, 8 | { 9 | name: 'archive', 10 | path: '/archive', 11 | component: 'pages/archive.vue' 12 | }, 13 | { 14 | name: 'entry', 15 | path: '/:entrySlug(\\d{4}.+)', 16 | component: 'pages/entry.vue' 17 | }, 18 | { 19 | name: 'page', 20 | path: '/:pageSlug(.+)', 21 | component: 'pages/page.vue' 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | 4 | 10 | -------------------------------------------------------------------------------- /assets/icons/feed.svg: -------------------------------------------------------------------------------- 1 | 3 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /feeds/rss.xml.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jonas Galvez 5 | http://hire.jonasgalvez.com.br/ 6 | Jonas Galvez 7 | <% entries.forEach((entry) => { %> 8 | http://<%= domain %>/<%= entry.permalink %> 9 | <%= entry.id %> 10 | <%= entry.published.toString() %> 11 | <%= entry.title %> 12 | 13 | <% }) %> 14 | 15 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxpress", 3 | "version": "0.1.0", 4 | "description": "Minimalist Markdown Blogging", 5 | "scripts": { 6 | "dev": "nuxt dev", 7 | "build": "nuxt build", 8 | "start": "nuxt start" 9 | }, 10 | "author": "Jonas Galvez", 11 | "dependencies": { 12 | "@dimerapp/markdown": "^3.2.0", 13 | "nuxt": "^2.0.0", 14 | "fs-extra": "^6.0.1", 15 | "lodash": "^4.17.10", 16 | "copy-webpack-plugin": "^4.5.2" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "^7.2.3", 20 | "babel-plugin-transform-do-expressions": "^6.22.0", 21 | "babel-plugin-transform-runtime": "^6.23.0", 22 | "babel-preset-env": "^1.7.0", 23 | "babel-preset-stage-2": "^6.24.1", 24 | "css-loader": "^0.28.4", 25 | "eslint": "^3.19.0", 26 | "eslint-config-standard": "^6.2.1", 27 | "eslint-loader": "^1.6.1", 28 | "eslint-plugin-html": "^2.0.0", 29 | "eslint-plugin-promise": "^3.4.1", 30 | "eslint-plugin-standard": "^2.0.1", 31 | "standard": "^10.0.2", 32 | "webpack-node-externals": "^1.6.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pages/entry.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | 36 | 102 | -------------------------------------------------------------------------------- /entries/atom-w3c-ietf.entry: -------------------------------------------------------------------------------- 1 | 2 | June 14, 2004 3 | 4 | A noteworthy interchange from the AtomSyntax mailing-list 5 | and a reminder on IETF's importance for the web.s 6 | 7 | # Atom W3C/IETF Discussion 8 | 9 | I've been lurking on the [AtomSyntax mailing-list][] for quite some time now, 10 | and I'm finding the way the project evolves very interesting. 11 | 12 | []: http://web.archive.org/web/20050209144510/http://imc.org/atom-syntax/index.html 13 | 14 | The group has always kept the discussion focused on technical issues and getting 15 | things done, but recently there has been a flurry of talk regarding the decision 16 | of whether to go to the [W3C][] or [IETF][]. 17 | 18 | []: http://web.archive.org/web/20050209144510/http://w3.org/ 19 | []: http://web.archive.org/web/20050209144510/http://ietf.org/ 20 | 21 | Both standards bodies are very receptive. The W3C entered the scene late by 22 | making an invitation at a rather inconvenient time. The Atom members had just 23 | sent their formal proposal to the IETF. After much talk, and despite the 24 | [insistence][] of the W3C folks, the IETF [accepted][] the creation of an 25 | Atom WG. From what I can see, most of the Atom mailing-list active members are 26 | happy with this, they just want to get back to technical issues as soon as 27 | possible, but there are some people who are still fighting to take the standard 28 | to the W3C. Here's a snippet from the ongoing [discussion]]: 29 | 30 | []: http://web.archive.org/web/20050209144510/http://bestkungfu.com/archive/?id=492 31 | []: http://web.archive.org/web/20050209144510/http://imc.org/atom-syntax/mail-archive/msg04555.html 32 | []: http://web.archive.org/web/20050209144510/http://www.imc.org/atom-syntax/mail-archive/msg04621.html 33 | 34 | > On a sad note, I have recently heard from a few companies who would like to 35 | > implement Atom but will only support this if it becomes a W3C standard not an 36 | > IETF spec. While I feel this is shortsighted, this is a reality and something 37 | > to consider. 38 | 39 | Here's a [response][] from [Joe Gregorio][]: 40 | 41 | []: http://web.archive.org/web/20050209144510/http://www.imc.org/atom-syntax/mail-archive/msg04636.html 42 | []: http://web.archive.org/web/20050209144510/http://bitworking.org/news/ 43 | 44 | > On a sad note I heard from a few companies who would like to implement Atom 45 | > but will only support this if it becomes a W3C standard not an IETF spec. 46 | > Now, they had to call me by phone since they don't use email ([RFC 821][]). 47 | > And I would love to point to their web site but they don't have one because 48 | > that would require DNS ([RFC1034][], [RFC1035][]). They considered using 49 | > raw IP addresses but that wouldn't help with the web pages since they can't 50 | > use HTTP ([RFC 2616][], [RFC 2617][]). 51 | > 52 | > Shall I go on? 53 | > 54 | > I'm calling this for what it's worth. FUD. If these companies really exist 55 | > and are that concerned with a move to the IETF then they should join the 56 | > list and voice their concerns directly, not anonymously by proxy. 57 | 58 | []: http://www.ietf.org/rfc/rfc821.txt 59 | []: http://www.ietf.org/rfc/rfc1034.txt 60 | []: http://www.ietf.org/rfc/rfc1035.txt 61 | []: http://www.ietf.org/rfc/rfc2616.txt 62 | []: http://www.ietf.org/rfc/rfc2617.txt 63 | 64 | I've added the links to the IETF specifications Joe cites. I think that for a 65 | moment in this whole discussion, I had forgotten how great the IETF is. What 66 | they are responsible for. They're basically responsible for the core standards 67 | that form the base of what we know as a **internet**. The IETF is a solid, 68 | strong, and well respected organization, and I just thought I'd share this 69 | little fact to anyone who's saddened Atom is not going to the W3C anymore. 70 | -------------------------------------------------------------------------------- /static/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jonas Galvez 5 | http://hire.jonasgalvez.com.br/ 6 | Jonas Galvez 7 | 8 | http://hire.jonasgalvez.com.br/2018/Jun/16/My-Nuxt-and-Koa-Boilerplate 9 | tag:hire.jonasgalvez.com.br,2018:1529118000000 10 | Sat Jun 16 2018 00:00:00 GMT-0300 (Brasilia Standard Time) 11 | My Nuxt and Koa Boilerplate 12 | 13 | 14 | http://hire.jonasgalvez.com.br/2018/May/20/You-Don't-Need-REST 15 | tag:hire.jonasgalvez.com.br,2018:1526785200000 16 | Sun May 20 2018 00:00:00 GMT-0300 (Brasilia Standard Time) 17 | You Don't Need REST 18 | 19 | 20 | http://hire.jonasgalvez.com.br/2017/Aug/25/Leaving-Python-for-JavaScript 21 | tag:hire.jonasgalvez.com.br,2017:1503630000000 22 | Fri Aug 25 2017 00:00:00 GMT-0300 (Brasilia Standard Time) 23 | Leaving Python for JavaScript 24 | 25 | 26 | http://hire.jonasgalvez.com.br/2017/Jul/23/Nuxt-and-ElementUI 27 | tag:hire.jonasgalvez.com.br,2017:1500778800000 28 | Sun Jul 23 2017 00:00:00 GMT-0300 (Brasilia Standard Time) 29 | Nuxt and ElementUI 30 | 31 | 32 | http://hire.jonasgalvez.com.br/2017/Jul/16/My-Approach-to-Remote-Work 33 | tag:hire.jonasgalvez.com.br,2017:1500174000000 34 | Sun Jul 16 2017 00:00:00 GMT-0300 (Brasilia Standard Time) 35 | My Approach to Remote Work 36 | 37 | 38 | http://hire.jonasgalvez.com.br/2017/Jun/23/Why-Choose-Vue.js 39 | tag:hire.jonasgalvez.com.br,2017:1498186800000 40 | Fri Jun 23 2017 00:00:00 GMT-0300 (Brasilia Standard Time) 41 | Why Choose Vue.js 42 | 43 | 44 | http://hire.jonasgalvez.com.br/2016/Apr/13/A-Million-Schema-Validations-on-Kubernetes 45 | tag:hire.jonasgalvez.com.br,2016:1460516400000 46 | Wed Apr 13 2016 00:00:00 GMT-0300 (Brasilia Standard Time) 47 | A Million Schema Validations on Kubernetes 48 | 49 | 50 | http://hire.jonasgalvez.com.br/2013/Mar/31/Embrace-Invisible-Labor 51 | tag:hire.jonasgalvez.com.br,2013:1364698800000 52 | Sun Mar 31 2013 00:00:00 GMT-0300 (Brasilia Standard Time) 53 | Embrace Invisible Labor 54 | 55 | 56 | http://hire.jonasgalvez.com.br/2004/Sep/25/A-40-line-Feed-Aggregator 57 | tag:hire.jonasgalvez.com.br,2004:1096081200000 58 | Sat Sep 25 2004 00:00:00 GMT-0300 (Brasilia Standard Time) 59 | A 40-line Feed Aggregator 60 | 61 | 62 | http://hire.jonasgalvez.com.br/2004/Jun/14/Atom-W3C/IETF-Discussion 63 | tag:hire.jonasgalvez.com.br,2004:1087182000000 64 | Mon Jun 14 2004 00:00:00 GMT-0300 (Brasilia Standard Time) 65 | Atom W3C/IETF Discussion 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /entries.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "permalink": "2018/Jun/16/My-Nuxt-and-Koa-Boilerplate", 4 | "published": "2018-06-16T03:00:00.000Z", 5 | "title": "My Nuxt and Koa Boilerplate", 6 | "summary": "Notes on JSON-Pure APIs and a walkthrough of an experimental API gateway using \nGo's reflection capabilities.", 7 | "publishedText": "June 16, 2018 ", 8 | "id": "tag:hire.jonasgalvez.com.br,2018:1529118000000", 9 | "markdown": "./entries/nuxt-boilerplate.entry" 10 | }, 11 | { 12 | "permalink": "2018/May/20/You-Don't-Need-REST", 13 | "published": "2018-05-20T03:00:00.000Z", 14 | "title": "You Don't Need REST", 15 | "summary": "Notes on JSON-Pure APIs and a walkthrough of an experimental API gateway using \nGo's reflection capabilities.", 16 | "publishedText": "May 20, 2018", 17 | "id": "tag:hire.jonasgalvez.com.br,2018:1526785200000", 18 | "markdown": "./entries/dont-need-rest.entry" 19 | }, 20 | { 21 | "permalink": "2017/Aug/25/Leaving-Python-for-JavaScript", 22 | "published": "2017-08-25T03:00:00.000Z", 23 | "title": "Leaving Python for JavaScript", 24 | "summary": "A piece on my new web development stack following \nmy ongoing work on three Nuxt applications", 25 | "publishedText": "August 25, 2017", 26 | "id": "tag:hire.jonasgalvez.com.br,2017:1503630000000", 27 | "markdown": "./entries/leaving-python/leaving-python.entry" 28 | }, 29 | { 30 | "permalink": "2017/Jul/23/Nuxt-and-ElementUI", 31 | "published": "2017-07-23T03:00:00.000Z", 32 | "title": "Nuxt and ElementUI", 33 | "summary": "A 30-second introduction to Nuxt.js plus \nnotes on getting ElementUI to run with it.", 34 | "publishedText": "July 23, 2017", 35 | "id": "tag:hire.jonasgalvez.com.br,2017:1500778800000", 36 | "markdown": "./entries/nuxt-elementui/nuxt-elementui.entry" 37 | }, 38 | { 39 | "permalink": "2017/Jul/16/My-Approach-to-Remote-Work", 40 | "published": "2017-07-16T03:00:00.000Z", 41 | "title": "My Approach to Remote Work", 42 | "summary": "An account on tracking and maintaining progress.", 43 | "publishedText": "July 16, 2017", 44 | "id": "tag:hire.jonasgalvez.com.br,2017:1500174000000", 45 | "markdown": "./entries/remote-work/remote-work.entry" 46 | }, 47 | { 48 | "permalink": "2017/Jun/23/Why-Choose-Vue.js", 49 | "published": "2017-06-23T03:00:00.000Z", 50 | "title": "Why Choose Vue.js", 51 | "summary": "Here I present the main reasons why I chose Vue over React.", 52 | "publishedText": "June 23, 2017", 53 | "id": "tag:hire.jonasgalvez.com.br,2017:1498186800000", 54 | "markdown": "./entries/choose-vue.entry" 55 | }, 56 | { 57 | "permalink": "2016/Apr/13/A-Million-Schema-Validations-on-Kubernetes", 58 | "published": "2016-04-13T03:00:00.000Z", 59 | "title": "A Million Schema Validations on Kubernetes", 60 | "summary": "How I used Python and Kubernetes to perform large-scale,\nautomated schema validation tests against an API.", 61 | "publishedText": "April 13, 2016", 62 | "id": "tag:hire.jonasgalvez.com.br,2016:1460516400000", 63 | "markdown": "./entries/schema-testing.entry" 64 | }, 65 | { 66 | "permalink": "2013/Mar/31/Embrace-Invisible-Labor", 67 | "published": "2013-03-31T03:00:00.000Z", 68 | "title": "Embrace Invisible Labor", 69 | "summary": "What if you spent 80% of the time thinking and \nonly 20% of the time actually typing things in?", 70 | "publishedText": "March 31, 2013", 71 | "id": "tag:hire.jonasgalvez.com.br,2013:1364698800000", 72 | "markdown": "./entries/invisible-labor.entry" 73 | }, 74 | { 75 | "permalink": "2004/Sep/25/A-40-line-Feed-Aggregator", 76 | "published": "2004-09-25T03:00:00.000Z", 77 | "title": "A 40-line Feed Aggregator", 78 | "summary": "A simple feed aggregator in Python leveraging the \nUniversal Feed Parser and the `__cmp__` method.", 79 | "publishedText": "September 25, 2004", 80 | "id": "tag:hire.jonasgalvez.com.br,2004:1096081200000", 81 | "markdown": "./entries/py-aggregator.entry" 82 | }, 83 | { 84 | "permalink": "2004/Jun/14/Atom-W3C/IETF-Discussion", 85 | "published": "2004-06-14T03:00:00.000Z", 86 | "title": "Atom W3C/IETF Discussion", 87 | "summary": "A noteworthy interchange from the AtomSyntax mailing-list \nand a reminder on IETF's importance for the web.s", 88 | "publishedText": "June 14, 2004", 89 | "id": "tag:hire.jonasgalvez.com.br,2004:1087182000000", 90 | "markdown": "./entries/atom-w3c-ietf.entry" 91 | } 92 | ] -------------------------------------------------------------------------------- /entries/py-aggregator.entry: -------------------------------------------------------------------------------- 1 | 2 | September 25, 2004 3 | 4 | A simple feed aggregator in Python leveraging the 5 | Universal Feed Parser and the `__cmp__` method. 6 | 7 | # A 40-line Feed Aggregator 8 | 9 | As you can probably guess, yes, in Python. Writing an aggregator like [MXNA][] 10 | or [Full as a Goog][] isn't [parkwalkian][] at all. Once you're out of the 11 | nightmare of parsing the multiple RSS variants and Atom, next you have to order 12 | items by date of publication. This means you'll have to write code to parse the 13 | different date formats used in RSS and Atom, which includes dealing with 14 | timezones, so you can finally convert them to Unix Time and make them sortable. 15 | 16 | []: http://web.archive.org/web/20050205221813/http://www.markme.com:80/mxna/ 17 | []: http://web.archive.org/web/20050213025445/http://www.fullasagoog.com:80/ 18 | []: http://web.archive.org/web/20050213014950/http://inamidst.com/notes/phenomic 19 | 20 | Fortunately, someone has spent time to provide us with a fast and stable library 21 | that can parse every known syndication format on earth seamlessly. The 22 | [Universal Feed Parser][] is an absolute must for any application that deals 23 | heavily with syndication formats. 24 | 25 | []: http://web.archive.org/web/20050213014950/http://feedparser.org/ 26 | 27 | If you were to build an aggregator, you would probably end up defining a class 28 | to represent entries and whatever additional information is associated with 29 | them. But first, there needs to be some sort of storage mechanism for the feed 30 | URLs. In this example, I'll use a simple text file. The code starts with the 31 | importing of the require modules, the parsing of the feeds' URLs and the 32 | definition of an array to hold the items once we parse them. 33 | 34 | ```py 35 | import time 36 | import feedparser 37 | 38 | sourceList = open('feeds.txt').readlines() 39 | postList = [] 40 | ``` 41 | 42 | Next, we define the Entry class. It will act as an wrapper for the entry object 43 | the Universal Feed Parser returns. The modified_parsed property contains the 44 | entry date in a tuple of nine elements, where the first six are the year, 45 | month, day, hour, minute and second. This tuple can be converted to Unix Epoch 46 | with the built-in method `time.mktime()`: 47 | 48 | ```py 49 | class Entry: 50 | def __init__(self, data, blog): 51 | self.blog = blog 52 | self.title = data.title 53 | self.date = time.mktime(data.modified_parsed) 54 | self.link = data.link 55 | def __cmp__(self, other): 56 | return other.date - self.date 57 | ``` 58 | 59 | The `__cmp__` method defines the standard comparision behavior of the 60 | class (you could also override specifically `==` behavior by defining a 61 | `__eq__` method, but `__cmp__` works just the same). Once we get an array 62 | with `Entry` instances and call `sort()`, the `__cmp__` method will be 63 | used to define the order. 64 | 65 | Now comes the part where the UFP saves us 200 lines of code. Since we want to 66 | show entries ordered by date, it's prudent to at least verify if the entry 67 | actually includes a date. Further measures would include checking if the date 68 | is within the current century. Or, you could just check for the `bozo` bit 69 | and refuse invalid feeds altogether. 70 | 71 | ```py 72 | for uri in sourceList: 73 | xml = feedparser.parse(uri.strip()) 74 | blog = xml.feed.title 75 | for e in xml.entries[:10]: 76 | if not e.has_key('modified_parsed'): 77 | continue 78 | postList.append(Entry(e, blog)) 79 | 80 | postList.sort() 81 | ``` 82 | 83 | Finally, we output the data: 84 | 85 | ```py 86 | print('Content-type: text/html\n') 87 | print('') 96 | ``` 97 | 98 | If you want something as complete as **Full as a Goog**, you might want to 99 | check [PlanetPlanet][], which also uses the Universal Feed Parser. But the 100 | simplicity of this example should invite you to improve and tweak it yourself. 101 | Let me know if you make something interesting out of it. Enjoy. 102 | 103 | []: http://web.archive.org/web/20050213014950/http://planetplanet.org/ 104 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | 2 | const webpack = require('webpack') 3 | const fs = require('fs-extra') 4 | const path = require('path') 5 | const lodash = require('lodash') 6 | const CopyWebpackPlugin = require('copy-webpack-plugin') 7 | 8 | const domain = 'hire.jonasgalvez.com.br' 9 | 10 | const isDirectory = (source) => { 11 | return fs.lstatSync(source).isDirectory() 12 | } 13 | 14 | const parseEntry = (entryFile) => { 15 | const raw = fs.readFileSync(entryFile, 'utf8') 16 | let [ 17 | published, 18 | summary 19 | ] = raw 20 | .substr(0, raw.indexOf('#')) 21 | .trim() 22 | .split(/\n\n/) 23 | const publishedText = published 24 | published = new Date(Date.parse(published)) 25 | const md = raw.substr(raw.indexOf('#')) 26 | const title = md 27 | .substr(raw.indexOf('\n')) 28 | .match(/^#\s+(.*)/)[1] 29 | return { 30 | title, 31 | summary, 32 | published, 33 | publishedText, 34 | id: `tag:${domain},${published.getFullYear()}:${published.getTime()}`, 35 | markdown: entryFile.replace(/^.*\/entries\//, './entries/') 36 | } 37 | } 38 | 39 | const loadEntry = (fullpath) => { 40 | if (isDirectory(fullpath)) { 41 | const entryFiles = fs.readdirSync(fullpath) 42 | const text = entryFiles.find((f) => f.endsWith('.entry')) 43 | if (!text) { 44 | return false 45 | } 46 | const assetsRoot = path.join('static', 'entries') 47 | fs.ensureDirSync(assetsRoot) 48 | const others = entryFiles 49 | .filter((f) => !f.endsWith('.entry')) 50 | .forEach((f) => { 51 | fs.copySync( 52 | path.join(fullpath, f), 53 | path.join(assetsRoot, f) 54 | ) 55 | }) 56 | return parseEntry(path.join(fullpath, text)) 57 | } else if (fullpath.endsWith('.entry')) { 58 | return parseEntry(fullpath) 59 | } else { 60 | return false 61 | } 62 | } 63 | 64 | const generateEntryPermalink = (title, published) => { 65 | const slug = title.replace(/\s+/g, '-') 66 | const date = published.toString().split(/\s+/).slice(1, 4).reverse() 67 | return `${date[0]}/${date[2]}/${date[1]}/${slug}` 68 | } 69 | 70 | const entries = (() => { 71 | const entriesRoot = path.resolve(__dirname, 'entries') 72 | const entries = [] 73 | const dirEntries = fs.readdirSync(entriesRoot) 74 | for (let i = 0; i < dirEntries.length; i++) { 75 | const validEntry = loadEntry(path.join(entriesRoot, dirEntries[i])) 76 | if (validEntry) { 77 | entries.push( 78 | Object.assign({}, { 79 | permalink: generateEntryPermalink(validEntry.title, validEntry.published), 80 | published: validEntry.published.toISOString() 81 | }, validEntry) 82 | ) 83 | } 84 | } 85 | entries.sort((a, b) => { 86 | return b.published - a.published 87 | }) 88 | return entries 89 | })() 90 | 91 | const generateIndex = () => { 92 | fs.writeFileSync( 93 | path.resolve(__dirname, 'entries.json'), 94 | JSON.stringify(entries, null, 2) 95 | ) 96 | } 97 | 98 | const generateFeeds = () => { 99 | const rssFeedTemplate = lodash.template( 100 | fs.readFileSync('./feeds/rss.xml.template', 'utf8') 101 | ) 102 | const atomFeedTemplate = lodash.template( 103 | fs.readFileSync('./feeds/atom.xml.template', 'utf8') 104 | ) 105 | const data = { 106 | entries: entries.slice(0, 10), 107 | domain 108 | } 109 | fs.writeFileSync('./static/rss.xml', rssFeedTemplate(data)) 110 | fs.writeFileSync('./static/atom.xml', atomFeedTemplate(data)) 111 | } 112 | 113 | const routes = require('./pages/index') 114 | 115 | export default { 116 | plugins: ['~/plugins/nuxpress.js'], 117 | srcDir: './', 118 | router: { 119 | extendRoutes: (nuxtRoutes, resolve) => { 120 | nuxtRoutes.splice(0, nuxtRoutes.length, ...routes.map((route) => { 121 | return Object.assign({}, route, { 122 | component: resolve(__dirname, route.component) 123 | }) 124 | })) 125 | } 126 | }, 127 | build: { 128 | babel: { 129 | plugins: ['transform-do-expressions'] 130 | }, 131 | plugins: [ 132 | new webpack.IgnorePlugin(/^entries/), 133 | new CopyWebpackPlugin([ 134 | { from: 'entries/*', to: 'entries/' }, 135 | { from: 'pages/*.md', to: 'pages/' } 136 | ]) 137 | ], 138 | extend (config, { isDev }) { 139 | // Generate entries.json file with all entry metadata 140 | generateIndex() 141 | // Generate /static/atom.xml and /static/rss.xml 142 | generateFeeds() 143 | // Tweak for GitHub pages 144 | if (!isDev) { 145 | config.output.publicPath = './_nuxt/' 146 | } else { 147 | // Ensure linting on dev mode 148 | config.module.rules.push({ 149 | enforce: 'pre', 150 | test: /\.(js|vue)$/, 151 | exclude: /(node_modules)/ 152 | }) 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /entries/nuxt-elementui/nuxt-elementui.entry: -------------------------------------------------------------------------------- 1 | 2 | July 23, 2017 3 | 4 | A 30-second introduction to Nuxt.js plus 5 | notes on getting ElementUI to run with it. 6 | 7 | # Nuxt and ElementUI 8 | 9 | ![Flash MX components][fmx-components] My first contact with _UI components_ 10 | was with [Flash MX][] back in 2002. For someone who had started coding with 11 | Flash and never used other tools, I was impressed. I thought these were 12 | going to take over the web back then. And studying their source accounted 13 | for much of my programming education. 14 | 15 | []: http://www.tutorialized.com/tutorial/CGI-Forms-With-Flash-MX-and-UI-Components-Set-2/3093 16 | [fmx-components]: /entries/nuxt-elementui-fmx.gif#float-right 17 | 18 | I'm feeling **_nostalgic_** about that time. 19 | 20 | Since then (_and moving on from Flash_) I have used [jQuery UI][], 21 | [ExtJS][] and most recently, [Bootstrap][]. None of them came close to 22 | giving me the excitement [ElementUI][] has. Built entirely in Vue (although 23 | there's also a [React fork][]), it has a wide range of beautifully designed 24 | UI controls, a [clean API][], [built-in form validation][] and a rather 25 | [decent grid system][]. I'm hooked. 26 | 27 | []: https://jqueryui.com 28 | []: https://www.sencha.com/products/extjs/#overview 29 | []: http://getbootstrap.com/components/ 30 | []: http://element.eleme.io 31 | []: https://github.com/eleme/element-react 32 | []: http://element.eleme.io/#/en-US/component/quickstart 33 | []: http://element.eleme.io/#/en-US/component/form 34 | []: http://element.eleme.io/#/en-US/component/layout 35 | 36 | I feel Vue lets me harness JavaScript's expressiveness on real problems, 37 | bringing boilerplate to the absolute minimum. The code I have to write is 38 | concise, but still hackable – and the same can be said for ElementUI – it's 39 | very easy to dive into the [Vue source for each component][] and figure things 40 | out. We're only afraid of magic when we don't understand it. I find Vue's magic 41 | is [approachable][] and [worthwhile to learn][]. 42 | 43 | []: https://github.com/ElemeFE/element/tree/dev/packages 44 | []: https://vuejs.org/v2/guide/render-function.html 45 | []: https://www.skyronic.com/blog/vuejs-internals-computed-properties 46 | 47 | [Nuxt][] is inspired by [Next][] and basically provides universal rendering 48 | for Vue applications. I was happy to discover it also features **the simplest, 49 | smartest set of conventions and defaults I've ever seen in a JavaScript 50 | framework**. In a world of endless configuration files and massive startup 51 | scripts, its simplicity is refreshing. 52 | 53 | []: http://nuxtjs.org 54 | []: https://github.com/zeit/next.js 55 | []: https://nuxtjs.org/examples 56 | 57 | **Here's a 30-second introduction to Nuxt**: `npm install nuxt`, add `nuxt` as 58 | the `start` script in `package.json`, create a `index.vue` file [like this][] 59 | under `pages/`, `npm start` and **you're done**. It auto generates routes based 60 | on `pages/` (dynamic routes [included][]), you can have master templates in 61 | `layouts/` plus `assets/`, `plugins/`, `components/` and `middleware/`. 62 | 63 | []: https://gist.github.com/galvez/45629a0065cb134172e3822c78e62e06 64 | []: https://nuxtjs.org/guide/routing#dynamic-routes 65 | 66 | [Vuex][] is automatically set up if you have a `stored/index.js` file. Then you 67 | just use [`nuxt.render`][] as a middleware on the server. All the asset 68 | bundling and the development server is handled with sensible defaults, or you 69 | can [cleanly extend Webpack][]. 70 | 71 | []: https://nuxtjs.org/examples/vuex-store 72 | []: https://nuxtjs.org/api/nuxt-render/ 73 | []: https://nuxtjs.org/api/configuration-build 74 | 75 | There's not much else to say, it's that simple. Nuxt has been fully compatible 76 | with ElementUI [since December 2016][]. Just `npm install element-ui` and add 77 | `plugins/element-ui.js`: 78 | 79 | []: https://twitter.com/nuxt_js/status/814761953558204417?lang=en 80 | 81 | ```js 82 | import Vue from 'vue' 83 | const ElementUI = require('element-ui') 84 | const locale = require('element-ui/lib/locale/lang/en') 85 | Vue.use(ElementUI, { locale }) 86 | ``` 87 | 88 | And then specify it in the `mono` entry of [`nuxt.config.js`][]. It did 89 | require setting _loaders_ explicitly as you would have in Webpack, to enable 90 | loading of the ElementUI CSS. I liked how easy it was to make sure the CSS 91 | was included beforehand. 92 | 93 | []: https://gist.github.com/galvez/f76aa8a7cc45ccdc40c9fbc110e29cb2 94 | 95 | Nuxt adds an `asyncData()` method to page components that runs 96 | before any rendering happens. You can return a `Promise`-resolved object 97 | from it which is then merged with the object returned by `data()`. The 98 | snippet bellow **retrieves an image on the server and renders it as inline 99 | Base64 on the client**: 100 | 101 | ``` 102 | 109 | 110 | 125 | ``` 126 | 127 | ElementUI also makes `$alert()` and `$message()` readily available on the Vue 128 | instance ([among others][]). Just a tiny convenience, but it resonates with 129 | the overall experience I'm having with ElementUI. It's the first time _**I 130 | truly liked**_ a framework implementation of pieces of functionality I had to 131 | reimplement a dozen times. This might very well be my new first choice for 132 | building web applications. 133 | 134 | []: http://element.eleme.io/#/en-US/component/quickstart 135 | -------------------------------------------------------------------------------- /entries/nuxt-boilerplate.entry: -------------------------------------------------------------------------------- 1 | 2 | June 16, 2018 3 | 4 | Notes on JSON-Pure APIs and a walkthrough of an experimental API gateway using 5 | Go's reflection capabilities. 6 | 7 | # My Nuxt and Koa Boilerplate 8 | 9 | Originally intended as a quick onboarding guide to new JavaScript developers in 10 | my day job, this is a guide to my preferred starter template, or boilerplate, 11 | for new Nuxt-based web applications. 12 | 13 | ## Why Koa and not Express 14 | 15 | Most people are probably unaware that [Koa][] and [Express][] were written by 16 | the same authors ([TJ Holowaychuk][] et al). Koa takes on a different, 17 | minimalist approach, restraining itself to around 2k LOC and focusing on 18 | enabling DIY middleware solutions. I won't go into detail on [why you should 19 | consider Koa instead of Express][] because honestly, I don't think it makes a 20 | dramatic difference, but for my projects and my team, it has definitely not 21 | been a regrettable choice. 22 | 23 | []: https://koajs.com 24 | []: https://expressjs.com 25 | []: https://github.com/tj 26 | []: https://medium.com/@l1ambda/why-you-should-use-koa-with-node-js-7c231a8174fa 27 | 28 | Using Koa does equal a little bit more work, as it requires you to put 29 | together some server functions yourself. For instance, Express has a built-in 30 | HTTP body parser, whereas in my Koa setup, I need to have a middleware defined 31 | as follows:

32 | 33 | ```js 34 | // Parses HTTP POST data (including uploads) 35 | // and automatically parses any sent JSON 36 | app.use(async (ctx, next) => { 37 | if (['GET', 'DELETE'].includes(ctx.request.method)) { 38 | await next() 39 | } else if (ctx.is('application/json')) { 40 | ctx.json = await parse.json(ctx) 41 | await next() 42 | } else if (ctx.is('multipart/*') === 'multipart/form-data') { 43 | ctx.parts = multipartParse(ctx, { autoFields: true }) 44 | await next() 45 | } else { 46 | try { 47 | ctx.json = await parse.json(ctx) 48 | } catch (err) { 49 | ctx.json = null 50 | } 51 | await next() 52 | } 53 | }) 54 | ``` 55 | 56 | That said, I prefer having a thin server and the ability to add complexity 57 | as needed. So the above snippet is one of a small set of additions to a base 58 | Koa server that I tend to use on every project. You can find it commented on 59 | [my boilerplate repo][]. 60 | 61 | []: https://github.com/galvez/boilerplates/blob/master/nuxt/src/server/index.js 62 | 63 | ## API request routing 64 | 65 | If you have data-only API functions that are used in client requests, you 66 | probably want to have them executed and returning before reaching the Nuxt 67 | renderer. Not that you couldn't do well with [Nuxt's own middleware setup][], 68 | [asyncData][] and [nuxtServerInit][], but it just makes a lot more sense to 69 | have Koa handle data-only requests directly. 70 | 71 | []: https://nuxtjs.org/examples/middleware/ 72 | []: https://nuxtjs.org/api/ 73 | []: https://nuxtjs.org/guide/vuex-store 74 | 75 | For this I use a middleware that will translate requests like 76 | `/api/service/method` into an actual `Service.method()` call on the server. The 77 | following middleware routes such requests to methods in a [services.js module][]. 78 | This can and probably should be tweaked to match your application's API needs: 79 | 80 | []: https://github.com/galvez/boilerplates/blob/master/nuxt/src/server/index.js 81 | 82 | ```js 83 | app.use(async (ctx, next) => { 84 | if (ctx.path.startsWith('/api') === false || ctx.request.method !== 'POST') { 85 | await next() 86 | } else { 87 | const apiMethod = ctx.path.split('/api/')[1] 88 | let [service, method] = apiMethod.split('/') 89 | service = translatePath(service) 90 | .replace(/^(.)(.*)/, (_, f, r) => `${f.toUpperCase()}${r}`) 91 | method = translatePath(method) 92 | ctx.body = await services[service][method](ctx.json) 93 | } 94 | }) 95 | ``` 96 | 97 | In [ContaGrama][], a project of mine that makes use of this boilerplate, it 98 | made sense to call this file [models.js][]. There you can see a more complex 99 | example, with relating calls in the [pages/index.vue][], 100 | [pages/foods/index.vue][] and [pages/foods/_id.vue][]. 101 | 102 | []: https://github.com/galvez/contagrama 103 | []: https://github.com/galvez/contagrama/blob/master/src/server/models.js 104 | []: https://github.com/galvez/contagrama/blob/master/src/pages/index.vue 105 | []: https://github.com/galvez/contagrama/blob/master/src/pages/foods/index.vue 106 | []: https://github.com/galvez/contagrama/blob/master/src/pages/foods/_id.vue 107 | 108 | ## ElementUI setup 109 | 110 | Over the past couple of years, I've worked on three ElementUI-based applications. 111 | ElementUI has now become a permanent addition to my stack, as it provides [a 112 | rich set of elements][] and rather idiomatic Vue implementation. In order to 113 | make it work in Nuxt, you need to include `element-theme-default`'s stylesheets 114 | and define a Nuxt plugin to instantiate it: 115 | 116 | []: http://element.eleme.io/#/en-US/component/installation 117 | 118 | ```js 119 | import Vue from 'vue' 120 | 121 | const ElementUI = require('element-ui') 122 | const locale = require('element-ui/lib/locale/lang/en') 123 | Vue.use(ElementUI, { locale }) 124 | ``` 125 | 126 | ## ESLint 127 | 128 | I can't live without ESLint anymore. Ideally, you'll want to have `npm run lint` 129 | automatically execute before any push. In my setup I've configured it to run 130 | automatically in Nuxt's development mode. 131 | 132 | My ESLint is also configured to use StandardJS, so things like semicolons, or 133 | parenthesis in a function or method definition with no preceding empty space 134 | will cause a linting error. It took me a while to get used to this formatting 135 | style, but I can see it's value after so much time. FWIW, Brendan Eich endorsed 136 | StandardJS, stating that it [enforces safe & cleaner low-semicolon style][]. 137 | 138 | []: https://twitter.com/BrendanEich/status/951560749825933312 139 | 140 | Get it from [https://github.com/galvez/boilerplates][]. 141 | 142 | []: https://github.com/galvez/boilerplates 143 | -------------------------------------------------------------------------------- /entries/remote-work/remote-work.entry: -------------------------------------------------------------------------------- 1 | 2 | July 16, 2017 3 | 4 | An account on tracking and maintaining progress. 5 | 6 | # My Approach to Remote Work 7 | 8 | I've written about this in the past, but I have since taken down my previous 9 | articles because none of them depicted, as it turned out in the years that 10 | passed, a truly sustainable approach. That is, this is not going to cover the 11 | [Pareto principle][], or [Parkinson's Law][], or [the fact that 40 hours a 12 | week work does not equal more productivity][], all the standard advice on 13 | [sleeping][] and [eating][] well, [exercising regularly][], [emilinating 14 | clutter][] or [limiting attention to distractions][]. I assume the reader is 15 | painfully aware of all this stuff. This is about tracking and maintaining 16 | progress – and it's what finally worked for me after 14 years working nearly 17 | exclusively remotely. 18 | 19 | []: http://en.wikipedia.org/wiki/Pareto_principle 20 | []: http://en.wikipedia.org/wiki/Parkinson's_Law 21 | []: https://metarabbit.wordpress.com/2017/07/07/no-research-does-not-say-that-you-produce-more-when-working-40-hours-per-week/ 22 | []: http://www.functionalps.com/blog/2012/06/12/10-tips-for-better-sleep/ 23 | []: https://raypeatforum.com/community/threads/ray-peat-diet-food-choices-and-general-guidelines.20/ 24 | []: http://www.functionalps.com/blog/ 25 | []: http://www.bbc.com/news/world-us-canada-10928032 26 | []: http://www.sciencedaily.com/releases/2010/02/100201171517.htm 27 | 28 | In 2015, I decided I would dilligently track how many hours I spent coding in 29 | the following year, using the [pomodoro][] as a time unit. I barely managed a 30 | total of 16 hours of coding time a month in that year. And despite fighting to 31 | catch up at the end I was ultimately devasted with my progress. 32 | 33 | []: https://en.wikipedia.org/wiki/Pomodoro_Technique 34 | 35 | ``` 36 | 2016 37 | January ○ 38 | February ● 39 | March ●● 40 | April ●●●●●● 41 | May ●●●●● 42 | June ●●●● 43 | July ●●●●● 44 | August ●●● 45 | September ●● 46 | October ●● 47 | November ●● 48 | December ●●●●●●● 49 | ``` 50 | 51 | Each ● ([U+25CF][]) represents **10 pomodoros**, and the ○ ([U+25CB][]) at 52 | the top represents **fewer than** 10 pomodoros. As I said, a total productivity 53 | disaster. It surely wasn't just laziness – I had a lot going on – but it was no 54 | excuse. I wanted my working routing to be as immune to adversities as possible. 55 | 56 | []: http://www.fileformat.info/info/unicode/char/25CF/index.htm 57 | []: http://www.fileformat.info/info/unicode/char/25cb/index.htm 58 | 59 | So what does 2017 looks like so far? **A lot better**. I kept my habit of 60 | tracking the time I spend coding, but stopped worrying about tracking the time 61 | I spent responding to e-mails, being active in Slack or participating in 62 | videoconferences. Worrying about tracking everything is counterproductive. 63 | Nevertheless, my 2017 chart has exhibited only steady progress so far: 64 | 65 | ``` 66 | 2017 67 | January ●●●●●●● 68 | February ●●●●●●● 69 | March ●●●●●●●●● 70 | April ●●●●●●●●●●● 71 | May ●●●●●●●●●●●● 72 | June ●●●●●●●●●●●● 73 | ``` 74 | 75 | **What changed?** Aside from the drive to improve after seeing my failure in the 76 | 2016 chart, not much changed. I am in complete control of my diet now and make 77 | sure I don't ever get any nutrient deficiencies. That is extremely important, 78 | but in general, it boils down to a few key principles I started following: 79 | 80 | - **Track your focus time** – this is essential. It's very easy to lose focus, 81 | and it's important to know what you have to make up for if your productivity 82 | goes down for a while. It happens. Maintaining awareness of your status will 83 | give you motivation not to ever lose steam. Anything works if you do it with 84 | dilligence – I personally just open a tab with [Google Stopwatch][]. 85 | 86 | []: https://www.google.com.br/search?q=stopwatch 87 | 88 | - **Make it interesting** – in my consulting engagements, I try to be as 89 | thorough as possible, so I find elements of interest that often turn work 90 | assignments into _actual fun_. What's interesting? Delivering ahead of time 91 | _**is interesting**_. Writing boring pieces of documentation that are actually 92 | useful _**is interesting**_. I turned pushing commits into a dopaminergic 93 | activitiy to me. It's important to internalise a mindset where _**being great**_ 94 | is something you truly believe is possible and rewarding to accomplish. 95 | 96 | - **Don't worry about working in 25-min chunks**. I just use the pomodoro as a 97 | time unit. I sometimes do a pomodoro in 3 to 5 minutes chunks, always pausing 98 | the timer, because I actually need to be busy with something else. What's 99 | important is that you track your overall daily focus time. I've had incredibly 100 | productive days with extremely fragmented bouts of focus. 101 | 102 | - [Be in complete control of your diet][] – avoid nutrient deficiencies at all 103 | cost. An easy way to achieve this is to [ignore all common advice against 104 | sugar][], and [eat liver once a week][]. [There's plenty research material 105 | on how to build an adequate diet][] – learn to separate _**fact**_ from 106 | _hype_ when it comes to nutrition. I also can't recommend following 107 | [CowsEatGrass][] enough. (_On a side note, these were some dark days where I 108 | removed sugar completely from my diet and my thyroid levels were so low I lost 109 | half a dozen jobs in a row._) 110 | 111 | []: https://www.patreon.com/dannyroddy 112 | []: https://cowseatgrass.org/category/sugar-feeds-thyroid/ 113 | []: https://www.youtube.com/watch?v=YkUKlu0Ze1M 114 | []: https://raypeatforum.com/community/threads/ray-peat-diet-food-choices-and-general-guidelines.20/ 115 | []: https://cowseatgrass.org/ 116 | 117 | - **Vigilance and foresight**. Force yourself to meet a minimum daily 118 | requirement of focus, and don't distract yourself **for nothing in the world**. 119 | Don't distract yourself because you're worried about something, or because you 120 | had an argument with your spouse, or because there's a new episode out on 121 | Netflix, or because whatever idiotic mundane reason throws you off balance. 122 | **Just don't**. The best way to fight boredom or axiety is to put your mind 123 | at work. Your focus is your life and success and you shouldn't allow anything 124 | or anyone to take that away from you. 125 | 126 | **What's next?** I didn't mention so far that nearly 40% of my focus time was 127 | spent on personal projects, and no matter how hard I seem to work, I never 128 | truly get tired or burned out anymore. My ultimate goal this year is to get to 129 | a baseline of 60 hours of coding time a month, intertwined with general 130 | availability in business hours. 131 | -------------------------------------------------------------------------------- /README-archived.md: -------------------------------------------------------------------------------- 1 | # nuxpress: Minimalist Markdown Blogging 2 | 3 | Code that powers [http://hire.jonasgalvez.com.br](http://hire.jonasgalvez.com.br). 4 | 5 | Nuxt + Markdown + a fancy custom loader = **nuxpress**. 6 | 7 | ## What it does 8 | 9 | - Takes **.entry** files from the **entries/** directory and uses the first and 10 | second paragraphs for publishing date and summary respectively, and following 11 | Markdown text to create a **chronological** entry. A permalink is automatically 12 | created and it gets printed (in descending order) in the index page. 13 | 14 | - Takes **.md** files from the **pages/** directory and makes them available 15 | at a path corresponding to the filename, so `pages/page.md` becomes `/page`. 16 | 17 | ## How it does it 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 48 | 49 | 50 | 51 | 53 | 54 |
nuxt.config.jsSupercharged with functions that load .entry files from a 23 | entries/ directory. Generates RSS and Atom feeds using 24 | .template files under feeds/.
plugins/nuxpress.jsUses an index entries.json file (generated by 29 | nuxt.config.js) to inject $entries and 30 | $pages (which contain paths) and $permalinks (which 31 | contains entry metadata) in Nuxt's app.
middleware/nuxpress.jsInfers the request URI and uses the provided $entries and 36 | $pages to read and process files, before passing them down as 37 | $entry or $page to Vue templates.
pages/index.vueasyncData() makes entries available to 42 | the template. Entries are in listed in descending order.
pages/entry.vueasyncData() makes entry available to 47 | the template, previously provided by the nuxpress middleware.
pages/page.vueasyncData() makes page available to 52 | the template, previously provided by the nuxpress middleware.
55 | 56 | If the loader encounters a directory under `entries`, it checks inside for an 57 | `.entry` file and copies all other files to `/static/entries/`, so you can 58 | reference any assets in your `.entry` files. 59 | 60 | The **first** and **second** _paragraphs_ of an `.entry` file are reserved 61 | for the **publishing date** and **summary** respectively. The **publishing 62 | date** can be anything `Date.parse()` can handle. Both are followed by the 63 | entry's Markdown block, which starts with `#` indicating the title (h1). 64 | 65 | ## Philosophy 66 | 67 | **nuxpress** is the result of me reading through [VuePress][1]'s source code for 68 | a week. It doesn't have blogging support yet, so I set out to try and cook 69 | something up with it. I learned a lot reading through Evan's code, but my 70 | feeling is that VuePress goes to great lengths to replicate [Nuxt][2]'s 71 | functionality for automatically setting up and launching a Vue app. 72 | 73 | [1]: https://vuepress.vuejs.org/ 74 | [2]: http://nuxtjs.org/ 75 | 76 | While I see the value of having `vuepress` as a standalone CLI tool and 77 | everything it does that Nuxt doesn't (e.g., all SEO-friendly publishing tweaks), 78 | for me really its **golden feature** is streamlining Markdown in Vue files. 79 | 80 | Now, I **really like** Nuxt's code organization standards. Having a blog as 81 | a Nuxt app makes sense, as it would give me total freedom for customization. 82 | VuePress's `eject` command gives you a similar functionality, but you miss 83 | the multitude of plugins and Nuxt-oriented solutions out there. 84 | 85 | So instead of trying to add blogging support to VuePress, I decided to add 86 | Markdown blogging support to a Nuxt app with the minimum amount of code 87 | and conventions I could possibly manage. 88 | 89 | ## Files that need customization 90 | 91 | The `domain` constant in `nuxt.config.js`, then: 92 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 112 | 113 | 114 | 115 | 116 | 117 |
pages/*Where index.vue, entry.vue and 97 | archive.vue live. As long as you understand and preserve the logic, 98 | layout and CSS can be modified as you like.
feeds/*Lodash templates for RSS and Atom feeds.
layouts/default.vueNuxt's default layout, set up to use components/LeftColumn.vue
components/LeftColumn.vueThe left column in the default two-column layout. This can be removed 111 | altogether and replaced by whatever layout structure you're using.
head.jsBase HTML head definitions used by pages/*.
118 | 119 | ## Improved Markdown links 120 | 121 | I love Markdown, but the fact that you need to use a unique identifier for 122 | link references makes things to hard to maintain. 123 | 124 | Here's an example: 125 | 126 | ``` 127 | To use it you need to [write a locustfile][1], which is just a Python file with 128 | at least one [Locust subclass][2]. Each Locust object specifies a list of tasks 129 | to be performed during the test, represented by a [`TaskSet`][3] class. 130 | 131 | [1]: http://docs.locust.io/en/latest/writing-a-locustfile.html 132 | [2]: http://docs.locust.io/en/latest/api.html#locust-class 133 | [3]: http://docs.locust.io/en/latest/api.html#taskset-class 134 | ``` 135 | 136 | If you remove the second link, you get an aesthetically unpleasing unordered 137 | list. Plus you have to keep track of references, even if you use slugs as link 138 | identifiers instead of numbers. In `nuxpress`, all you have to do is: 139 | 140 | ``` 141 | To use it you need to [write a locustfile][], which is just a Python file with 142 | at least one [Locust subclass][]. Each Locust object specifies a list of tasks 143 | to be performed during the test, represented by a [`TaskSet`][] class. 144 | 145 | []: http://docs.locust.io/en/latest/writing-a-locustfile.html 146 | []: http://docs.locust.io/en/latest/api.html#locust-class 147 | []: http://docs.locust.io/en/latest/api.html#taskset-class 148 | ``` 149 | 150 | And it will automatically link ordered references. Use this if you're ok with 151 | not ever having two references to the same link in paragraphs, and want a 152 | streamlined way of adding and editing them. Otherwise, regular Markdown link 153 | references will work as expected. 154 | 155 | ## Running 156 | 157 | ```sh 158 | npm install 159 | npm run dev # development mode 160 | npm start # nuxt server 161 | ``` 162 | 163 | ## Contributing 164 | 165 | **nuxpress** is missing quite a few features, such as: 166 | 167 | - Progressive web app enhancements 168 | - Syntax highlighting for code blocks 169 | - A decent archiving system (`$archive` and `pages/archive.vue` is a hack) 170 | 171 | Plus the entry loading code can probably be improved to make better use of 172 | async I/O. My first attempts were giving me a headache so its current version 173 | processes everything linearly. PRs are most definitely welcome. 174 | 175 | ## License 176 | 177 | MIT 178 | 179 | Although this repo is a boilerplate, all sample `entries/` and Vue layouts are 180 | from my live blog. Feel free to reuse the layout, **but replace `entries/` with 181 | your own**. -------------------------------------------------------------------------------- /entries/dont-need-rest.entry: -------------------------------------------------------------------------------- 1 | 2 | May 20, 2018 3 | 4 | Notes on JSON-Pure APIs and a walkthrough of an experimental API gateway using 5 | Go's reflection capabilities. 6 | 7 | # You Don't Need REST 8 | 9 | Nearly 10 years ago, [Leonard Richardson][] and [Sam Ruby][] published 10 | [RESTful Web Services][]. I remember eagerly waiting for that book before it 11 | came out. Today, REST is still regarded as the state-of-the-art API architecture. 12 | It's not hard to see its benefit in comparison to preceding RPC solutions of 13 | that time. RESTful APIs _make sense_, because of the obvious HTTP verb mapping 14 | to CRUD methods, and the ease at which individual resources may be addressed and 15 | cached in HTTP middleware. 16 | 17 | []: https://www.crummy.com/writing/ 18 | []: http://intertwingly.net/blog/ 19 | []: http://shop.oreilly.com/product/9780596529260.do 20 | 21 | When you're implementing client software to talk to RESTful APIs though, 22 | naturally the HTTP gets abstracted: `GET /resource/id` becomes `resource.get(id)` 23 | and `POST /resource` becomes `resource.create()`. The vast majority of 24 | applications I touched in the past two years weren't so data intensive to 25 | warrant even thinking about HTTP caching strategies. And if there were one or 26 | two endpoints that did require caching, a localized implementation worked best. 27 | 28 | So having RESTfulness as a design constraint, in this case, is just 29 | **unnecessary complexity**. In order to streamline our JavaScript client's calls 30 | to our core RESTful API at [STORED e-commerce][], we created a gateway that 31 | maps certain URLs to method calls, and let the code inside those methods peform 32 | the actual RESTful requests to our core API. First we added it directly to our 33 | [Nuxt][] stack as a Koa middleware: 34 | 35 | []: http://stored.com.br 36 | []: https://nuxtjs.org/ 37 | 38 | ```js 39 | app.use(async (ctx, next) => { 40 | if (!ctx.path.startsWith('/api') || ctx.request.method !== 'POST') { 41 | await next() 42 | } else { 43 | const apiMethod = ctx.path.split('/api/')[1] 44 | let [resource, method] = apiMethod.split('/') 45 | method = translatePathToMethod(method) 46 | const request = { payload: ctx.json.payload } 47 | const response = await api[resource][ethod](request) 48 | ... 49 | } 50 | ``` 51 | 52 | `translatePathToMethod()` translates an HTTP request like `/api/resource/method` 53 | to a `api.resource.method()` JavaScript call on the server. Our actual code 54 | does quite a bit more, such as checking and refreshing auth tokens on-the-fly, 55 | but you get the idea. 56 | 57 | The takeaway here is that if you're writing a RPC-like proxy to 58 | perform RESTful API calls that do not require any caching, you might as well 59 | remove the extra call and place all your code in that RPC method and basically 60 | have a [JSON-pure API][]. 61 | 62 | []: https://mmikowski.github.io/json-pure/ 63 | 64 | **David Gilbertson**, **Michael S. Mikowski** and **Thomas Jetzinger** [have][] 65 | [written][] [similar][] pieces about REST's unnecessary complexity. 66 | 67 | []: https://hackernoon.com/o-api-an-alternative-to-rest-apis-e9a2ed53b93c 68 | []: https://www.linkedin.com/pulse/using-json-pure-api-instead-restful-approach-thomas-jetzinger/ 69 | []: https://mmikowski.github.io/the_lie/ 70 | 71 | ## An API gateway in Go 72 | 73 | Our Koa-based API proxy gets the job done, but looking forward we wanted to turn 74 | this into a fast and reliable piece of our toolset. At [STORED e-commerce][] we 75 | already do a lot of Python, with our core RESTful API written entirely in it. 76 | But we have had an infatuation with Go that's been growing over the years, 77 | and decided to give it a try. 78 | 79 | []: http://stored.com.br 80 | 81 | For references we looked at many HTTP client implementations in Go but 82 | eventually settled on [go-github][], written by Google developers. go-github 83 | offers an excellent starting point with carefully crafted yet minimal `net/http` 84 | package abstractions. Each set of methods associated with a resource is kept in 85 | a separate file (such as [activity.go][]), with all key interfaces and methods 86 | defined in the main [github.go][] file. 87 | 88 | []: https://github.com/google/go-github 89 | []: https://github.com/google/go-github/blob/master/github/activity.go 90 | []: https://github.com/google/go-github/blob/master/github/github.go 91 | 92 | The problem is that we need to infer what resource and method are being called 93 | from the request URI. Not a trivial task in Go. I started with the main request 94 | handler, using gorilla's [mux][] as my routing library, and parsing the 95 | necessary parts to make the method call: 96 | 97 | []: https://github.com/gorilla/mux 98 | 99 | ```go 100 | func APIGateway(w http.ResponseWriter, request *http.Request) { 101 | body, err := ioutil.ReadAll(request.Body) 102 | if err != nil { 103 | http.Error(w, err.Error(), http.StatusInternalServerError) 104 | } 105 | apiCall := new(APICall) 106 | if err := json.Unmarshal(body, &apiCall.Payload); err != nil { 107 | http.Error(w, err.Error(), http.StatusInternalServerError) 108 | } 109 | methodParts := strings.Split(mux.Vars(request)["method"], "/") 110 | resource := strings.Title(methodParts[0]) 111 | method := translatePathToMethod(methodParts[1]) 112 | apiClient := NewClient() 113 | data, _, err := apiClient.CallMethodByName(resource, method, apiCall.Payload) 114 | parsedData, err := json.Marshal(&data) 115 | if err != nil { 116 | http.Error(w, err.Error(), http.StatusInternalServerError) 117 | } 118 | w.Header().Set("Content-Type", "application/json") 119 | log.Println(string(parsedData)) 120 | io.WriteString(w, string(parsedData)) 121 | } 122 | 123 | func main() { 124 | router := mux.NewRouter().StrictSlash(true) 125 | router.HandleFunc("/api/{method:.*}", APIGateway) 126 | log.Println("Running API gateway at port 4000") 127 | log.Fatal(http.ListenAndServe(":4000", router)) 128 | } 129 | ``` 130 | 131 | Several helper functions and type definitions have been omitted for brevity. 132 | The most challenging part is of course `CallMethodByName()`. This did take a 133 | lengthy research but thanks to [this StackOverflow thread][] and subsequent 134 | reading of [The Laws of Reflection][], I was able to put it together below. 135 | The cool thing about Go's [reflect package][] is that it can give access to 136 | nearly everything in the language, making it as malleable as JavaScript if 137 | you manage to wrap your head around it. 138 | 139 | []: https://stackoverflow.com/questions/14116840/dynamically-call-method-on-interface-regardless-of-receiver-type?rq=1 140 | []: https://blog.golang.org/laws-of-reflection 141 | []: https://golang.org/pkg/reflect/ 142 | 143 | ```go 144 | func (c *Client) CallMethodByName( 145 | resource string, 146 | method string, 147 | payload json.RawMessage, 148 | ) ( 149 | *json.RawMessage, 150 | *Response, 151 | error, 152 | ) { 153 | resourceObj, err := resourceByName(c, resource) 154 | if err != nil { 155 | log.Fatal(err) 156 | } 157 | methodFunc, err := methodByName(resourceObj, method) 158 | if err != nil { 159 | log.Fatal(err) 160 | } 161 | in := []reflect.Value{ 162 | reflect.ValueOf(context.Background()), 163 | reflect.ValueOf(payload), 164 | } 165 | results := methodFunc.Call(in) 166 | data := results[0].Interface().(*json.RawMessage) 167 | response := results[1].Interface().(*Response) 168 | return data, response, nil 169 | } 170 | ``` 171 | 172 | As you can see, values are passed to the final method in the form of a 173 | `reflect.Value` slice. Return values are then cast back to their expected 174 | types before returning. Our code is still evolving, and there are of course 175 | several potential errors that need to be addressed, but it now successfully 176 | translates `resource/get-something` to `Resource.GetSomething()` and all you 177 | have to do is add the service definitions and methods you'll use. 178 | 179 | A working boilerplate is available at [github.com/stored/pathway][]. 180 | Pull requests are very much welcome. 181 | 182 | []: http://github.com/stored/pathway 183 | -------------------------------------------------------------------------------- /entries/leaving-python/leaving-python.entry: -------------------------------------------------------------------------------- 1 | 2 | August 25, 2017 3 | 4 | A piece on my new web development stack following 5 | my ongoing work on three Nuxt applications 6 | 7 | # Leaving Python for JavaScript 8 | 9 | After over 10 years using Python as main my programming language, I have moved 10 | on to JavaScript (the [ES8][] specification). Python still remains my second 11 | main language, as I'm actively involved in a Django-powered project, still 12 | maintain [xmlwitch][] and use tons of private scripts written in Python every 13 | day. But it's true, I can now say I really prefer JavaScript over Python. And 14 | it's not just me — [everyone seems to be following suit][]. 15 | 16 | []: https://hackernoon.com/es8-was-released-and-here-are-its-main-new-features-ee9c394adf66 17 | []: https://github.com/galvez/xmlwitch 18 | []: https://dev.to/anthonydelgado/javascript-is-eating-the-world 19 | 20 | Not only as a tool that helps me deliver working products, but also as a 21 | language, for the sheer satisfaction I have working with it. The thought might 22 | be heretic to some good Pythonistas I know — I was shocked myself when I read 23 | about [Ian Bicking moving on too][] — but it doesn't take much to explain it. 24 | 25 | []: http://www.ianbicking.org/blog/2014/02/saying-goodbye-to-python.html 26 | 27 | While indentation-based scoping has always been a huge plus for me in Python, 28 | class definition boilerplate is still hard to look at. List comprehensions and 29 | generator expressions go a long a way, but unable to beat the expressiveness of 30 | JavaScript for me. 31 | 32 | Also, **there's no acceptable way to pass a function body to another in Python**. 33 | My code usually revolves around _higher order functions_, `reduce()`, `map()`, 34 | `forEach()` etc. I can't remember the last time I wrote a regular for loop. 35 | 36 | So with JavaScript you've got [arrow functions][], the [method shorthand 37 | definition syntax][], the [spread operator][], [destructuring assignments][], 38 | [all functional Array methods][] and [async functions][]. Combined with 39 | [Vue][]'s minimal patterns and [Nuxt][]'s conventions, I can't think of a 40 | better language to write web applications with. 41 | 42 | []: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions 43 | []: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions 44 | []: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Spread_operator 45 | []: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment 46 | []: http://eloquentjavascript.net/1st_edition/chapter6.html 47 | []: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function 48 | []: https://vuejs.org/ 49 | []: https://nuxtjs.org 50 | 51 | ## Backend 52 | 53 | The last project I started using a [Flask][] backend was a little over three 54 | months ago. Since then, I worked on so many client-side **and** server-side 55 | rendered JavaScript applications that I've grown completely accostumed to the 56 | ways of [Koa][] and [async/await][]. 57 | 58 | []: http://flask.pocoo.org/ 59 | []: http://koajs.com/ 60 | []: https://hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9 61 | 62 | [Nuxt][] is very opinionated but has very hackable internals. I would describe 63 | [Koa][] as meticulously minimalist and precise, it's definitely very pleasant 64 | to work with and has proven to be production-ready on a number of projects. 65 | 66 | []: https://nuxtjs.org 67 | []: http://koajs.com/ 68 | 69 | When working with Nuxt, I usually place server-side only (non-Nuxt) pieces 70 | under `/api` (with [koa-router][]) and then prevent [`nuxt.render()`][] from 71 | running under that route. 72 | 73 | []: https://github.com/alexmingoia/koa-router 74 | []: https://nuxtjs.org/api/nuxt-render/ 75 | 76 | ```js 77 | app.use((ctx) => { 78 | if (!ctx.request.path.startsWith('/api')) { 79 | ctx.status = 200 80 | return new Promise((resolve, reject) => { 81 | ctx.res.on('close', resolve) 82 | ctx.res.on('finish', resolve) 83 | nuxt.render(ctx.req, ctx.res, (promise) => { 84 | promise.then(resolve).catch(reject) 85 | }) 86 | }) 87 | } 88 | }) 89 | ``` 90 | 91 | My `package.json` basic dependencies are: [dotenv][] (lets you load the 92 | environment from an `.env` file), [axios][] (HTTP client library), [cheerio][] 93 | (HTML parsing library), [bcrypt][] (password hashing), [co-body][] (HTTP body 94 | parser), [co-busboy][] (HTTP multipart parser), [jsonwebtoken][] (JWT 95 | generator), [koa-jwt][] (JWT middleware), [koa-router][], [koa-sslify][], 96 | [vue-no-ssr][] and [source-map][]. 97 | 98 | []: https://www.npmjs.com/package/dotenv 99 | []: https://www.npmjs.com/package/axios 100 | []: https://www.npmjs.com/package/cheerio 101 | []: https://www.npmjs.com/package/bcrypt 102 | []: https://www.npmjs.com/package/co-body 103 | []: https://www.npmjs.com/package/co-busboy 104 | []: https://www.npmjs.com/package/jsonwebtoken 105 | []: https://www.npmjs.com/package/koa-jwt 106 | []: https://github.com/alexmingoia/koa-router 107 | []: https://www.npmjs.com/package/koa-sslify 108 | []: https://www.npmjs.com/package/vue-no-ssr 109 | []: https://www.npmjs.com/package/source-map 110 | 111 | I follow the [Twelve-Factor App][] methodology in architecting applications and 112 | recommend [Google Cloud Platform][] and [Kubernetes][] in all my projects, but 113 | have seen successful container deployments with AWS. I still need to explore 114 | **HTTP/2**, especially now [it has made into Node's core][]. 115 | 116 | []: https://12factor.net/ 117 | []: https://cloud.google.com/ 118 | []: https://kubernetes.io/ 119 | []: https://github.com/nodejs/node/pull/14811 120 | 121 | If I ever need better performance on the backend, I'm more inclined to look at 122 | Go (which I've used in the past and like very much) and [Otto][] (or [Goby][]) 123 | than Python again. Goby are Otto are incredible ideas — having a Go-powered Nuxt 124 | application would cover a much wider range of applications. 125 | 126 | []: https://github.com/robertkrimen/otto 127 | []: https://github.com/goby-lang/goby 128 | 129 | ## Frontend 130 | 131 | In addition to [Nuxt][] and [Vue][], [iView][] and [Sass][] power most of 132 | my frontend code. iView is comparable to [ElementUI][], but has worked better 133 | for my projects so far (despite [my initial excitement with ElementUI][]). 134 | Nuxt routes are automatically built from the file system for convenience, but 135 | can be easily extended. 136 | 137 | []: https://nuxtjs.org 138 | []: https://vuejs.org/ 139 | []: https://www.iviewui.com/ 140 | []: http://sass-lang.com/ 141 | []: http://element.eleme.io/ 142 | []: http://hire.jonasgalvez.com.br/2017/Jul/23/Nuxt-and-ElementUI 143 | 144 | You'll want to keep [vue-no-ssr][] around for dealing with non-SSR friendly 145 | Vue dependencies. 146 | 147 | []: https://www.npmjs.com/package/vue-no-ssr 148 | 149 | I have always avoided CSS transpilers, and while I enjoy curly braces in 150 | JavaScript (it's hard to imagine indentation-scoped JavaScript), I find them 151 | unnecessary in CSS. Eliminating the need for curly braces (and semicolons) in 152 | CSS makes the code easier to read and scroll through, **especially when using 153 | single file components as a development convention**. 154 | 155 | ## Others 156 | 157 | Nuxt's build tools have been sufficient for my projects so far. 158 | 159 | I'm keeping an eye on [Brunch][] though. I used Brunch recently and while I was 160 | presented with some of its shortcomings, I was also impressed with its speed and 161 | simplicity. Once it gets a little closer to Webpack's feature set, I wouldn't be 162 | surprised if the Nuxt team decided to migrate to it. 163 | 164 | []: http://brunch.io/ 165 | 166 | I mainly use [Sublime Text][], [ack][] and [integrated eslint][] (configured 167 | to use [StandardJS][]) for my programming, and [ColorSnapper 2][], 168 | [Sketch][] and Photoshop for my UI/UX design needs. 169 | 170 | []: https://www.sublimetext.com/ 171 | []: https://beyondgrep.com/ 172 | []: https://nuxtjs.org/guide/development-tools/ 173 | []: https://standardjs.com/ 174 | []: https://colorsnapper.com/ 175 | []: https://www.sketchapp.com/ 176 | 177 | Article cover adapted from [photo by Frank McKenna][] on [Unsplash][]. 178 | 179 | []: https://unsplash.com/photos/tjX_sniNzgQ 180 | []: https://unsplash.com 181 | -------------------------------------------------------------------------------- /pages/influences.md: -------------------------------------------------------------------------------- 1 | # Influences 2 | 3 | [Paul Graham][] is quite possibly my biggest professional influence. In fact, he 4 | influenced an entire generation of young technologists and entrepreneurs, and 5 | the effecs of his influence have been shown to be [long-lasting][]. [Beating 6 | the Averages][], [Being Popular][], [Why Nerds are Unpopular][], [The 7 | Hundred-Year Language][] and [How to Make Wealth][] are my favorite pieces. 8 | 9 | []: http://www.paulgraham.com/ 10 | []: https://web.archive.org/web/20110514015121/http://mixergy.com/y-combinator-paul-graham/ 11 | []: http://www.paulgraham.com/avg.html 12 | []: http://www.paulgraham.com/popular.html 13 | []: http://www.paulgraham.com/nerds.html 14 | []: http://www.paulgraham.com/hundred.html 15 | []: http://www.paulgraham.com/wealth.html 16 | 17 | [Claus Wahlers][] is an [old school][] Flash and ActionScript hacker (currently 18 | [kicking ass on React-land][]). He is the author of [DENG][], a crazy Flash 19 | browser implementation that implemented various W3C standards before they landed 20 | anywhere else. He also wrote [FC64][], a Commodore 64 emulator for Flash Player 9. 21 | We have all forgotten about Flash, but back in the day these were prime examples 22 | of ActionScript mastery. He mentored me in my early years as a programmer and 23 | unwillingly taught me most of my English over instant messaging. 24 | 25 | []: http://wahlers.com.br/clausblog 26 | []: http://deng.com.br/ 27 | []: https://github.com/zeit/next-plugins/pull/159 28 | []: http://codeazur.com.br/stuff/fc64_final/ 29 | 30 | [Mark Pilgrim][] blogged about technology and everything else for about 10 years 31 | before [he vanished][] from the internet. He wrote [Dive Into Accessibility][], 32 | [Dive Into Python][], [Dive Into Python 3][] and most recently, [HTML5: Up & 33 | Running][]. He also wrote the Universal Feed Parser, which I believe is the 34 | [most complete, fault-tolerant RSS and Atom parser][] out there. I spent weeks 35 | reading the source code. He was one of the main [advocates][] of the [Atom 36 | format][] during its development. 37 | 38 | []: https://web.archive.org/web/20110514015121/http://diveintomark.org/about 39 | []: https://en.wikipedia.org/wiki/Mark_Pilgrim#%22Disappearance%22_from_the_Internet 40 | []: https://web.archive.org/web/20110514015121/http://diveintoaccessibility.org/ 41 | []: https://web.archive.org/web/20110514015121/http://diveintopython.org/ 42 | []: https://www.amazon.com/Dive-into-Python-Mark-Pilgrim/dp/1430224150/ 43 | []: https://www.amazon.com/HTML5-Running-Dive-Future-Development/dp/0596806027 44 | []: https://pypi.org/project/feedparser/ 45 | []: http://tools.ietf.org/html/rfc4287#appendix-A 46 | []: https://en.wikipedia.org/wiki/Atom_(Web_standard) 47 | 48 | [Maciej Ceglowski][] makes [great web software][], writes [intriguing][] 49 | [essays][] and contributes to [real change][]. To paraphrase his own words, he 50 | lives everywhere he can. He seems to live a truly mobile life which results in 51 | the [most][] [interesting][] [little][] [stories][]. His articles on [vector 52 | space search engines][] and [bloom filters][] introduced me to code that 53 | inspired me to study, investigate and write good software. 54 | 55 | []: http://idlewords.com/about.htm 56 | []: http://pinboard.in/tour/ 57 | []: http://idlewords.com/2005/08/a_rocket_to_nowhere.htm 58 | []: http://idlewords.com/2003/12/100_years_of_turbulence.htm 59 | []: https://www.theverge.com/2018/3/8/17092684/great-slate-fundraising-congressional-campaign 60 | []: http://idlewords.com/2006/04/argentina_on_two_steaks_a_day.htm 61 | []: http://idlewords.com/2006/05/i_spy.htm 62 | []: http://idlewords.com/2004/05/attacked_by_thugs.htm 63 | []: http://idlewords.com/2003/11/a_morning_in_iceland.htm 64 | []: http://search.cpan.org/~MCEGLOWS/ 65 | []: http://www.perl.com/pub/2003/02/19/engine.html 66 | []: http://www.perl.com/pub/2004/04/08/bloom_filters.html 67 | 68 | [Joe Gregorio][] is the co-author of the [Atom Publishing Protocol 69 | specification][] and a blogger advocating [good software development 70 | practices][]. He has also authored the [URI templates specification][] and the 71 | Python libraries [httplib2][] and [mimeparse][]. Lately it seems Joe also has 72 | [left Python][] work to [focus on modern JavaScript][]. 73 | 74 | []: http://bitworking.org/news/bio 75 | []: http://www.ietf.org/rfc/rfc5023.txt 76 | []: http://bitworking.org/news/ 77 | []: http://code.google.com/p/uri-templates/ 78 | []: http://code.google.com/p/httplib2/ 79 | []: http://code.google.com/p/mimeparse/ 80 | []: http://hire.jonasgalvez.com.br/2017/Aug/25/Leaving-Python-for-JavaScript 81 | []: https://bitworking.org/news/2018/07/the-experience-driven-source-of-elements-sk 82 | 83 | [Simon Willison][] has been for a [long time][] one of my main references in 84 | Python programming. He has a good eye for exciting emergent technology, having 85 | told us to [stop everything we were doing to go learn Node.js][] a year before 86 | it started getting so [massively popular][]. 87 | 88 | []: http://simonwillison.net/about/ 89 | []: http://simonwillison.net/2002/ 90 | []: http://simonwillison.net/2009/Nov/23/node/ 91 | []: https://web.archive.org/web/20110514015121/http://nodeknockout.com/ 92 | 93 | [Mark Nottingham][] is a [one-man standards body][]. In fact, it was his blog 94 | that introduced me to the IETF in the first place. It was after reading his 95 | [Caching Tutorial][] that I bothered to learn HTTP and dive into others 96 | underlying protocols of the web. He is also the co-author of the [Atom format 97 | specification][]. His [website][] and [writings][] influenced my sense of 98 | simplicity a great deal. 99 | 100 | []: http://www.mnot.net/personal/ 101 | []: http://www.mnot.net/personal/resume.html#publications 102 | []: http://www.mnot.net/cache_docs/ 103 | []: http://www.ietf.org/rfc/rfc4287 104 | []: http://www.mnot.net/ 105 | []: http://www.mnot.net/blog/ 106 | 107 | [Tim Bray][] is one of the co-editors of the [XML specification][]. His blog 108 | covers the [recent history of modern web software development][] and has some 109 | articles you [just][] [can't][] [miss][]. 110 | 111 | []: http://www.tbray.org/ongoing/misc/Tim 112 | []: http://www.w3.org/TR/REC-xml/ 113 | []: http://www.tbray.org/ongoing/When/ 114 | []: http://www.tbray.org/ongoing/When/200x/2006/01/08/No-New-XML-Languages 115 | []: http://www.tbray.org/ongoing/When/200x/2006/05/24/On-Grids 116 | []: http://www.tbray.org/ongoing/When/200x/2009/12/28/Your-Life-Online 117 | 118 | [Ryan Tomayko][] is the founder of [lesscode.org][], a blog which advocated 119 | smarter and leaner enterprise applications through the use of open source 120 | software and scripting languages. The archives are [filled with goodies][]. Ryan 121 | also worked at GitHub for a long time. 122 | 123 | []: http://tomayko.com/about 124 | []: http://lesscode.org/ 125 | []: http://lesscode.org/archives/ 126 | 127 | [Hugh MacLeod][] has been amusing and educating me for a long time with his 128 | [cartoons][] and [guerrilla business advice][]. He's still [blogging][] and 129 | [tweeting][] like crazy. One of my essential daily reads. 130 | 131 | []: http://gapingvoid.com/about 132 | []: http://gapingvoidgallery.com/ 133 | []: http://www.amazon.com/gp/product/159184259X 134 | []: http://gapingvoid.com/ 135 | []: https://twitter.com/hughcards 136 | 137 | [Paul Ford][] is my brand of obsessive. He listened to all of the songs from 138 | SXSW 2008 and published [a review for every and each one of them][]. He came up 139 | with [200 ingeniously hilarious ways][] to say "I Love You". He [makes a 140 | difference][]. He freely ponders about [cleaning his room][] and [anything 141 | else][] [that][] [comes][] [to][] [mind][]. 142 | 143 | []: http://www.ftrain.com/ftrain_faq.html 144 | []: https://web.archive.org/web/20110514015121/http://www.themorningnews.org/archives/reviews/sixword_reviews_of_763_sxsw_mp3s.php 145 | []: https://themorningnews.org/article/how-to-say-i-love-you 146 | []: http://www.ftrain.com/i-am-making-a-difference.html 147 | []: http://www.ftrain.com/cleaning_the_room.html 148 | []: http://www.ftrain.com/Followup.html 149 | []: http://www.ftrain.com/a-semantic-web-fear.html 150 | []: http://www.ftrain.com/SortableLists.html 151 | []: http://www.ftrain.com/until_the_water_boils.html 152 | []: http://www.ftrain.com/editors-ship-dammit.html 153 | 154 | [Michael Barrish][] is quite possibly my favorite writer. [Pervert][], 155 | [Inscription][] and [Letter][] are must reads. I remember when I first read 156 | them several years ago, I think it was the very first time I stopped to think 157 | about how good content can and does come out of the web. [All of his 158 | stories][] would make a classic book. 159 | 160 | []: http://oblivio.com/about/ 161 | []: http://oblivio.com/stories/pervert.html 162 | []: http://oblivio.com/archives/04071301.html 163 | []: http://oblivio.com/stories/letter.html 164 | []: http://oblivio.com/archives/ 165 | -------------------------------------------------------------------------------- /entries/choose-vue.entry: -------------------------------------------------------------------------------- 1 | 2 | June 23, 2017 3 | 4 | Here I present the main reasons why I chose Vue over React. 5 | 6 | # Why Choose Vue.js 7 | 8 | In the past five years or so, while the web development world was [going mad 9 | with the JavaScript revolution][] – I stayed mostly indifferent, occupying 10 | myself with backend work and distracting myself with things like [Kubernetes][]. 11 | In the rare occasions I needed to write JavaScript, I'd still just use jQuery or 12 | its lightweight counterpart, [Zepto.js][], and ended up crafting a tiny custom 13 | framework for every project. 14 | 15 | []: https://hackernoon.com/how-it-feels-to-learn-javascript-in-2016-d3a717dd577f 16 | []: https://kubernetes.io 17 | []: http://zeptojs.com/ 18 | 19 | Ember and CoffeeScript were easy to ignore, as they were mostly contained within 20 | the Ruby community. But then came Angular, React, Babel and recently Webpack – 21 | and substantial paid work. Turns out CoffeeScript would indeed not last, but the 22 | community adherence to Babel (and ES6+) has been overwhelming. 23 | 24 | My involvement with frontend work started peaking again a couple years ago, just 25 | when Webpack-based builds were becoming the norm. Working as a consultant on 26 | many different projects with different tools has given me a solid perspective on 27 | each framework. Above all, it has given me the instinct to avoid complexity 28 | whenever possible. Nowadays, I follow a simple rule when adding dependencies: if 29 | the main application code is under 1000 lines, keep it in a single file. Or even 30 | a separate file for HTML, CSS and JavaScript, but still, no further abstractions. 31 | Most of the time, having fewer tabs to iterate over in my code editor makes me 32 | more productive. Having a lot of different files that add up to a small piece 33 | actually hurts productivity. 34 | 35 | I have fully incorporated ES6 features that are natively supported by Chrome and 36 | Firefox (such as `Array` functional methods and arrow functions) in my 37 | programming style, but will defer adding [Webpack][] to a project (that can 38 | make full use of [ES2017+ through Babel][]) by the same rule. 39 | 40 | []: https://webpack.github.io/ 41 | []: https://babeljs.io/docs/plugins/preset-es2017/ 42 | 43 | Before I had the chance to try anything real with Polymer, [I was amazed by Vue 44 | at its very first release][]. The thing about Vue is that its minimalism makes 45 | the API feel like a natural extension to HTML, much like a web standard. 46 | 47 | []: http://blog.evanyou.me/2014/02/11/first-week-of-launching-an-oss-project/#comment-1270294920 48 | 49 | While there's [a dozen articles comparing Vue to other frameworks][], here I 50 | present the main characteristics that make me choose **Vue** over **React**. 51 | 52 | []: https://www.google.com.br/search?q=react+vs+vue 53 | 54 | ## No need to explictly bind methods 55 | 56 | In React, you must explictly bind methods to `this` in the constructor or in 57 | event handler assignments. 58 | 59 | ```js 60 | class MyComponent extends React.Component { 61 | constructor (props) { 62 | super(props) 63 | this.myMethod = this.myMethod.bind(this) 64 | } 65 | myMethod () { 66 | console.log(this) 67 | } 68 | } 69 | ``` 70 | 71 | You also need that `super(props)` if overriding `constructor`. Vue simplifies 72 | this by not using inheritance when defining components. Even when using fancy 73 | [single file components][], all you need to do is return an object with a 74 | `methods` dictionary: 75 | 76 | []: https://vuejs.org/v2/guide/single-file-components.html 77 | 78 | ```js 79 | export default { 80 | methods: { 81 | myMethod () { 82 | console.log(this) 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | The irony is that there's [a whole segment][] in React's documentation on why 89 | composition is better than inheritance. 90 | 91 | []: https://facebook.github.io/react/docs/composition-vs-inheritance.html 92 | 93 | ## Easier state management and Vuex 94 | 95 | In React, you need to use the `setState()` to trigger rendering updates as you 96 | modify the state. In Vue, you simply assign things to `this` and, if necessary, 97 | the component's automatically rerendered. You do need to provide an initial 98 | state (`data`) for safety (and it will complain if you use undeclared properties). 99 | 100 | ```html 101 | 104 | 105 | 115 | ``` 116 | 117 | If state management needs evolve, [Vuex][] provides a clean implementation of 118 | the same pattern seen in Flux and Redux. Instead of relying on multiple files 119 | like Redux, Vuex introduces store modules, that can divide handling of different 120 | keys under the same unified state. The [store module API][] lets you specify 121 | `state`, `mutations`, `actions` and `getters` in a single object: 122 | 123 | []: https://vuex.vuejs.org/en/ 124 | []: https://vuex.vuejs.org/en/modules.html 125 | 126 | ```js 127 | const state = { 128 | currentModal: null 129 | } 130 | 131 | const getters = { 132 | currentModal: (state) => state.currentModal 133 | } 134 | 135 | const actions = { 136 | [action.OPEN_MODAL] ({commit}, modal) { 137 | commit(mutation.MODAL_OPENED, modal) 138 | } 139 | } 140 | 141 | const mutations = { 142 | [mutation.MODAL_OPENED] (state, modal) { 143 | state.currentModal = modal 144 | } 145 | } 146 | 147 | export default { 148 | state, 149 | getters, 150 | actions, 151 | mutations 152 | } 153 | ``` 154 | 155 | ## Mixins, watchers and computed properties 156 | 157 | Vue is similar to React in the sense it uses the _props-down, events-up_ model. 158 | You can define _props_ and _data_ (state) in a Vue component. But you can also 159 | very easily **watch** for changes in the state and [_preload_ components 160 | with properties from a _mixin_][]. So, following the earlier Vuex example for 161 | opening modals, we could have a `ui` mixin that listens to `currentModal`: 162 | 163 | []: https://vuejs.org/v2/guide/mixins.html 164 | 165 | ```js 166 | const ui = { 167 | computed: { 168 | ...mapState({ 169 | currentModal: (state) => state.ui.currentModal 170 | }) 171 | }, 172 | watch: { 173 | currentModal (modal) { 174 | // code to open a modal 175 | } 176 | } 177 | } 178 | 179 | export default { 180 | mixins: [ui] 181 | } 182 | ``` 183 | 184 | ## Extremely flexible templating with no JSX 185 | 186 | Despite the immense popularity of [JSX][], I find Vue's markup-based logic 187 | control to be simpler and easier to extend. Below is an example straight from 188 | React's [documentation][]: 189 | 190 | []: https://facebook.github.io/react/docs/introducing-jsx.html 191 | []: https://facebook.github.io/react/docs/jsx-in-depth.html 192 | 193 | ```js 194 | function Item (props) { 195 | return
  • {props.message}
  • ; 196 | } 197 | 198 | function TodoList () { 199 | const todos = ['finish doc', 'submit pr', 'nag dan to review'] 200 | return ( 201 | 204 | ) 205 | } 206 | ``` 207 | 208 | JSX requires you to return and compose complete elements, and will force you to 209 | use inline JavaScript to render collections (or an inline call to a function 210 | that returns JSX). In Vue, you get declarative conditional rendering as a 211 | natural extension of the markup (like Angular, without the boilerplate hell). 212 | 213 | ```html 214 | 224 | 225 | 230 | ``` 231 | 232 | ## Vue.js and Web Components 233 | 234 | Joe Gregorio's memorable [_No more JS frameworks_][] prompted me to keep an eye 235 | on the development of [Web Components][]. When I looked at [Polymer][] it 236 | felt uncomfortably more verbose than React so I stuck with Vue. 237 | 238 | []: https://bitworking.org/news/2014/05/zero_framework_manifesto 239 | []: https://www.webcomponents.org/ 240 | []: https://www.polymer-project.org/ 241 | 242 | Nowdays however web components are natively supported by most browsers and [you 243 | can use Vue to build them][]. But perhaps an even bigger point to be made is 244 | that it's important to abstract away from frameworks and simply adhere to 245 | component-driven design. Just architect your application as a series of reusable 246 | components and any half-decent framework will help you put it all together. 247 | 248 | []: https://vuejsdevelopers.com/2018/05/21/vue-js-web-component/ 249 | -------------------------------------------------------------------------------- /entries/schema-testing.entry: -------------------------------------------------------------------------------- 1 | 2 | April 13, 2016 3 | 4 | How I used Python and Kubernetes to perform large-scale, 5 | automated schema validation tests against an API. 6 | 7 | # A Million Schema Validations on Kubernetes 8 | 9 | I was tasked with the job of running a schema validation test on roughly a 10 | million API requests. The API I was testing was already live and actively used 11 | by thousands of customers which employed nearly all of its capacities. I had 12 | access to 20+ log files ranging between 200MB to 300MB each, compressed. 13 | 14 | ## Harvesting logs to infer common signatures 15 | 16 | My first attempts at parsing them made it clear they needed some serious 17 | sanitization. After experimenting with a variety of URL sanitization 18 | approaches, I set out to find a concise way to describe the endpoints I 19 | needed to test, and use such descriptions to parse valid API call signatures 20 | from the logs. Using an hypothetical API to list foods as an example, this is 21 | what it came to look like: 22 | 23 | ``` 24 | fruits 25 | /api/fruits 26 | apikey! sort items page 27 | /api/fruits/ 28 | apikey! sort items page 29 | 30 | vegetables 31 | /api/vegetables 32 | apikey! sort items page 33 | /api/vegetables/ 34 | apikey! sort items page 35 | ``` 36 | 37 | This is of course only illustrative as the actual API had dozens endpoints and 38 | potential parameter combinations that needed to be tested. I suggestively named 39 | this file `ENDPOINTS` and placed it at the root of my test suite. I used 40 | two-space indentation for structuring each endpoint description. The first line 41 | specifies the filename where sanitized URLs will be collected. The second line 42 | specifies the URI and URI parameters and the third line, query string parameters. 43 | If a parameter is required, it's suffixed with an exclamation mark. With this 44 | simple specification file, I then used [werkzeug's routing module][] to parse 45 | []: http://werkzeug.pocoo.org/docs/0.11/routing/ 46 | 47 | ```py 48 | import re 49 | from werkzeug.routing import Map, Rule 50 | 51 | def endpoints(host): 52 | map = Map() 53 | current_endpoint = None 54 | with open('ENDPOINTS') as endpoints: 55 | for line in endpoints: 56 | line = line.rstrip() 57 | if re.search('^[^\s]+', line): 58 | endpoint_group = line.strip() 59 | index = 1 60 | elif re.search('^\s+/', line): 61 | current_endpoint = Rule( 62 | line.strip(), 63 | endpoint = '%s-%s' % (endpoint_group, index) 64 | ) 65 | index += 1 66 | elif re.search('^\s+', line): 67 | current_endpoint.params = re.split('\s+', line) 68 | map.add(current_endpoint) 69 | return map.bind(host) 70 | ``` 71 | 72 | Parsing of URI parameters is already handled by werkzeug, and since they're 73 | part of the endpoint itself they're implicitly required. I store query string 74 | parameters for verification later as `params` in each `Rule` object (a custom 75 | addition). Moving on to the actual log parser, my main goals were: a) discarding 76 | malformed calls and removing junk from otherwise valid requests (sanitization) 77 | and b) avoiding repetitive API calls in the test suite. 78 | 79 | The parsing relies heavily on [`MapAdapter`][], which raises HTTP error 80 | exceptions and returns parsed URI parameters. Using the previously stored 81 | `params` list from each `Rule` object, I then ensure required parameters are 82 | present and remove all invaliad parameters (missing from each definition in 83 | `ENDPOINTS`) at the end. Also worth noting [`tqdm`][] provides a progress bar 84 | while reading the files. 85 | 86 | []: http://werkzeug.pocoo.org/docs/0.11/routing/#werkzeug.routing.MapAdapter 87 | []: https://github.com/tqdm/tqdm 88 | 89 | ```py 90 | with open(logfile, 'rb') as logfile_handler: 91 | size = os.path.getsize(logfile) 92 | with tqdm(total=size) as pbar: 93 | newpos = None 94 | oldpos = logfile_handler.tell() 95 | for line in gzip.GzipFile(fileobj=logfile_handler): 96 | newpos = logfile_handler.tell() 97 | if newpos != oldpos: 98 | pbar.update(newpos if oldpos == 0 else newpos-oldpos) 99 | oldpos = newpos 100 | request_data = re.findall('"([A-Z]+)\s+(.*?)\s+HTTP/1.1', line) 101 | if len(request_data): 102 | uri = request_data[0][1] 103 | try: 104 | parsed_uri = urlparse.urlparse(uri) 105 | rule, args = endpoints.match(parsed_uri.path, return_rule=True) 106 | except NotFound, RequestRedirect: 107 | continue 108 | if random() > 0.0009: 109 | continue 110 | parsed_query = cgi.parse_qs(parsed_uri.query) 111 | if getattr(rule, 'params', False): 112 | required_params = [ 113 | param[:-1] 114 | for param in rule.params 115 | if param.endswith('!') 116 | ] 117 | optional_params = [ 118 | param 119 | for param in rule.params 120 | if not param.endswith('!') 121 | ] 122 | else: 123 | required_params, optional_params = [], [] 124 | all_parameters = set(required_params+optional_params) 125 | for param in set(parsed_query.keys()).difference(all_parameters): 126 | del parsed_query[param] 127 | ``` 128 | 129 | Since each file had millions of lines, in order to generate a manageably sized 130 | data set I gradually lowered the exclusion rate until it got to about 200 131 | results from each file. But there were still a lot of duplicates. I figured I 132 | didn't need to test the same API call signature (combination of parameters used) 133 | more than a few times. So I used Python's [native hash function][] to generate 134 | a unique signature built from the `Rule` definition and a hash of all parameters 135 | passed to each call. Picking up from the previous snippet: 136 | 137 | []: https://docs.python.org/2/library/functions.html#hash 138 | 139 | ```py 140 | keyset = frozenset(parsed_query.keys()) 141 | signature = '%s%s' % (rule.rule, hash(keyset)) 142 | if signatures.get(signature): 143 | if len(keyset) == 0 and signatures[signature] > 0: 144 | continue 145 | if signatures[signature] > 5: 146 | continue 147 | signatures[signature] += 1 148 | else: 149 | signatures[signature] = 1 150 | ``` 151 | 152 | If the parser sees an API call and the number of times it's been collected 153 | doesn't exceed the limit, it's then reassembled with the sanitized parameters 154 | and written to disk under a directory named after the log file, and the filename 155 | spcified in the `ENDPOINTS` file. 156 | 157 | ```py 158 | cleaned_query = { 159 | key: parsed_query[key][0] 160 | for key in parsed_query.keys() 161 | } 162 | sample_filename = os.path.join(samplesdir, '%s.sample' % rule.endpoint) 163 | cleaned_uri = '%s?%s' % ( 164 | parsed_uri.path, 165 | urllib.urlencode(cleaned_query) 166 | ) 167 | with open(sample_filename, 'a+') as sample: 168 | sample.write('%s\n' % cleaned_uri) 169 | ``` 170 | 171 | To run multiple instances of it in parallel I just used [`screen`][]. Starting 172 | too many instances at once seemed to cause a few processes to crash (this was a 173 | virtualised development server), so I divided it in batches (in `${A[@]:x:y}`, 174 | `x` is the starting index and `y` is the number of items to slice): 175 | 176 | []: https://www.gnu.org/software/screen/ 177 | 178 | ```sh 179 | logfiles=(~/logs/*.gz) 180 | for file in ${logfiles[@]:0:5}; do 181 | screen -dmS $(basename $file) fab harvest:$file 182 | done 183 | ``` 184 | 185 | Running screen with `-dMS` sets a custom title for each session (in this case, 186 | the log filename) and detaches it from the terminal before running. To monitor 187 | I would attach any of the running sessions (e.g., `screen -r 24124`), which 188 | would show me the progress bar and an exit message if done: 189 | 190 | ``` 191 | 100%|█████████████████████████| 309156334/309156334 [48:12<00:00, 106864.79it/s] 192 | Press any key to exit. 193 | ``` 194 | 195 | The next and final step was to _map and reduce_ the results. First, concatenate 196 | results from all directories into `mapped/`, then `sort -u` all of them into 197 | `reduced/`. Doesn't make this step of the process scalable, but definitely good 198 | enough for harvesting a couple dozen log files. 199 | 200 | ```sh 201 | mkdir -p mapped reduced 202 | cat */fruits.sample > mapped/fruits.sample 203 | cat */vegetables.sample > mapped/vegetables.sample 204 | sort -u mapped/fruits.sample > reduced/fruits.sample 205 | sort -u mapped/vegetables.sample > reduced/vegetables.sample 206 | ``` 207 | 208 | ## Saving responses from Load Test 209 | 210 | [Mark Nottingham][] has a [seminal starting point][] on the subject, bringing 211 | attention to the necessity of verifying the test server's bandwidth and making 212 | sure you don't get too close to the limit, among many other sensible 213 | recommendations. Mark's post led me to [autobench][] and many others tools, 214 | such as [siege][], [loads][], [funkload][], [blitz][] (SaaS), [gatling][] 215 | and [tsung][]. None of them came close to the elegance and simplicity of 216 | [Locust][] though, which aside from being written in modern Python, has a 217 | clear, concise API and built-in support for running a [distributed cluster][]. 218 | 219 | []: https://www.mnot.net/ 220 | []: https://www.mnot.net/blog/2011/05/18/http_benchmark_rules 221 | []: http://www.xenoclast.org/autobench/ 222 | []: https://www.joedog.org/siege-home/ 223 | []: https://github.com/loads 224 | []: http://funkload.nuxeo.org/ 225 | []: http://blitz.io/ 226 | []: http://gatling-tool.org/ 227 | []: http://tsung.erlang-projects.org/user_manual/index.html 228 | []: http://locust.io/ 229 | []: http://docs.locust.io/en/latest/running-locust-distributed.html 230 | 231 | To use it you need to [write a locustfile][], which is just a Python file with 232 | at least one [Locust subclass][]. Each Locust object specifies a list of tasks 233 | to be performed during the test, represented by a [`TaskSet`][] class. 234 | 235 | []: http://docs.locust.io/en/latest/writing-a-locustfile.html 236 | []: http://docs.locust.io/en/latest/api.html#locust-class 237 | []: http://docs.locust.io/en/latest/api.html#taskset-class 238 | 239 | Although you can use `TaskSet` objects to build a test suite, I believe it falls 240 | short in comparison to the [myriad of benefits of pytest][]. I chose to let 241 | Locust only do the load test and save responses from the API, for further 242 | consumption by a pytest suite subsequently. The file system would have been 243 | fine but since I intended to run Locust distributedly, I used Redis as 244 | storage for the responses. `TaskSet` methods are dynamically generated from 245 | the reduced list of URIs. 246 | 247 | []: http://pytest.org/latest/ 248 | 249 | ```py 250 | import os 251 | import redis 252 | import glob 253 | import functools 254 | from locust import HttpLocust, TaskSet 255 | 256 | BASE = os.path.abspath(os.path.dirname(__file__)) 257 | 258 | def _save_response(uri, tset): 259 | response = tset.client.get(uri) 260 | tset.redis.hset('responses', uri, response.content) 261 | 262 | class ResponseSaver(TaskSet): 263 | def __init__(self, parent): 264 | super(ResponseSaver, self).__init__(parent) 265 | self.tasks = list(self.gen_tasks()) 266 | self.redis = redis.Redis() 267 | def gen_tasks(self): 268 | samples = glob.glob('%s/samples/reduced/*.sample' % BASE) 269 | for sample in samples: 270 | with open(sample) as handler: 271 | for line in handler: 272 | yield functools.partial(_save_response, line.strip()) 273 | 274 | class APIUser(HttpLocust): 275 | task_set = ResponseSaver 276 | host = "http://api.host" 277 | ``` 278 | 279 | Locust is currently based on gevent, but I wouldn't be surprised if they upgrade 280 | it to [aiohttp][] which [makes use][] of [PEP 3156][] and [PEP 492][]. 281 | 282 | []: http://aiohttp.readthedocs.org/en/stable/ 283 | []: http://pawelmhm.github.io/asyncio/python/aiohttp/2016/04/22/asyncio-aiohttp.html 284 | []: https://www.python.org/dev/peps/pep-3156/ 285 | []: https://www.python.org/dev/peps/pep-0492/#await-expression 286 | 287 | ## Running test suite against responses 288 | 289 | Now that we got past load testing and got our [pretty graphs][] from Locust, we 290 | can proceed to writing some actual unit tests. The primary goal was to test data 291 | integrity, i.e., validate schemas of all JSON responses. That's where 292 | [marshmallow][] comes in. It's a ORM/ODM/framework-agnostic library that allows 293 | you to cleanly define a schema and validate a JSON document against it. It's 294 | better than manually validating each response, but I still found it too verbose 295 | to maintain. I wanted something simple and concise as the `ENDPOINTS` file. So 296 | I came up with another small DSL, a thin layer on top of marshmallow: 297 | 298 | []: https://truveris.github.io/articles/locust/ 299 | []: https://marshmallow.readthedocs.org/en/latest/ 300 | 301 | ``` 302 | success:bool data:{name:str link:url vitamins,minerals:str[]} 303 | /api/fruits 304 | /api/fruits/ 305 | /api/vegetables 306 | /api/vegetables/ 307 | ``` 308 | 309 | I called this `SCHEMAS`. By now it's clear I have a knack for DSLs. The syntax 310 | is fairly straightforward: `field:type`, where `field` can be a comma-separated 311 | list, and `type` can be followed by `[]` to indicate an array and `?` to 312 | indicate it's optional (since optional parameters were more common in 313 | `ENDPOINTS`, it used `!` instead, to indicate which fields were required). To 314 | specify a nested schema within each response (for instance, an array of objects 315 | under `data`), you can enclose its fields with `{}` where a single `type` would 316 | normally be defined. 317 | 318 | Like in `ENDPOINTS`, single space indentation for listing endpoints to validate 319 | schemas against. To match URIs, again I used `werkzeug.routing`. 320 | 321 | Following next is `validation.py`, where `schemas()` is responsible for parsing 322 | the `SCHEMAS` file and returning a validation function that will know what 323 | schema to use according to the URL it gets. The validation function takes 324 | `uri` and `response` as parameters, both strings. 325 | 326 | ```py 327 | def schemas(host='api.host'): 328 | map = Map() 329 | with open('%s/SCHEMAS' % BASE) as schemas: 330 | for line in schemas: 331 | line = line.rstrip() 332 | if re.search('^[^\s]+', line): 333 | schema = _gen_schema(line) 334 | elif re.search('^\s+', line): 335 | endpoint = re.sub('\W+', '-', line) 336 | rule = Rule(line.strip(), endpoint=endpoint) 337 | rule.schema = schema 338 | map.add(rule) 339 | _validate.matcher = map.bind(host) 340 | return _validate 341 | 342 | def _validate(uri, response): 343 | try: 344 | rule, args = _validate.matcher.match(uri, return_rule=True) 345 | data, errors = rule.schema().loads(response) 346 | return errors 347 | except NotFound, RequestRedirect: 348 | return None 349 | 350 | def _gen_schema(spec): 351 | fields = [] 352 | for nested in re.findall('([\w,]+):{(.+)}(\??)', spec.strip()): 353 | for field in nested[0].split(','): 354 | fields.append([field, _gen_schema._handle_nested(nested[1]), not len(nested[2])]) 355 | {% raw %}spec = spec.replace('%s:{%s}' % nested[:2], ''){% endraw %} 356 | spec = spec.replace(' ', ' ') 357 | fields += _gen_schema._handle_nested(spec) 358 | for index, value in enumerate(fields): 359 | if type(value[1]) is list: 360 | nested = _gen_schema._make_schema(dict(value[1])) 361 | fields[index] = [value[0], Nested(nested, required=not value[2], many=True)] 362 | return _gen_schema._make_schema(fields) 363 | 364 | def _make_schema(fields): 365 | if type(fields) is list: 366 | fields = dict(fields) 367 | ts = str(time.time()).replace('.', '') 368 | return type('schema_%s' % ts, (Schema,), fields) 369 | 370 | def _handle_nested(spec): 371 | fields = [] 372 | for group in re.findall('([\w,]+):([\w\[\]]+)(\??)', spec): 373 | for field in group[0].split(','): 374 | ftype = None 375 | if group[1].endswith('[]'): 376 | value = _gen_schema._handle_list(group[1], not len(group[2])) 377 | fields.append([field, value]) 378 | else: 379 | value = _gen_schema._type_hash[group[1]](required=not len(group[2])) 380 | fields.append([field, value]) 381 | return fields 382 | 383 | def _handle_list(ltype, required): 384 | return List(_gen_schema._type_hash[ltype.split('[]')[0]], required=required) 385 | 386 | _gen_schema._type_hash = { 387 | 'str': Str, 388 | 'bool': Bool, 389 | 'int': Int, 390 | 'dt': DateTime, 391 | 'url': URL 392 | } 393 | 394 | _gen_schema._make_schema = _make_schema 395 | _gen_schema._handle_nested = _handle_nested 396 | _gen_schema._handle_list = _handle_list 397 | ``` 398 | 399 | Finally we get to **tests.py**, which will import `schemas()` and use it to 400 | validate schemas of all responses saved in Redis: 401 | 402 | ```py 403 | import pytest 404 | import redis 405 | from validation import schemas 406 | from concurrent.futures import ThreadPoolExecutor 407 | from os.path import abspath, dirname 408 | 409 | def test_schemas(): 410 | validate = schemas() 411 | def _validate_schema(uri): 412 | response = REDIS.hget('responses', uri) 413 | return validate(uri, response), uri 414 | with ThreadPoolExecutor(max_workers=64) as executor: 415 | for result in executor.map(_validate_schema, REDIS.hgetall('responses')): 416 | if result[0] is not None: 417 | assert not len(result[0].keys()), result[1] 418 | 419 | BASE = abspath(dirname(__file__)) 420 | REDIS = redis.Redis() 421 | ``` 422 | 423 | ## Deployment and Scaling with Kubernetes 424 | 425 | [I first experimented][] with Kubernetes in 2015 while migrating an 426 | application from AWS to [GCP][]. Back then it was still in its early stages 427 | and I ran into problems so mysterious to debug that I opted for regular 428 | [Compute Engine][] instances. 429 | 430 | []: https://github.com/kubernetes/kubernetes/issues/5410 431 | []: https://cloud.google.com/ 432 | []: https://cloud.google.com/compute/ 433 | 434 | But time passed and Kubernetes evolved. Interest in the platform [keeps 435 | trending up][1] as it nears its 1.3 release. 436 | 437 | [1]: https://www.google.com/trends/explore#q=Kubernetes 438 | 439 | Johan Haleby has [a compelling case in Kubernetes' support][], listing 440 | shortcomings with other container orchestration offerings: [AWS ECS][] 441 | (security group hassle, lack of service discovery and port management), 442 | [Tutum][] (unable to reschedule containers from a failed node), 443 | [Docker Swarm][] (not a managed service, insufficient API for a cluster), 444 | [Mesosphere DCOS][] (not a managed service, multi-cloud capabilitites only 445 | available in the paid version). 446 | 447 | []: http://code.haleby.se/2016/02/12/why-we-chose-kubernetes/ 448 | []: https://aws.amazon.com/ecs/getting-started/ 449 | []: https://www.tutum.co/ 450 | []: https://www.docker.com/products/docker-swarm 451 | []: https://mesosphere.com/ 452 | 453 | Despite initially considering Docker Swarm for its alignment to the Docker API, 454 | I decided to revisit Kubernetes and stuck with it. 455 | 456 | As far as development and operations go, I believe Kubernetes is going to be a 457 | bigger standard than Docker. There already is [support for Rocket containers][] 458 | and there are [very good reasons][] to try it. 459 | 460 | []: http://www.theregister.co.uk/2015/05/04/kubernetes_rkt_integration/ 461 | []: http://cloudtweaks.com/2015/03/docker-vs-rocket-container-technology/ 462 | 463 | Kubernetes has a [rather elaborate jargon][], but in short, it lets you specify 464 | groups of containers ([pods][]), replicated groups of containers 465 | ([replication][] controllers) and load balancers ([services][]), much like you 466 | can write a Dockerfile specifying a single container. With replication 467 | controllers, Kubernetes allows you to scale resources without downtime when 468 | needed, while also providing reliable failure resilience by automatically 469 | watching over running pods and ensuring they match the desired count, i.e., new 470 | pods are automatically spawn if any of them go down for any reason. Replication 471 | controllers are generally better than pods because they ensure capacity. Even 472 | when using a single node, it guarantees resilience if it fails for any reason. 473 | 474 | []: https://github.com/hazbo/kubernetes-overview 475 | []: http://kubernetes.io/docs/user-guide/pods/ 476 | []: http://kubernetes.io/docs/user-guide/replication-controller/ 477 | []: http://kubernetes.io/docs/user-guide/services/ 478 | 479 | I used three replication controllers, one for Redis and the others for the 480 | Locust master node and Locust slave nodes respectively, and two services to 481 | allow Locust to connect to Redis, and Locust slaves to connect to the master. 482 | 483 | Kubernetes requires you to upload your container images to [Docker Hub][] or 484 | in the case of [Container Engine][], its own [private container registry][]. 485 | 486 | []: https://hub.docker.com/ 487 | []: https://cloud.google.com/container-engine/ 488 | []: https://cloud.google.com/container-registry/ 489 | 490 | It's recommended to have behaviour set through environment variables (see 491 | [The Twelve-Factor App][1] for best practices in deployment) and data made 492 | available via [volumes][2], also carefully specified in the Kubernetes API. 493 | 494 | [1]: http://12factor.net/ 495 | [2]: http://kubernetes.io/v1.0/docs/user-guide/volumes.html 496 | 497 | The master replication controller is defined as follows: 498 | 499 | ``` 500 | kind: ReplicationController 501 | apiVersion: v1 502 | metadata: 503 | name: testrunner-master 504 | labels: 505 | name: testrunner 506 | role: master 507 | spec: 508 | replicas: 1 509 | selector: 510 | name: testrunner 511 | role: master 512 | template: 513 | metadata: 514 | labels: 515 | name: testrunner 516 | role: master 517 | spec: 518 | containers: 519 | - name: testrunner 520 | image: gcr.io/<id>/testrunner:latest 521 | env: 522 | - name: TESTRUNNER_ROLE 523 | key: TESTRUNNER_ROLE 524 | value: master 525 | ports: 526 | - name: tr-port-8089 527 | containerPort: 8089 528 | protocol: TCP 529 | - name: tr-port-5557 530 | containerPort: 5557 531 | protocol: TCP 532 | - name: tr-port-5558 533 | containerPort: 5558 534 | protocol: TCP 535 | ``` 536 | 537 | Below is the slave replication controller: 538 | 539 | ``` 540 | kind: ReplicationController 541 | apiVersion: v1 542 | metadata: 543 | name: testrunner-slaves 544 | labels: 545 | name: testrunner 546 | role: slave 547 | spec: 548 | replicas: 10 549 | selector: 550 | name: testrunner 551 | role: slave 552 | template: 553 | metadata: 554 | labels: 555 | name: testrunner 556 | role: slave 557 | spec: 558 | containers: 559 | - name: testrunner 560 | image: gcr.io/<id>/testrunner:latest 561 | env: 562 | - name: TESTRUNNER_ROLE 563 | key: TESTRUNNER_ROLE 564 | value: slave 565 | - name: TESTRUNNER_MASTER 566 | key: TESTRUNNER_MASTER 567 | value: testrunner-master 568 | ``` 569 | 570 | And finally, the master service definition: 571 | 572 | ``` 573 | kind: Service 574 | apiVersion: v1 575 | metadata: 576 | name: testrunner-master 577 | labels: 578 | name: testrunner 579 | role: master 580 | spec: 581 | ports: 582 | - port: 8089 583 | targetPort: tr-port-8089 584 | protocol: TCP 585 | name: tr-port-8089 586 | - port: 5557 587 | targetPort: tr-port-5557 588 | protocol: TCP 589 | name: tr-port-5557 590 | - port: 5558 591 | targetPort: tr-port-5558 592 | protocol: TCP 593 | name: tr-port-5558 594 | selector: 595 | name: locust 596 | role: master 597 | type: LoadBalancer 598 | ``` 599 | 600 | Notice how in the slave replication controller, the `TESTRUNNER_MASTER` 601 | environment variable which is passed to the container is set to 602 | `testrunner-master`, a hostname made available by the service definition above. 603 | Also, all service `targetPort` values match the ones defined in the 604 | [targeted][1] replication controller. Redis is made available to 605 | the Python processes the very same way, through a service and replication 606 | controller in Kubernetes. 607 | 608 | [1]: http://kubernetes.io/docs/user-guide/labels/ 609 | 610 | On the testrunner's Dockerfile, `ENTRYPOINT` is set to run `scripts/main.sh` 611 | which picks up the environment variables and starts the locust process in 612 | the appropriate mode. 613 | 614 | ``` 615 | #!/bin/bash 616 | if [[ "$TESTRUNNER_ROLE" = "master" ]]; then 617 | locust --master 618 | elif [[ "$TESTRUNNER_ROLE" = "slave" ]]; then 619 | locust --slave --master-host=$TESTRUNNER_MASTER 620 | fi 621 | ``` 622 | 623 | Once you end a Locust run and have your API responses saved, you can SSH to the 624 | master node and run the py.test suite from the command-line, or, run it locally 625 | or elsewhere by simply enabling remote access to the Redis service. 626 | 627 | This article was mostly illustrative, but you can find supporting source code 628 | and more implementation details in [GCP's sample Locust project][1]. It doesn't 629 | include any of the log harvesting and schema validation code from this article, 630 | but it can get you a cluster running in no time. 631 | 632 | [1]: https://github.com/GoogleCloudPlatform/distributed-load-testing-using-kubernetes 633 | --------------------------------------------------------------------------------