lam 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +107 -0
  7. data/Guardfile +22 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +37 -0
  10. data/Rakefile +6 -0
  11. data/bin/lam +14 -0
  12. data/lam.gemspec +32 -0
  13. data/lib/lam.rb +14 -0
  14. data/lib/lam/base_controller.rb +54 -0
  15. data/lib/lam/build.rb +43 -0
  16. data/lib/lam/build/handler_generator.rb +34 -0
  17. data/lib/lam/build/lambda_deducer.rb +47 -0
  18. data/lib/lam/build/templates/handler.js +156 -0
  19. data/lib/lam/build/traveling_ruby.rb +108 -0
  20. data/lib/lam/cli.rb +23 -0
  21. data/lib/lam/cli/help.rb +19 -0
  22. data/lib/lam/command.rb +25 -0
  23. data/lib/lam/process.rb +18 -0
  24. data/lib/lam/process/base_processor.rb +23 -0
  25. data/lib/lam/process/controller_processor.rb +36 -0
  26. data/lib/lam/process/help.rb +11 -0
  27. data/lib/lam/process/processor_deducer.rb +52 -0
  28. data/lib/lam/util.rb +13 -0
  29. data/lib/lam/version.rb +3 -0
  30. data/notes/design.md +43 -0
  31. data/notes/traveling-ruby-packaging-lam.md +26 -0
  32. data/notes/traveling-ruby-packaging.md +103 -0
  33. data/notes/traveling-ruby.md +82 -0
  34. data/spec/fixtures/project/.gitignore +3 -0
  35. data/spec/fixtures/project/.ruby-version +1 -0
  36. data/spec/fixtures/project/Gemfile +4 -0
  37. data/spec/fixtures/project/Gemfile.lock +35 -0
  38. data/spec/fixtures/project/app/controllers/application_controller.rb +2 -0
  39. data/spec/fixtures/project/app/controllers/posts_controller.rb +12 -0
  40. data/spec/fixtures/project/bin/lam +22 -0
  41. data/spec/fixtures/project/handlers/controllers/posts.js +156 -0
  42. data/spec/lib/cli_spec.rb +20 -0
  43. data/spec/lib/lam/base_controller_spec.rb +18 -0
  44. data/spec/lib/lam/build/lambda_deducer_spec.rb +20 -0
  45. data/spec/lib/lam/build_spec.rb +29 -0
  46. data/spec/lib/lam/process/controller_processor_spec.rb +22 -0
  47. data/spec/lib/lam/process/infer_spec.rb +24 -0
  48. data/spec/lib/lam/process_spec.rb +18 -0
  49. data/spec/spec_helper.rb +25 -0
  50. metadata +249 -0
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ // handler: <%= @handler %>
4
+ const spawn = require('child_process').spawn;
5
+
6
+ // Once hooked up to API Gateway can use the curl command to test:
7
+ // curl -s -X POST -d @event.json https://endpoint | jq .
8
+
9
+ // Filters out lines so only the error lines remain.
10
+ // Uses the "RubyError: " marker to find the starting error lines.
11
+ //
12
+ // Input: String
13
+ // random line
14
+ // RubyError: RuntimeError: error in submethod
15
+ // line1
16
+ // line2
17
+ // line3
18
+ //
19
+ // Output: String
20
+ // RubyError: RuntimeError: error in submethod
21
+ // line1
22
+ // line2
23
+ // line3
24
+ function filterErrorLines(text) {
25
+ var lines = text.split("\n")
26
+ var markerIndex = lines.findIndex(line => line.startsWith("RubyError: ") )
27
+ lines = lines.filter((line, index) => index >= markerIndex )
28
+ return lines.join("\n")
29
+ }
30
+
31
+ // Produces an Error object that displays in the AWS Lambda test console nicely.
32
+ // The backtrace are the ruby lines, not the nodejs shim error lines.
33
+ // The json payload in the Lambda console looks something like this:
34
+ //
35
+ // {
36
+ // "errorMessage": "RubyError: RuntimeError: error in submethod",
37
+ // "errorType": "RubyError",
38
+ // "stackTrace": [
39
+ // [
40
+ // "line1",
41
+ // "line2",
42
+ // "line3"
43
+ // ]
44
+ // ]
45
+ // }
46
+ //
47
+ // Input: String
48
+ // RubyError: RuntimeError: error in submethod
49
+ // line1
50
+ // line2
51
+ // line3
52
+ //
53
+ // Output: Error object
54
+ // { RubyError: RuntimeError: error in submethod
55
+ // line1
56
+ // line2
57
+ // line3 name: 'RubyError' }
58
+ function customError(text) {
59
+ text = filterErrorLines(text) // filter for error lines only
60
+ var lines = text.split("\n")
61
+ var message = lines[0]
62
+ var error = new Error(message)
63
+ error.name = message.split(':')[0]
64
+ error.stack = lines.slice(0, lines.length-1) // drop final empty line
65
+ .map(e => e.replace(/^\s+/g,'')) // trim leading whitespaces
66
+ .join("\n")
67
+ return error
68
+ }
69
+
70
+ module.exports.<%= @js_method %> = (event, context, callback) => {
71
+ // To test on mac, set these environment variables:
72
+ // export RUBY_BIN=$HOME/.rbenv/shims/ruby
73
+ // export PROCESSOR_COMMAND="lam process controller"
74
+
75
+ // Command: lam process controller [event] [context] [handler]
76
+ const processor_command = process.env.PROCESSOR_COMMAND || "lam"
77
+ var args = [
78
+ "process",
79
+ "<%= @process_type %>",
80
+ JSON.stringify(event),
81
+ JSON.stringify(context),
82
+ "<%= @handler %>"
83
+ ]
84
+ // console.log("processor_command %o", processor_command)
85
+ // console.log("args %o", args)
86
+
87
+ var ruby = spawn("bin/lam", args);
88
+
89
+ // string concatation in javascript is faster than array concatation
90
+ // http://bit.ly/2gBMDs6
91
+ var stdout_buffer = ""; // stdout buffer
92
+ // In the processor_command we do NOT call puts directly and write to stdout
93
+ // because it will mess up the eventual response that we want API Gateway to
94
+ // process.
95
+ // The Lambda prints out function to whatever the return value the ruby method
96
+ ruby.stdout.on('data', function(data) {
97
+ // Not using console.log because it decorates output with a newline.
98
+ //
99
+ // Uncomment process.stdout.write to see stdout streamed for debugging.
100
+ // process.stdout.write(data)
101
+ stdout_buffer += data;
102
+ });
103
+
104
+ // react to potential errors
105
+ var stderr_buffer = "";
106
+ ruby.stderr.on('data', function(data) {
107
+ // not using console.error because it decorates output with a newline
108
+ stderr_buffer += data
109
+ process.stderr.write(data)
110
+ });
111
+
112
+ //finalize when ruby process is done.
113
+ ruby.on('close', function(exit_code) {
114
+ // http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html#nodejs-prog-model-handler-callback
115
+
116
+ // succcess
117
+ if (exit_code == 0) {
118
+ var result
119
+ try {
120
+ result = JSON.parse(stdout_buffer)
121
+ } catch(e) {
122
+ // if json cannot be parse assume simple text output intended
123
+ process.stderr.write("WARN: error parsing json, assuming plain text is desired.")
124
+ result = stdout_buffer
125
+ }
126
+ callback(null, result);
127
+
128
+ // callback(null, stdout_buffer);
129
+ } else {
130
+
131
+ // TODO: if this works, allow a way to not decorate the error in case
132
+ // it actually errors in javascript land
133
+ // Customize error object with ruby error info
134
+ var error = customError(stderr_buffer)
135
+ callback(error);
136
+ // console.log("error!")
137
+ }
138
+ });
139
+ }
140
+
141
+ // for local testing
142
+ if (process.platform == "darwin") {
143
+ // fake event and context
144
+ var event = {"hello": "world"}
145
+ // var event = {"body": {"hello": "world"}} // API Gateway wrapper structure
146
+ var context = {"fake": "context"}
147
+ module.exports.<%= @js_method %>(event, context, (error, message) => {
148
+ console.error("\nLOCAL TESTING OUTPUT")
149
+ if (error) {
150
+ console.error("error message: %o", error)
151
+ } else {
152
+ console.error("success message %o", message)
153
+ // console.log(JSON.stringify(message)) // stringify
154
+ }
155
+ })
156
+ }
@@ -0,0 +1,108 @@
1
+ require "fileutils"
2
+ require "open-uri"
3
+ require "colorize"
4
+
5
+ class Lam::Build
6
+ TRAVELING_RUBY_VERSION = 'http://d6r77u77i8pq3.cloudfront.net/releases/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz'.freeze
7
+ TEMP_BUILD_DIR = '/tmp/lam_build'.freeze
8
+
9
+ class TravelingRuby
10
+ def build
11
+ if File.exist?("#{Lam.root}bundled")
12
+ puts "Ruby bundled already exists."
13
+ puts "To force rebundling: rm -rf bundled"
14
+ return
15
+ end
16
+
17
+ check_ruby_version
18
+
19
+ FileUtils.mkdir_p(TEMP_BUILD_DIR)
20
+ copy_gemfiles
21
+
22
+ Dir.chdir(TEMP_BUILD_DIR) do
23
+ download_traveling_ruby
24
+ unpack_traveling_ruby
25
+ bundle_install
26
+ end
27
+
28
+ move_bundled_to_project
29
+ end
30
+
31
+ def check_ruby_version
32
+ return if ENV['LAM_SKIP_RUBY_CHECK'] # only use if you absolutely need to
33
+ traveling_version = TRAVELING_RUBY_VERSION.match(/-((\d+)\.(\d+)\.(\d+))-/)[1]
34
+ if RUBY_VERSION != traveling_version
35
+ puts "You are using ruby version #{RUBY_VERSION}."
36
+ abort("You must use ruby #{traveling_version} to build the project because it's what Traveling Ruby uses.".colorize(:red))
37
+ end
38
+ end
39
+
40
+ def copy_gemfiles
41
+ FileUtils.cp("#{Lam.root}Gemfile", "#{TEMP_BUILD_DIR}/")
42
+ FileUtils.cp("#{Lam.root}Gemfile.lock", "#{TEMP_BUILD_DIR}/")
43
+ end
44
+
45
+ def download_traveling_ruby
46
+ puts "Downloading traveling ruby from #{traveling_ruby_url}."
47
+
48
+ FileUtils.rm_rf("#{TEMP_BUILD_DIR}/#{bundled_ruby_dest}")
49
+ File.open(traveling_ruby_tar_file, 'wb') do |saved_file|
50
+ # the following "open" is provided by open-uri
51
+ open(traveling_ruby_url, 'rb') do |read_file|
52
+ saved_file.write(read_file.read)
53
+ end
54
+ end
55
+
56
+ puts 'Download complete.'
57
+ end
58
+
59
+ def unpack_traveling_ruby
60
+ puts 'Unpacking traveling ruby.'
61
+
62
+ FileUtils.mkdir_p(bundled_ruby_dest)
63
+
64
+ success = system("tar -xzf #{traveling_ruby_tar_file} -C #{bundled_ruby_dest}")
65
+ abort('Unpacking traveling ruby failed') unless success
66
+ puts 'Unpacking traveling ruby successful.'
67
+
68
+ puts 'Removing tar.'
69
+ FileUtils.rm_rf(traveling_ruby_tar_file)
70
+ end
71
+
72
+ def bundle_install
73
+ puts 'Installing bundle.'
74
+ require "bundler" # dynamicaly require bundler so user can use any bundler
75
+ Bundler.with_clean_env do
76
+ success = system(
77
+ "cd #{TEMP_BUILD_DIR} && " \
78
+ 'env BUNDLE_IGNORE_CONFIG=1 bundle install --path bundled/gems --without development'
79
+ )
80
+
81
+ abort('Bundle install failed, exiting.') unless success
82
+ end
83
+
84
+ puts 'Bundle install success.'
85
+ end
86
+
87
+ def move_bundled_to_project
88
+ if File.exist?("#{Lam.root}bundled")
89
+ puts "Removing current bundled folder"
90
+ FileUtils.rm_rf("#{Lam.root}bundled")
91
+ end
92
+ puts "Moving bundled ruby to your project."
93
+ FileUtils.mv("#{TEMP_BUILD_DIR}/bundled", Lam.root)
94
+ end
95
+
96
+ def bundled_ruby_dest
97
+ "bundled/ruby"
98
+ end
99
+
100
+ def traveling_ruby_url
101
+ TRAVELING_RUBY_VERSION
102
+ end
103
+
104
+ def traveling_ruby_tar_file
105
+ File.basename(traveling_ruby_url)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,23 @@
1
+ require "thor"
2
+ require "lam/cli/help"
3
+
4
+ module Lam
5
+
6
+ class CLI < Command
7
+ class_option :verbose, type: :boolean
8
+ class_option :noop, type: :boolean
9
+
10
+ desc "build", "Builds and prepares project for Lambda"
11
+ long_desc Help.build
12
+ option :force, type: :boolean, aliases: "-f", desc: "override existing starter files"
13
+ option :quiet, type: :boolean, aliases: "-q", desc: "silence the output"
14
+ option :format, type: :string, default: "yaml", desc: "starter project template format: json or yaml"
15
+ def build
16
+ Lam::Build.new(options).run
17
+ end
18
+
19
+ desc "process TYPE", "process subcommand tasks"
20
+ long_desc Help.process
21
+ subcommand "process", Lam::Process
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Lam
2
+ class CLI < Command
3
+ class Help
4
+ class << self
5
+ def build
6
+ <<-EOL
7
+ Builds and prepares project for AWS Lambda. Generates a node shim and vendors Traveling Ruby. Creates a zip file to be uploaded to Lambda for each handler.
8
+ EOL
9
+ end
10
+
11
+ def process
12
+ <<-EOL
13
+ TODO: update process help menu
14
+ EOL
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ require "thor"
2
+
3
+ module Lam
4
+ class Command < Thor
5
+ class << self
6
+ def dispatch(m, args, options, config)
7
+ # Allow calling for help via:
8
+ # lam command help
9
+ # lam command -h
10
+ # lam command --help
11
+ # lam command -D
12
+ #
13
+ # as well thor's normal way:
14
+ #
15
+ # lam help command
16
+ help_flags = Thor::HELP_MAPPINGS + ["help"]
17
+ if args.length > 1 && !(args & help_flags).empty?
18
+ args -= help_flags
19
+ args.insert(-2, "help")
20
+ end
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ class Lam::Process < Lam::Command
2
+ autoload :Help, 'lam/process/help'
3
+ autoload :ProcessorDeducer, 'lam/process/processor_deducer'
4
+ autoload :BaseProcessor, 'lam/process/base_processor'
5
+ autoload :ControllerProcessor, 'lam/process/controller_processor'
6
+
7
+ class_option :verbose, type: :boolean
8
+ class_option :noop, type: :boolean
9
+ class_option :project_root, desc: "Project folder. Defaults to current directory", default: "."
10
+ class_option :region, desc: "AWS region"
11
+
12
+ desc "create STACK", "create a CloudFormation stack"
13
+ option :randomize_stack_name, type: :boolean, desc: "tack on random string at the end of the stack name", default: nil
14
+ long_desc Help.controller
15
+ def controller(event, context, handler)
16
+ ControllerProcessor.new(event, context, handler).run
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ require 'json'
2
+ require_relative 'processor_deducer'
3
+
4
+ # Global overrides for Lambda processing
5
+ $stdout.sync = true
6
+ # This might seem weird but we want puts to write to stderr which is set in
7
+ # the node shim to write to stderr. This directs the output to Lambda logs.
8
+ # Printing to stdout can managle up the payload returned from Lambda function.
9
+ # This is not desired if you want to return say a json payload to API Gateway
10
+ # eventually.
11
+ def puts(text)
12
+ $stderr.puts(text)
13
+ end
14
+
15
+ class Lam::Process::BaseProcessor
16
+ attr_reader :event, :context, :handler
17
+ def initialize(event, context, handler)
18
+ # assume valid json from Lambda
19
+ @event = JSON.parse(event)
20
+ @context = JSON.parse(context)
21
+ @handler = handler
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "base_processor"
2
+
3
+ class Lam::Process
4
+ class ControllerProcessor < Lam::Process::BaseProcessor
5
+ def run
6
+ # Use the handler value (ie: posts.create) to deduce the user's business
7
+ # code to require and run.
8
+ deducer = ProcessorDeducer.new(handler)
9
+ path = deducer.controller[:path]
10
+ code = deducer.controller[:code]
11
+
12
+ begin
13
+ require path # require "app/controllers/posts_controller.rb"
14
+ # Puts the return value of user's code to stdout because this is
15
+ # what eventually gets used by API Gateway.
16
+ # Explicitly using $stdout since puts redirected to $stderr.
17
+
18
+ # result = PostsController.new(event, context).create
19
+ result = instance_eval(code, path)
20
+
21
+ # JSON.dump is pretty robust. If it cannot dump the structure into a
22
+ # json string, it just dumps it to a plain text string.
23
+ $stdout.puts JSON.dump(result) # only place where we write to stdout.
24
+ rescue Exception => e
25
+ # Customize error message slightly so nodejs shim can process the
26
+ # returned error message.
27
+ # The "RubyError: " is a marker that the javascript shim scans for.
28
+ $stderr.puts("RubyError: #{e.class}: #{e.message}") # js needs this as the first line
29
+ backtrace = e.backtrace.map {|l| " #{l}" }
30
+ $stderr.puts(backtrace)
31
+ # $stderr.puts("END OF RUBY OUTPUT")
32
+ exit 1 # instead of re-raising to control the error backtrace output
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ class Lam::Process::Help
2
+ class << self
3
+ def controller
4
+ <<-EOL
5
+ Examples:
6
+
7
+ lam process controller '{ "we" : "love", "using" : "Lambda" }' '{"test": "1"}' "handlers/controllers/posts.create"
8
+ EOL
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,52 @@
1
+ class Lam::Process::ProcessorDeducer
2
+ def initialize(handler)
3
+ @handler = handler
4
+ end
5
+
6
+ # Deduces the path and method from the handler. Example:
7
+ #
8
+ # ProcessorDeducer.new("handlers/functions/posts.create").function
9
+ # => {path: "app/functions/posts.rb", code: "create(event, context)"}
10
+ #
11
+ # Summary:
12
+ #
13
+ # Input:
14
+ # handler: handlers/functions/posts.create
15
+ # Output:
16
+ # path: app/functions/posts.rb
17
+ # code: create(event, context) # code to instance_eval
18
+ #
19
+ # Returns: {path: path, code: code}
20
+ def function
21
+ path, meth = @handler.split('.')
22
+ path = Lam.root + path.sub("handlers", "app") + ".rb"
23
+ code = "#{meth}(event, context)"
24
+ {path: path, code: code}
25
+ end
26
+
27
+ # Deduces the path and method from the handler. Example:
28
+ #
29
+ # ProcessorDeducer.new("handlers/controllers/posts.create").controller
30
+ # => {path: "controllers/posts_controller.rb", code: "create"}
31
+ #
32
+ # Summary:
33
+ #
34
+ # Input:
35
+ # handler: handlers/controllers/posts.create
36
+ # Output:
37
+ # path: app/controllers/posts_controller.rb
38
+ # code: create # code to instance_eval
39
+ #
40
+ # Returns: {path: path, code: code}
41
+ def controller
42
+ handler_path, meth = @handler.split('.')
43
+
44
+ path = Lam.root + handler_path.sub("handlers", "app") + "_controller.rb"
45
+
46
+ controller_name = handler_path.sub(%r{.*handlers/controllers/}, "") + "_controller" # posts_controller
47
+ controller_class = controller_name.split('_').collect(&:capitalize).join # PostsController
48
+ code = "#{controller_class}.new(event, context).#{meth}" # PostsController.new(event, context).create
49
+
50
+ {path: path, code: code, class_name: controller_class}
51
+ end
52
+ end