lam 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +16 -8
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -3
  6. data/Gemfile.lock +107 -0
  7. data/Guardfile +22 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +19 -17
  10. data/Rakefile +4 -0
  11. data/bin/lam +14 -0
  12. data/bin/lamb +14 -0
  13. data/lam.gemspec +18 -20
  14. data/lib/lam.rb +10 -1
  15. data/lib/lam/base_controller.rb +54 -0
  16. data/lib/lam/build.rb +43 -0
  17. data/lib/lam/build/handler_generator.rb +34 -0
  18. data/lib/lam/build/lambda_deducer.rb +47 -0
  19. data/lib/lam/build/templates/handler.js +156 -0
  20. data/lib/lam/build/traveling_ruby.rb +108 -0
  21. data/lib/lam/cli.rb +23 -0
  22. data/lib/lam/cli/help.rb +19 -0
  23. data/lib/lam/command.rb +25 -0
  24. data/lib/lam/process.rb +18 -0
  25. data/lib/lam/process/base_processor.rb +23 -0
  26. data/lib/lam/process/controller_processor.rb +36 -0
  27. data/lib/lam/process/help.rb +11 -0
  28. data/lib/lam/process/processor_deducer.rb +52 -0
  29. data/lib/lam/util.rb +13 -0
  30. data/lib/lam/version.rb +1 -1
  31. data/notes/design.md +43 -0
  32. data/notes/traveling-ruby-packaging-lam.md +26 -0
  33. data/notes/traveling-ruby-packaging.md +103 -0
  34. data/notes/traveling-ruby.md +82 -0
  35. data/spec/fixtures/project/.gitignore +3 -0
  36. data/spec/fixtures/project/.ruby-version +1 -0
  37. data/spec/fixtures/project/Gemfile +4 -0
  38. data/spec/fixtures/project/Gemfile.lock +35 -0
  39. data/spec/fixtures/project/app/controllers/application_controller.rb +2 -0
  40. data/spec/fixtures/project/app/controllers/posts_controller.rb +12 -0
  41. data/spec/fixtures/project/bin/lam +22 -0
  42. data/spec/fixtures/project/handlers/controllers/posts.js +156 -0
  43. data/spec/lib/cli_spec.rb +20 -0
  44. data/spec/lib/lam/base_controller_spec.rb +18 -0
  45. data/spec/lib/lam/build/lambda_deducer_spec.rb +20 -0
  46. data/spec/lib/lam/build_spec.rb +29 -0
  47. data/spec/lib/lam/process/controller_processor_spec.rb +22 -0
  48. data/spec/lib/lam/process/infer_spec.rb +24 -0
  49. data/spec/lib/lam/process_spec.rb +18 -0
  50. data/spec/spec_helper.rb +25 -0
  51. metadata +191 -21
  52. data/bin/console +0 -14
  53. data/bin/setup +0 -8
@@ -0,0 +1,34 @@
1
+ require "fileutils"
2
+ require "erb"
3
+
4
+ class Lam::Build
5
+ class HandlerGenerator
6
+ # handler_info:
7
+ # {:handler=>"handlers/controllers/posts.create",
8
+ # :js_path=>"handlers/controllers/posts.js",
9
+ # :js_method=>"create"}
10
+ def initialize(handler_info)
11
+ @handler_info = handler_info
12
+ @handler = handler_info[:handler]
13
+ @js_path = handler_info[:js_path]
14
+ @js_method = handler_info[:js_method]
15
+ end
16
+
17
+ def generate
18
+ js_path = "#{Lam.root}#{@js_path}"
19
+ FileUtils.mkdir_p(File.dirname(js_path))
20
+
21
+ template_path = File.expand_path('../templates/handler.js', __FILE__)
22
+ template = IO.read(template_path)
23
+
24
+ # Important ERB variables with examples:
25
+ # @handler - handlers/controllers/posts.create
26
+ # @process_type - controller
27
+ @process_type = @handler.split('/')[1].singularize
28
+ result = ERB.new(template, nil, "-").result(binding)
29
+ puts "generating #{js_path}"
30
+ IO.write(js_path, result)
31
+ # FileUtils.cp(template_path, js_path)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ class Lam::Build
2
+ class LambdaDeducer
3
+ attr_reader :handlers
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def run
9
+ deduce
10
+ end
11
+
12
+ def deduce
13
+ # Example: require "./app/controllers/posts_controller.rb"
14
+ require_path = @path.starts_with?('/') ? @path : "#{Lam.root}#{@path}"
15
+ require require_path
16
+
17
+ # Example: @klass_name = "PostsController"
18
+ @klass_name = File.basename(@path, '.rb').classify
19
+ klass = @klass_name.constantize
20
+ @handlers = klass.lambda_functions.map { |fn| handler_info(fn) }
21
+ self
22
+ end
23
+
24
+ # Transform the method to the handler info
25
+ def handler_info(function_name)
26
+ handler = get_handler(function_name)
27
+ js_path = get_js_path(function_name)
28
+ {
29
+ handler: handler,
30
+ js_path: js_path,
31
+ js_method: function_name.to_s
32
+ }
33
+ end
34
+
35
+ def get_handler(function_name)
36
+ "handlers/controllers/#{module_name}.create"
37
+ end
38
+
39
+ def get_js_path(function_name)
40
+ "handlers/controllers/#{module_name}.js"
41
+ end
42
+
43
+ def module_name
44
+ @klass_name.sub(/Controller$/,'').underscore
45
+ end
46
+ end
47
+ end
@@ -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