zero 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +2 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +59 -0
  6. data/Guardfile +15 -0
  7. data/README.md +60 -0
  8. data/Thorfile +6 -0
  9. data/lib/zero.rb +7 -0
  10. data/lib/zero/controller.rb +46 -0
  11. data/lib/zero/rack_request.rb +44 -0
  12. data/lib/zero/renderer.rb +139 -0
  13. data/lib/zero/request.rb +100 -0
  14. data/lib/zero/request/accept.rb +28 -0
  15. data/lib/zero/request/accept_type.rb +58 -0
  16. data/lib/zero/request/client.rb +31 -0
  17. data/lib/zero/request/parameter.rb +101 -0
  18. data/lib/zero/request/server.rb +41 -0
  19. data/lib/zero/response.rb +80 -0
  20. data/lib/zero/router.rb +63 -0
  21. data/lib/zero/version.rb +3 -0
  22. data/spec/fixtures/templates/index.html.erb +1 -0
  23. data/spec/fixtures/templates/index.json.erb +1 -0
  24. data/spec/spec_helper.rb +65 -0
  25. data/spec/unit/controller/call_spec.rb +22 -0
  26. data/spec/unit/controller/renderer_spec.rb +11 -0
  27. data/spec/unit/renderer/read_template_path_spec.rb +53 -0
  28. data/spec/unit/renderer/render_spec.rb +50 -0
  29. data/spec/unit/renderer/template_path.rb +8 -0
  30. data/spec/unit/renderer/type_map_spec.rb +9 -0
  31. data/spec/unit/request/accept/encoding_spec.rb +9 -0
  32. data/spec/unit/request/accept/language_spec.rb +9 -0
  33. data/spec/unit/request/accept/types_spec.rb +9 -0
  34. data/spec/unit/request/accept_spec.rb +7 -0
  35. data/spec/unit/request/accepttype/each_spec.rb +10 -0
  36. data/spec/unit/request/accepttype/preferred_spec.rb +35 -0
  37. data/spec/unit/request/client/address_spec.rb +9 -0
  38. data/spec/unit/request/client/hostname_spec.rb +9 -0
  39. data/spec/unit/request/client/user_agent_spec.rb +9 -0
  40. data/spec/unit/request/client_spec.rb +8 -0
  41. data/spec/unit/request/content_type_spec.rb +16 -0
  42. data/spec/unit/request/create_spec.rb +21 -0
  43. data/spec/unit/request/delete_spec.rb +15 -0
  44. data/spec/unit/request/get_spec.rb +15 -0
  45. data/spec/unit/request/head_spec.rb +15 -0
  46. data/spec/unit/request/method_spec.rb +8 -0
  47. data/spec/unit/request/parameter/[]_spec.rb +56 -0
  48. data/spec/unit/request/parameter/custom_spec.rb +18 -0
  49. data/spec/unit/request/parameter/initialize_spec.rb +12 -0
  50. data/spec/unit/request/parameter/payload_spec.rb +33 -0
  51. data/spec/unit/request/parameter/query_spec.rb +25 -0
  52. data/spec/unit/request/params_spec.rb +8 -0
  53. data/spec/unit/request/patch_spec.rb +15 -0
  54. data/spec/unit/request/path_spec.rb +9 -0
  55. data/spec/unit/request/post_spec.rb +15 -0
  56. data/spec/unit/request/put_spec.rb +15 -0
  57. data/spec/unit/request/server/hostname_spec.rb +9 -0
  58. data/spec/unit/request/server/port_spec.rb +7 -0
  59. data/spec/unit/request/server/protocol_spec.rb +9 -0
  60. data/spec/unit/request/server/software_spec.rb +9 -0
  61. data/spec/unit/request/server_spec.rb +8 -0
  62. data/spec/unit/response/response_spec.rb +146 -0
  63. data/spec/unit/router/call_spec.rb +55 -0
  64. data/zero.gemspec +27 -0
  65. metadata +345 -0
@@ -0,0 +1,28 @@
1
+ require_relative 'accept_type'
2
+
3
+ module Zero
4
+ class Request
5
+ # encapsulates the accept header to easier work with
6
+ # this is partly copied from sinatra
7
+ class Accept
8
+ MEDIA_TYPE_SEPERATOR = ','
9
+ MEDIA_PARAM_SEPERATOR = ';'
10
+ MEDIA_QUALITY_REGEX = /q=[01]\./
11
+
12
+ KEY_HTTP_ACCEPT = 'HTTP_ACCEPT'
13
+ KEY_HTTP_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'
14
+ KEY_HTTP_ACCEPT_ENCODING = 'HTTP_ACCEPT_ENCODING'
15
+
16
+ # create a new accept object
17
+ def initialize(environment)
18
+ @types = AcceptType.new(environment[KEY_HTTP_ACCEPT])
19
+ @language = AcceptType.new(environment[KEY_HTTP_ACCEPT_LANGUAGE])
20
+ @encoding = AcceptType.new(environment[KEY_HTTP_ACCEPT_ENCODING])
21
+ end
22
+
23
+ attr_reader :types
24
+ attr_reader :language
25
+ attr_reader :encoding
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,58 @@
1
+ module Zero
2
+ class Request
3
+ # This class provides an interface to access information of accept schemas.
4
+ class AcceptType
5
+ MEDIA_TYPE_SEPERATOR = ','
6
+ MEDIA_PARAM_SEPERATOR = ';'
7
+ MEDIA_QUALITY_REGEX = /q=[01]\./
8
+
9
+ # create a new instance of AcceptType
10
+ def initialize(string)
11
+ if string.nil?
12
+ @elements = []
13
+ else
14
+ @elements = parse_elements(string)
15
+ end
16
+ end
17
+
18
+ # return the preferred type
19
+ # @return String the preferred media type
20
+ def preferred
21
+ @elements.first
22
+ end
23
+
24
+ # iterate over all media types
25
+ def each
26
+ @elements.each {|element| yield element}
27
+ end
28
+
29
+ private
30
+
31
+ # converts the accept string to a useable array
32
+ # @param string the string containing media ranges and options
33
+ def parse_elements(string = '*/*')
34
+ string.
35
+ gsub(/\s/, '').
36
+ split(MEDIA_TYPE_SEPERATOR).
37
+ map do |accept_range|
38
+ extract_order(*accept_range.split(MEDIA_PARAM_SEPERATOR))
39
+ end.
40
+ sort_by(&:last).
41
+ map(&:first)
42
+ end
43
+
44
+ # extract the order of the type
45
+ # @param media_type the type itself
46
+ # @param params further options to the type
47
+ # @return Array the media type and quality in that order
48
+ def extract_order(media_type, *params)
49
+ params.each do |param|
50
+ if param.match(MEDIA_QUALITY_REGEX)
51
+ return [media_type, 10 - param[4..-1].to_i]
52
+ end
53
+ end
54
+ [media_type, 0]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,31 @@
1
+ module Zero
2
+ class Request
3
+ # This class represents all information about the client of a request.
4
+ class Client
5
+ # the key for the ip of the client
6
+ KEY_REMOTE_ADDR = 'REMOTE_ADDR'
7
+ # the key for the hostname
8
+ KEY_REMOTE_HOST = 'REMOTE_HOST'
9
+ # the key for the user agent
10
+ KEY_USER_AGENT = 'HTTP_USER_AGENT'
11
+
12
+ # creates a new client with the data of the request environment
13
+ # @param environment a hash representation of the request
14
+ def initialize(environment)
15
+ @address = environment[KEY_REMOTE_ADDR]
16
+ @hostname = environment[KEY_REMOTE_HOST]
17
+ @user_agent = environment[KEY_USER_AGENT]
18
+ end
19
+
20
+ # the ip address of the client
21
+ # @return [String] the address of the client
22
+ attr_reader :address
23
+ # the hostname of the client
24
+ # @return [String] the hostname of the client
25
+ attr_reader :hostname
26
+ # the user agent of the client
27
+ # @return [String] the user agent of the client
28
+ attr_reader :user_agent
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,101 @@
1
+ # TODO should that go into the main zero file?
2
+ require 'set'
3
+
4
+ module Zero
5
+ class Request
6
+ # represents all parameter set in a session
7
+ #
8
+ # This class holds all parameters available in the rack environment, split
9
+ # on query and payload parameters.
10
+ class Parameter
11
+ # they key for the query string
12
+ ENV_KEY_QUERY = 'QUERY_STRING'
13
+ # the key for the payload
14
+ ENV_KEY_PAYLOAD = 'rack.input'
15
+ # the key for custom parameters
16
+ ENV_KEY_CUSTOM = 'zero.params.custom'
17
+ # the key for the content type
18
+ ENV_KEY_CONTENT_TYPE = 'CONTENT_TYPE'
19
+ # all content types which used for using the body as a parameter input
20
+ PAYLOAD_CONTENT_TYPES = [
21
+ 'application/x-www-form-urlencoded',
22
+ 'multipart/form-data'
23
+ ].to_set
24
+
25
+ # get the query parameters
26
+ attr_reader :query
27
+ alias_method(:get, :query)
28
+
29
+ # get the payload or form data parameters
30
+ attr_reader :payload
31
+ alias_method(:post, :payload)
32
+
33
+ # get all custom parameters
34
+ attr_reader :custom
35
+
36
+ # creates a new parameter instance
37
+ #
38
+ # This should never be called directly, as it will be generated for you.
39
+ # This instance gives you the options to get query parameters (mostly
40
+ # called GET parameters) and payload parameters (or POST parameters).
41
+ # @param environment [Hash] the rack environment
42
+ def initialize(environment)
43
+ @query = extract_query_params(environment)
44
+ @payload = extract_payload_params(environment)
45
+ if environment.has_key?(ENV_KEY_CUSTOM)
46
+ @custom = environment[ENV_KEY_CUSTOM]
47
+ else
48
+ @custom = {}
49
+ environment[ENV_KEY_CUSTOM] = @custom
50
+ end
51
+ end
52
+
53
+ # get a parameter
54
+ #
55
+ # With this method you can get the value of a parameter. First the
56
+ # custom parameters are checked, then payload and after that the query
57
+ # ones.
58
+ #
59
+ # *Beware, that this may lead to security holes!*
60
+ #
61
+ # @param key [String] a key to look for
62
+ # @returns [String] the value of the key
63
+ def [](key)
64
+ @custom[key] || @payload[key] || @query[key]
65
+ end
66
+
67
+ # set a custom key/value pair
68
+ #
69
+ # Use this method if you want to set a custom parameter for later use. If
70
+ # the key was already set it will be overwritten.
71
+ # @param key [String] the key to use for saving the parameter
72
+ # @param value [Object] the value for the key
73
+ def []=(key, value)
74
+ @custom[key] = value
75
+ end
76
+
77
+ private
78
+
79
+ # extracts the key value pairs from the environment
80
+ # @return Hash all key value pairs from query string
81
+ def extract_query_params(environment)
82
+ return {} if environment[ENV_KEY_QUERY].length == 0
83
+ parse_string(environment[ENV_KEY_QUERY])
84
+ end
85
+
86
+ # extracts the key value pairs from the body
87
+ # @return Hash all key value pairs from the payload
88
+ def extract_payload_params(environment)
89
+ return {} unless PAYLOAD_CONTENT_TYPES.include?(environment[ENV_KEY_CONTENT_TYPE])
90
+ parse_string(environment[ENV_KEY_PAYLOAD].read)
91
+ end
92
+
93
+ # parse the query string like input to a hash
94
+ # @param query [String] the query string
95
+ # @return [Hash] the key/valuie pairs
96
+ def parse_string(query)
97
+ Hash[URI.decode_www_form(query)]
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,41 @@
1
+ module Zero
2
+ class Request
3
+ # This class represents all server related information of a request.
4
+ class Server
5
+ # the key for the server name
6
+ # @api private
7
+ KEY_SERVER_NAME = 'SERVER_NAME'
8
+ # the key for the server port
9
+ # @api private
10
+ KEY_SERVER_PORT = 'SERVER_PORT'
11
+ # the key for the server protocol
12
+ # @api private
13
+ KEY_SERVER_PROTOCOL = 'SERVER_PROTOCOL'
14
+ # the key for the server software
15
+ # @api private
16
+ KEY_SERVER_SOFTWARE = 'SERVER_SOFTWARE'
17
+
18
+ # This creates a new server instance extracting all server related
19
+ # information from the environment.
20
+ def initialize(environment)
21
+ @hostname = environment[KEY_SERVER_NAME]
22
+ @port = environment[KEY_SERVER_PORT].to_i
23
+ @protocol = environment[KEY_SERVER_PROTOCOL]
24
+ @software = environment[KEY_SERVER_SOFTWARE]
25
+ end
26
+
27
+ # get the port
28
+ # @return [Numeric] the port
29
+ attr_reader :port
30
+ # get the hostname of the server
31
+ # @return [String] the hostname
32
+ attr_reader :hostname
33
+ # get the protocol of the server (normally it should be HTTP/1.1)
34
+ # @return [String] the protocol
35
+ attr_reader :protocol
36
+ # get the server software
37
+ # @return [String] the server software name
38
+ attr_reader :software
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,80 @@
1
+ module Zero
2
+
3
+ # This is the representation of a response
4
+ #
5
+ class Response
6
+ attr_reader :status
7
+ attr_accessor :header, :body
8
+
9
+ # Constructor
10
+ # Sets default status code to 200.
11
+ #
12
+ def initialize
13
+ @status = 200
14
+ @header = {}
15
+ @body = []
16
+ end
17
+
18
+ # Sets the status.
19
+ # Also converts every input directly to an integer.
20
+ #
21
+ # @param [Integer] status The status code
22
+ #
23
+ def status=(status)
24
+ @status = status.to_i
25
+ end
26
+
27
+ # Returns the data of the response as an array:
28
+ # [status, header, body]
29
+ # to be usable by any webserver.
30
+ #
31
+ # Sets the Content-Type to 'text/html', if it's not already set.
32
+ # Sets the Content-Length, if it's not already set. (Won't fix wrong
33
+ # lengths!)
34
+ # Removes Content-Type, Content-Length and body on status code 204 and 304.
35
+ #
36
+ # @return [Array] Usable by webservers
37
+ #
38
+ def to_a
39
+ # Remove content length and body, on status 204 and 304
40
+ if status == 204 or status == 304
41
+ header.delete('Content-Length')
42
+ header.delete('Content-Type')
43
+ self.body = []
44
+ else
45
+ # Set content length, if not already set
46
+ content_length unless header.has_key? 'Content-Length'
47
+ # Set content type, if not already set
48
+ self.content_type = 'text/html' unless header.has_key? 'Content-Type'
49
+ end
50
+
51
+ [status, header, body]
52
+ end
53
+
54
+ # Sets the content length header to the current length of the body
55
+ # Also creates one, if it does not exists
56
+ #
57
+ def content_length
58
+ self.header['Content-Length'] = body.join.bytesize.to_s
59
+ end
60
+
61
+ # Sets the content type header to the given value
62
+ # Also creates it, if it does not exists
63
+ #
64
+ # @param [String] value Content-Type tp set
65
+ #
66
+ def content_type=(value)
67
+ self.header['Content-Type'] = value
68
+ end
69
+
70
+ # Sets the Location header to the given URL and the status code to 302.
71
+ #
72
+ # @param [String] location Redirect URL
73
+ #
74
+ def redirect(location, status = 302)
75
+ self.status = status
76
+ self.header['Location'] = location
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,63 @@
1
+ module Zero
2
+ # makes it possible to route urls to rack applications
3
+ #
4
+ # This class can be used to build a small rack application which routes
5
+ # requests to the given application.
6
+ # In the URLs it is also possible to use placeholders which then get assigned
7
+ # as variables to the parameters.
8
+ #
9
+ # @example of a simple router
10
+ # router = Zero::Router.new(
11
+ # '/' => WelcomeController,
12
+ # '/posts' => PostController
13
+ # )
14
+ #
15
+ # @example of a router with variables
16
+ # router = Zero::Router.new(
17
+ # '/foo/:id' => FooController
18
+ # )
19
+ class Router
20
+ # match for variables in routes
21
+ VARIABLE_MATCH = %r{:(\w+)[^/]?}
22
+ # the replacement string to make it an regex
23
+ VARIABLE_REGEX = '(?<\1>.+?)'
24
+
25
+ # create a new router instance
26
+ #
27
+ # @example of a simple router
28
+ # router = Zero::Router.new(
29
+ # '/' => WelcomeController,
30
+ # '/posts' => PostController
31
+ # )
32
+ #
33
+ # @param routes [Hash] a map of URLs to rack compatible applications
34
+ def initialize(routes)
35
+ @routes = {}
36
+ routes.each do |route, target|
37
+ @routes[
38
+ Regexp.new(
39
+ route.gsub(VARIABLE_MATCH, VARIABLE_REGEX) + '$')] = target
40
+ end
41
+ end
42
+
43
+ # call the router and call the matching application
44
+ #
45
+ # This method has to be called with a rack compatible environment, then the
46
+ # method will find a matching route and call the application.
47
+ # @param env [Hash] a rack environment
48
+ # @return [Array] a rack compatible response
49
+ def call(env)
50
+ request = Zero::Request.create(env)
51
+ @routes.each do |route, target|
52
+ match = route.match(request.path)
53
+ if match
54
+ match.names.each_index do |i|
55
+ request.params[match.names[i]] = match.captures[i]
56
+ end
57
+ return target.call(request.env)
58
+ end
59
+ end
60
+ [404, {'Content-Type' => 'text/html'}, ['Not found!']]
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Zero
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1 @@
1
+ success
@@ -0,0 +1 @@
1
+ {text: 'success'}
@@ -0,0 +1,65 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter '_spec.rb'
4
+ add_filter 'spec_helper.rb'
5
+ end
6
+
7
+ require 'rack'
8
+ require 'erb'
9
+ require 'tilt'
10
+ require 'zero'
11
+
12
+ class SpecTemplateContext
13
+ attr_accessor :name
14
+
15
+ def initialize(name)
16
+ @name = name
17
+ end
18
+ end
19
+
20
+ class SpecController < Zero::Controller
21
+ def process; end
22
+ def render; @response = [200, {'Content-Type' => 'text/html'}, ['foo']]; end
23
+ end
24
+
25
+ class SpecApp
26
+ attr_reader :env
27
+
28
+ def self.call(env)
29
+ @env = env
30
+ return [200, {'Content-Type' => 'text/html'}, ['success']]
31
+ end
32
+ end
33
+
34
+ class EnvGenerator
35
+ KEY_REQUEST_METHOD = 'REQUEST_METHOD'
36
+ KEY_REQUEST_GET = 'GET'
37
+ KEY_REQUEST_HEAD = 'HEAD'
38
+ KEY_REQUEST_POST = 'POST'
39
+ KEY_REQUEST_PUT = 'PUT'
40
+ KEY_REQUEST_DELETE = 'DELETE'
41
+
42
+ def self.generate_env(uri, options)
43
+ Rack::MockRequest.env_for(uri, options)
44
+ end
45
+
46
+ def self.get(uri, options = {})
47
+ generate_env(uri, options.merge(KEY_REQUEST_METHOD => KEY_REQUEST_GET))
48
+ end
49
+
50
+ def self.head(uri, options = {})
51
+ generate_env(uri, options.merge(KEY_REQUEST_METHOD => KEY_REQUEST_HEAD))
52
+ end
53
+
54
+ def self.post(uri, options = {})
55
+ generate_env(uri, options.merge(KEY_REQUEST_METHOD => KEY_REQUEST_POST))
56
+ end
57
+
58
+ def self.put(uri, options = {})
59
+ generate_env(uri, options.merge(KEY_REQUEST_METHOD => KEY_REQUEST_PUT))
60
+ end
61
+
62
+ def self.delete(uri, options = {})
63
+ generate_env(uri, options.merge(KEY_REQUEST_METHOD => KEY_REQUEST_DELETE))
64
+ end
65
+ end