lam 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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