I'm Jonas Galvez,
21 | a JavaScript and Go engineer at STORED
22 | e-commerce. I started my carreer as an ActionScript developer 18 years
23 | ago. Since then, I've had a 10-year long affair with Python and have now
24 | returned to ECMAScript land.
38 |
39 |
40 |
41 |
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('
')
88 |
89 | for post in postList[:20]: # last 20 items
90 | date = time.gmtime(post.date)
91 | date = time.strftime('%Y-%m-%d %H:%M:%S', date)
92 | item = '\t
')
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 |
103 |
107 |
108 |
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 |
nuxt.config.js
22 |
Supercharged with functions that load .entry files from a
23 | entries/ directory. Generates RSS and Atom feeds using
24 | .template files under feeds/.
25 |
26 |
27 |
plugins/nuxpress.js
28 |
Uses 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.
32 |
33 |
34 |
middleware/nuxpress.js
35 |
Infers 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.
38 |
39 |
40 |
pages/index.vue
41 |
asyncData() makes entries available to
42 | the template. Entries are in listed in descending order.
43 |
44 |
45 |
pages/entry.vue
46 |
asyncData() makes entry available to
47 | the template, previously provided by the nuxpress middleware.
48 |
49 |
50 |
pages/page.vue
51 |
asyncData() makes page available to
52 | the template, previously provided by the nuxpress middleware.
53 |
54 |
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 |
pages/*
96 |
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.
99 |
100 |
101 |
feeds/*
102 |
Lodash templates for RSS and Atom feeds.
103 |
104 |
105 |
layouts/default.vue
106 |
Nuxt's default layout, set up to use components/LeftColumn.vue
107 |
108 |
109 |
components/LeftColumn.vue
110 |
The left column in the default two-column layout. This can be removed
111 | altogether and replaced by whatever layout structure you're using.
112 |
113 |
114 |
head.js
115 |
Base HTML head definitions used by pages/*.
116 |
117 |
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 |
102 |
{{ message }}
103 |
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 |
202 | {todos.map((message) => )}
203 |
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 |
226 |
227 |
228 |
229 |
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 |
--------------------------------------------------------------------------------