zero 0.1.0

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 (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