kenji 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ pkg
2
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ gem 'inifile'
2
+ gem 'json'
3
+ gem 'rack'
4
+
5
+ source :rubygems
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ The Azure License
2
+
3
+ Copyright (c) 2011 Kenneth Ballenegger
4
+
5
+ Attribute to Kenneth Ballenegger - http://kswizz.com/
6
+
7
+ You (the licensee) are hereby granted permission, free of charge, to deal in this software or source code (this "Software") without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sublicense this Software, subject to the following conditions:
8
+
9
+ You must give attribution to the party mentioned above, by name and by hyperlink, in the about box, credits document and/or documentation of any derivative work using a substantial portion of this Software.
10
+
11
+ You may not use the name of the copyright holder(s) to endorse or promote products derived from this Software without specific prior written permission.
12
+
13
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN THIS SOFTWARE.
14
+
15
+ http://license.azuretalon.com/
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ *Project is still actively in development.*
2
+
3
+
4
+ # Kenji
5
+
6
+ Kenji is a lightweight backend framework for Ruby.
7
+
8
+
9
+ ## Rationale
10
+
11
+ Kenji believes that a traditional web application should be divided into two parts: an client application running in the browser (HTML/JS/CSS), and a backend API with which it communicates. Kenji is the backend side of the equation, while the front-end architecture is left up to the user. (Popular options are [backbone][] and [spine][].)
12
+
13
+ [backbone]: http://documentcloud.github.com/backbone/
14
+ [spine]: http://spinejs.com/
15
+
16
+ Kenji believes that in order to keep clean and organized code, routes should be defined inline with their code.
17
+
18
+ Kenji believes that an app should be usable as a library from scripts or from the command line. An app should be automatable and testable.
19
+
20
+ Lastly, Kenji is opinionated, but only about things that directly pertain to routing and code architecture. Kenji believes in being a ligthweight module that only solves the problem it focuses on. Everything else is left up to the user. (ORM, data store, web server, message queue, front-end framework, deployment process, etc.)
21
+
22
+
23
+ ### Routing
24
+
25
+ Kenji wants you to organize your code into logical units of code, aka. controllers. The controllers will automatically be selected based on the url requested, and the rest of the route is defined inline in the controller, with a domain-specific-language.
26
+
27
+ The canonical Hello World example for the URL `/hello/world` in Kenji would look like this, in `controller/hello.rb`:
28
+
29
+ ````ruby
30
+ class HelloController < Kenji::Controller
31
+ get '/world' do
32
+ {hello: :world}
33
+ end
34
+ end
35
+ ````
36
+
37
+ A more representative example might be:
38
+
39
+ ````ruby
40
+ class UserController < Kenji::Controller
41
+
42
+ # ...
43
+
44
+ get '/:id/friends' do |id|
45
+ # list friends for id
46
+ end
47
+
48
+ post '/:id/friend/:id' do |id, friend_id|
49
+ # add connection from user id to friend_id
50
+ end
51
+
52
+ delete '/:id/friend/:id' do |id, friend_id|
53
+ # delete connection from user id to friend_id
54
+ end
55
+ end
56
+ ````
57
+
58
+
59
+ ### Data Transport
60
+
61
+ JSON is used as the singular data transport for Kenji. Requests are assumed to have:
62
+
63
+ Content-Type: application/json; charset=utf-8
64
+ Accept: application/json; charset=utf-8
65
+
66
+
67
+ ## Usage
68
+
69
+ Getting started with Kenji could not be any easier. All it takes is a few lines and a terminal:
70
+
71
+ $ gem install kenji # (once kenji is on the rubygems main source)
72
+ $ kenji-init app_name; cd app_name
73
+ $ rackup # launch the webserver
74
+
75
+ And already, your app is ready to go:
76
+
77
+ $ curl http://localhost:9292/hello/world
78
+ {"hello":"world"}
79
+
80
+
81
+ ## Requirements & Assumptions
82
+
83
+ - Requires rubygems and bundler.
84
+ - Requires Ruby 1.9.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/bin/kenji-init ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ path = '.' << '/' << (ARGV.first || '.')
4
+ app_name = File.basename(path)
5
+
6
+ Dir.mkdir(path) unless File.directory?(path)
7
+ path = File.realpath(path)
8
+
9
+ init_source = File.realpath(File.dirname(File.dirname(__FILE__))) << '/inited'
10
+
11
+ for_earch_item = lambda do |item, base|
12
+ next if item == '.' || item == '..'
13
+ item_to_create = item.gsub('__APP_NAME__', app_name)
14
+
15
+
16
+ if File.directory?("#{init_source}/#{base}/#{item}")
17
+ Dir.mkdir("#{path}/#{base}/#{item_to_create}") unless File.directory?("#{path}/#{base}/#{item}")
18
+ puts "Initializing directory #{base}/#{item_to_create}"
19
+ Dir.foreach("#{init_source}/#{base}/#{item}") { |i| for_earch_item.call(i, "#{base}/#{item}") }
20
+ else
21
+ puts "Generating content for file #{base}/#{item_to_create}"
22
+ data = ''
23
+ f = File.open("#{init_source}/#{base}/#{item}", 'r')
24
+ while line = f.gets
25
+ data += (line.gsub('__APP_NAME__', app_name))
26
+ end
27
+ f.close
28
+ f = File.open("#{path}/#{base}/#{item_to_create}", 'w')
29
+ f.puts data
30
+ f.close
31
+ end
32
+ end
33
+
34
+ Dir.foreach(init_source) { |i| for_earch_item.call(i, '.') }
35
+
36
+ system("cd #{path}; bundle install")
data/inited/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ tmp
2
+ Gemfile.lock
data/inited/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ gem 'rack'
2
+ gem 'kenji'
3
+
4
+ source :rubygems
data/inited/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Hello, world!
2
+
3
+ This is the readme for __APP_NAME__, an awesome new app.
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/ruby
2
+
3
+ class Main
4
+
5
+ def self.init
6
+ require 'rubygems'
7
+ require 'bundler/setup'
8
+ require $root+'/lib/kenji/kenji'
9
+ @@k = Kenji::Kenji.new({}, $root)
10
+ end
11
+
12
+ def self.main args
13
+ verb = args.first.to_sym unless args.empty?
14
+
15
+ # case verb
16
+ # when :import
17
+ # puts "Calling import script..."
18
+ # @@k.controller_for(:import)._caffeine @@k
19
+ # when :process
20
+ # puts "Processing values..."
21
+ # @@k.controller_for(:processing)._value @@k
22
+ # when :configure
23
+ # require $root + '/lib/configure'
24
+ # skip_update = (args[1] =~ /-?-?skip[-_]update/)
25
+ # AnalyticsModule.configure __FILE__, skip_update
26
+ # else
27
+ # puts <<-EOO
28
+ # No verb defined. Usage: ./main [verb]
29
+ #
30
+ # configure [--skip-update]:
31
+ # process the value of users
32
+ # optionally, skip the self-update process
33
+ # import:
34
+ # import install from caffeine.io
35
+ # process:
36
+ # process the value of users
37
+ # EOO
38
+ # end
39
+ end
40
+
41
+ def kenji
42
+ @@k
43
+ end
44
+ end
45
+
46
+ require 'pathname'
47
+ $root = File.dirname Pathname.new(__FILE__).realpath.to_s
48
+
49
+ Main.init unless ARGV.first == 'configure'
50
+ if __FILE__ == $0
51
+ Main.main ARGV
52
+ end
data/inited/config.ru ADDED
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rack'
4
+ require 'kenji'
5
+
6
+ # use Rack::ShowExceptions
7
+
8
+ app = proc do |env|
9
+ Kenji::Kenji.new(env, File.dirname(__FILE__)).call
10
+ end
11
+ run app
@@ -0,0 +1 @@
1
+ This is where configuration goes.
@@ -0,0 +1,6 @@
1
+
2
+ class MainController
3
+ get '/index' do
4
+ {hello: :world}
5
+ end
6
+ end
data/inited/lib/README ADDED
File without changes
@@ -0,0 +1 @@
1
+ This is where models go.
@@ -0,0 +1 @@
1
+ Handcrafted with love by Kenneth Ballenegger
@@ -0,0 +1,19 @@
1
+ <html>
2
+ <head>
3
+ <title>Hello, world!</title>
4
+
5
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript" charset="utf-8"></script>
6
+ <script type="text/javascript" charset="utf-8">
7
+ $(document).ready(function() {
8
+ $.ajax('/main/index.json', {
9
+ success: function(data) {
10
+ $('#name').html(data.hello);
11
+ }
12
+ });
13
+ });
14
+ </script>
15
+ </head>
16
+ <body>
17
+ Hello, <span id="name">User</span>!
18
+ </body>
19
+ </html>
@@ -0,0 +1 @@
1
+ This is where scripts go.
data/inited/tmp/README ADDED
@@ -0,0 +1 @@
1
+ This folder is for Rack & Passenger to use.
File without changes
data/kenji.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ $:.push File.expand_path('../lib', __FILE__)
2
+ require 'kenji/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'kenji'
6
+ s.version = Kenji::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.date = '2012-04-05'
9
+ s.summary = 'Kenji'
10
+ s.description = 'A lightweight Ruby web framework.'
11
+ s.authors = ['Kenneth Ballenegger']
12
+ s.email = ['kenneth@ballenegger.com']
13
+ s.homepage =
14
+ 'https://github.com/kballenegger/kenji'
15
+
16
+ s.add_dependency 'json'
17
+ s.add_dependency 'rack'
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ['lib']
23
+
24
+ end
@@ -0,0 +1,108 @@
1
+
2
+ module Kenji
3
+ class Controller
4
+
5
+ # use the reader freely to grab the kenji object
6
+ attr_accessor :kenji
7
+
8
+ # Routes below will accept routes in the format, eg.:
9
+ # /hello/:id/children
10
+ # Can contain any number of :id, but must be in their own url segment.
11
+ # Colon-prefixed segments become variables, passed onto the given block as arguments.
12
+ # The name given to the variable is irrelevant, and is thrown away: /hello/:/children is equivalent to the example above.
13
+ # Given block must have correct arity.
14
+
15
+ # Route GET
16
+ def self.get(path, &block)
17
+ route(:get, path, &block)
18
+ end
19
+
20
+ # Route POST
21
+ def self.post(path, &block)
22
+ route(:post, path, &block)
23
+ end
24
+
25
+ # Route PUT
26
+ def self.put(path, &block)
27
+ route(:put, path, &block)
28
+ end
29
+
30
+ # Route DELETE
31
+ def self.delete(path, &block)
32
+ route(:delete, path, &block)
33
+ end
34
+
35
+ # TODO: figure out whether I want PATCH, OPTIONS, HEAD
36
+
37
+ # Route all methods for given path
38
+ def self.all(path, &block)
39
+ route(:get, :post, :put, :delete, path, &block)
40
+ end
41
+
42
+ def self.fallback(&block)
43
+ define_method(:fallback, &block)
44
+ nil # void method
45
+ end
46
+
47
+ # Route a given path to the correct block, for any given methods
48
+ #
49
+ # Note: this works by building a tree for the path,
50
+ # each node being a path segment or variable segment, and the leaf @action being the block
51
+
52
+ def self.route(*methods, path, &block)
53
+ # bind the block to self as an instance method, so its context is correct
54
+ define_method(:_tmp_route_action, &block)
55
+ block = instance_method(:_tmp_route_action)
56
+ remove_method(:_tmp_route_action)
57
+ # store the block for each method
58
+ methods.each do |method|
59
+ node = ((@routes ||= {})[method] ||= {})
60
+ segments = path.split('/')
61
+ segments = segments.drop(1) if segments.first == '' # discard leading /'s empty segment
62
+ segments.each do |segment| # lazily create tree
63
+ segment = ':' if segment =~ /^:/ # discard :variable name
64
+ node = (node[segment.to_sym] ||= {})
65
+ end
66
+ node[:@action] = block # store block as leaf in @action
67
+ end
68
+ nil # void method
69
+ end
70
+
71
+
72
+ # Most likely only used by Kenji itself.
73
+ # Override to implement your own routing, if you'd like.
74
+ def call(method, path)
75
+ segments = path.split('/')
76
+ segments = segments.drop(1) if segments.first == '' # discard leading /'s empty segment
77
+ node = self.class.routes[method]
78
+ variables = []
79
+ segments.each do |segment| # traverse tree to find
80
+ if node[segment.to_sym]
81
+ node = node[segment.to_sym] # attempt to move down to the plain text segment
82
+ else
83
+ node = node[:':'] # attempt to find a variable segment
84
+ variables << segment # either we've found a variable, or the `unless` below will trigger
85
+ end
86
+ break unless node # break if as an instance method nil, fallback below
87
+ end
88
+ if node && action = node[:@action] # the block is stored in the @action leaf
89
+ return action.bind(self).call(*variables)
90
+ else # or, fallback if necessary store the block for each method
91
+ if respond_to? :fallback
92
+ if self.class.instance_method(:fallback).arity == 1
93
+ return fallback(path)
94
+ else
95
+ return fallback
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ private
102
+ # Accessor for @routes
103
+ def self.routes
104
+ @routes || {}
105
+ end
106
+ end
107
+ end
108
+
@@ -0,0 +1,16 @@
1
+ # Kenji string extensions
2
+
3
+ class String
4
+ def to_underscore!
5
+ self.gsub!(/(.)([A-Z])/,'\1_\2').downcase!
6
+ end
7
+ def to_underscore
8
+ self.clone.to_underscore!
9
+ end
10
+ def to_camelcase!
11
+ self.replace self.split('_').each{ |s| s.capitalize! }.join('')
12
+ end
13
+ def to_camelcase
14
+ self.clone.to_camelcase!
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+
2
+ module Kenji
3
+ VERSION = "0.3.0"
4
+ end
5
+
data/lib/kenji.rb ADDED
@@ -0,0 +1,114 @@
1
+ require 'json'
2
+ require 'kenji/controller'
3
+ require 'kenji/string_extensions'
4
+ require 'rack'
5
+
6
+ module Kenji
7
+ class Kenji
8
+
9
+ attr_reader :env, :root
10
+
11
+ def initialize(env, root)
12
+ @headers = {
13
+ 'Content-Type' => 'application/json'
14
+ }
15
+ @status = 200
16
+ @root = File.expand_path(root) + '/'
17
+ @env = env
18
+ end
19
+
20
+ def call
21
+ path = @env['PATH_INFO']
22
+
23
+ # deal with static files
24
+ static = "#{@root}public#{path}"
25
+ return Rack::File.new("#{@root}public").call(@env) if File.file?(static)
26
+
27
+
28
+ # new routing code
29
+ segments = path.split('/')
30
+ segments = segments.drop(1) if segments.first == '' # discard leading /'s empty segment
31
+
32
+ acc = ''; out = {}
33
+ while head = segments.shift
34
+ acc = "#{acc}/#{head}"
35
+ if controller = controller_for(acc) # if we have a valid controller
36
+ begin
37
+ out = controller.call(@env['REQUEST_METHOD'].downcase.to_sym, '/'+segments.join('/')).to_json
38
+ rescue KenjiRespondControlFlowInterrupt => e
39
+ out = e.response
40
+ end
41
+ break
42
+ end
43
+ end
44
+
45
+ [@status, @headers, [out]]
46
+ end
47
+
48
+
49
+
50
+ # Methods for users!
51
+
52
+
53
+ # Sets one or multiple headers, as named arametres. eg.
54
+ #
55
+ # kenji.header 'Content-Type' => 'hello/world'
56
+ def header(hash={})
57
+ hash.each do |key, value|
58
+ @headers[key] = value
59
+ end
60
+ end
61
+
62
+ # Fetch (and cache) the json input to the request
63
+ # Return a Hash
64
+ def input_as_json
65
+ return @json_input if @json_input
66
+ require 'json'
67
+ raw = @env['rack.input'].read if @env['rack.input']
68
+ begin
69
+ return @json_input = JSON.parse(raw)
70
+ rescue JSON::ParserError => e
71
+ end if raw
72
+ {} # default return value
73
+ end
74
+
75
+ # Respond to the request
76
+ def respond code, message, hash={}
77
+ @status = code
78
+ response = { # default structure. TODO: figure out if i really want to keep this
79
+ :status => code,
80
+ :message => message
81
+ }
82
+ hash.each { |k,v| response[k]=v }
83
+ raise KenjiRespondControlFlowInterrupt.new(response.to_json)
84
+ end
85
+
86
+
87
+
88
+ # Private methods
89
+
90
+ # Will attempt to fetch the controller, and verify that it is a implements call
91
+ def controller_for subpath
92
+ path = "#{@root}controllers#{subpath}.rb"
93
+ return nil unless File.exists?(path)
94
+ require path
95
+ controller_name = subpath.split('/').last
96
+ controller_class = Object.const_get(controller_name.to_s.to_camelcase+'Controller')
97
+ return unless controller_class.method_defined?(:call) && controller_class.instance_method(:call).arity == 2 # ensure protocol compliance
98
+ controller = controller_class.new
99
+ controller.kenji = self if controller.respond_to?(:kenji=)
100
+ return controller if controller
101
+ nil # default return value
102
+ end
103
+
104
+ end
105
+
106
+
107
+ class KenjiRespondControlFlowInterrupt < StandardError
108
+ attr_accessor :response
109
+ def initialize(response)
110
+ @response = response
111
+ end
112
+ end # early exit containing a response
113
+ end
114
+
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kenji
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kenneth Ballenegger
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-05 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: &70195188110740 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70195188110740
25
+ - !ruby/object:Gem::Dependency
26
+ name: rack
27
+ requirement: &70195188110000 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70195188110000
36
+ description: A lightweight Ruby web framework.
37
+ email:
38
+ - kenneth@ballenegger.com
39
+ executables:
40
+ - kenji-init
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - LICENSE
47
+ - README.md
48
+ - Rakefile
49
+ - bin/kenji-init
50
+ - inited/.gitignore
51
+ - inited/Gemfile
52
+ - inited/README.md
53
+ - inited/__APP_NAME__
54
+ - inited/config.ru
55
+ - inited/configuration/README
56
+ - inited/controllers/main.rb
57
+ - inited/lib/README
58
+ - inited/models/README
59
+ - inited/public/humans.txt
60
+ - inited/public/index.html
61
+ - inited/scripts/README
62
+ - inited/tmp/README
63
+ - inited/tmp/always_restart.txt
64
+ - kenji.gemspec
65
+ - lib/kenji.rb
66
+ - lib/kenji/controller.rb
67
+ - lib/kenji/string_extensions.rb
68
+ - lib/kenji/version.rb
69
+ homepage: https://github.com/kballenegger/kenji
70
+ licenses: []
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.10
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Kenji
93
+ test_files: []
94
+ has_rdoc: