flame 1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c69eaa9cc9a97a1ad4962f3e54011450dc1e1f11
4
+ data.tar.gz: 91d5016a225a54dbd43b36b8ef12b07f9b174051
5
+ SHA512:
6
+ metadata.gz: 669fed7b35fc590a1c895292f704fe2d20a112189c34e14ee87fbf95ae70441f425a45648d070d1cf20809077234cbfc716809394290141d74f062dc978772b5
7
+ data.tar.gz: 7ec7a41b6c8ba4b58c83ad613df1f54280504d3cbbb0f22bc27cb7f4827f16e1fb8bdb269b821fe75abb6be69c9c1e0998bd5323bae54d74b1fd8be62e0bb920
data/lib/flame.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'gorilla-patch/string'
2
+
3
+ require_relative './flame/application'
4
+ require_relative './flame/controller'
@@ -0,0 +1,43 @@
1
+ require_relative './router'
2
+ require_relative './dispatcher'
3
+
4
+ module Flame
5
+ ## Core class, like Framework::Application
6
+ class Application
7
+ ## Framework configuration
8
+ def self.config
9
+ @config ||= {}
10
+ end
11
+
12
+ def config
13
+ self.class.config
14
+ end
15
+
16
+ def self.inherited(app)
17
+ root_dir = File.dirname(caller[0].split(':')[0])
18
+ app.config.merge!(
19
+ root_dir: root_dir,
20
+ public_dir: File.join(root_dir, 'public'),
21
+ views_dir: File.join(root_dir, 'views')
22
+ )
23
+ end
24
+
25
+ ## Init function
26
+ def call(env)
27
+ Dispatcher.new(self, env).run!
28
+ end
29
+
30
+ def self.mount(ctrl, path = nil, &block)
31
+ router.add_controller(ctrl, path, block)
32
+ end
33
+
34
+ ## Router for routing
35
+ def self.router
36
+ @router ||= Flame::Router.new
37
+ end
38
+
39
+ def router
40
+ self.class.router
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ require_relative './render'
2
+
3
+ module Flame
4
+ ## Class for controllers helpers, like Framework::Controller
5
+ class Controller
6
+ include Flame::Render
7
+
8
+ def initialize(dispatcher)
9
+ @dispatcher = dispatcher
10
+ end
11
+
12
+ def config
13
+ @dispatcher.config
14
+ end
15
+
16
+ def params
17
+ @dispatcher.params
18
+ end
19
+
20
+ def path_to(*params)
21
+ @dispatcher.path_to(*params)
22
+ end
23
+
24
+ ## TODO: Add more helpers
25
+ end
26
+ end
@@ -0,0 +1,102 @@
1
+ require 'rack'
2
+
3
+ module Flame
4
+ ## Class initialize when Application.call(env) invoked
5
+ ## For new request and response
6
+ class Dispatcher
7
+ def initialize(app, env)
8
+ @app = app
9
+ @env = env
10
+ end
11
+
12
+ def run!
13
+ body = catch :halt do
14
+ try_route ||
15
+ try_static ||
16
+ try_static(File.join(__dir__, '..', '..', 'public')) ||
17
+ halt(404)
18
+ end
19
+ response.write body
20
+ response.finish
21
+ end
22
+
23
+ ## Helpers
24
+ def config
25
+ @app.config
26
+ end
27
+
28
+ def request
29
+ @request ||= Rack::Request.new(@env)
30
+ end
31
+
32
+ def params
33
+ request.params
34
+ end
35
+
36
+ def response
37
+ @response ||= Rack::Response.new
38
+ end
39
+
40
+ def status(value = nil)
41
+ response.status ||= 200
42
+ value ? response.status = value : response.status
43
+ end
44
+
45
+ def halt(new_status, body = '', new_headers = {})
46
+ status new_status
47
+ response.headers.merge!(new_headers)
48
+ # p response.body
49
+ if body.empty? &&
50
+ !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
51
+ body = Rack::Utils::HTTP_STATUS_CODES[status]
52
+ end
53
+ throw :halt, body
54
+ end
55
+
56
+ def path_to(ctrl, action, args = {})
57
+ route = @app.router.find_route(controller: ctrl, action: action)
58
+ fail RouteNotFoundError.new(ctrl, action) unless route
59
+ route.assign_arguments(args)
60
+ end
61
+
62
+ private
63
+
64
+ def try_route
65
+ method = params['_method'] || request.request_method
66
+ path = request.path_info
67
+ route = @app.router.find_route(
68
+ method: method,
69
+ path: path
70
+ )
71
+ # p route
72
+ return nil unless route
73
+ status 200
74
+ route.execute(self)
75
+ end
76
+
77
+ def try_static(dir = config[:public_dir])
78
+ static_file = File.join(dir, request.path_info)
79
+ # p static_file
80
+ return nil unless File.exist?(static_file) && File.file?(static_file)
81
+ return_static(static_file)
82
+ end
83
+
84
+ def return_static(file)
85
+ since = @env['HTTP_IF_MODIFIED_SINCE']
86
+ file_time = File.mtime(file)
87
+ halt 304 if since && Time.httpdate(since).to_i >= file_time.to_i
88
+ response.headers.merge!(
89
+ 'Content-Type' => file_mime_type(file),
90
+ 'Last-Modified' => file_time.httpdate
91
+ # 'Content-Disposition' => 'attachment;' \
92
+ # "filename=\"#{File.basename(static_file)}\"",
93
+ # 'Content-Length' => File.size?(static_file).to_s
94
+ )
95
+ halt 200, File.read(file)
96
+ end
97
+
98
+ def file_mime_type(file)
99
+ Rack::Mime.mime_type(File.extname(file))
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,80 @@
1
+ module Flame
2
+ module RouterError
3
+ ## Error for Flame::Router.compare_actions
4
+ class ActionsError < StandardError
5
+ def initialize(ctrl, extra_actions)
6
+ @ctrl = ctrl
7
+ @extra_actions = extra_actions
8
+ end
9
+ end
10
+
11
+ ## Error if routes have more actions, than controller
12
+ class ExtraRoutesActionsError < ActionsError
13
+ def message
14
+ "Controller '#{@ctrl}' has no methods" \
15
+ " '#{@extra_actions.join(', ')}' from routes"
16
+ end
17
+ end
18
+
19
+ ## Error if controller has not assigned in routes actions
20
+ class ExtraControllerActionsError < ActionsError
21
+ def message
22
+ "Routes for '#{@ctrl}' has no methods" \
23
+ " '#{@extra_actions.join(', ')}'"
24
+ end
25
+ end
26
+
27
+ ## Error for Flame::Router::RouteRefine.arguments_valid?
28
+ class ArgumentsError < StandardError
29
+ def initialize(ctrl, action, path, extra_args)
30
+ @ctrl = ctrl
31
+ @action = action
32
+ @path = path
33
+ @extra_args = extra_args
34
+ end
35
+ end
36
+
37
+ ## Error if path has more arguments, than controller's method
38
+ class ExtraPathArgumentsError < ArgumentsError
39
+ def message
40
+ "Method '#{@action}' from controller '#{@ctrl}'" \
41
+ " does not know arguments '#{@extra_args.join(', ')}'" \
42
+ " from path '#{@path}'"
43
+ end
44
+ end
45
+
46
+ ## Error if path has no arguments, that controller's method required
47
+ class ExtraActionArgumentsError < ArgumentsError
48
+ def message
49
+ "Path '#{@path}' does not contain required arguments" \
50
+ " '#{@extra_args.join(', ')}' of method '#{@action}'" \
51
+ " from controller '#{@ctrl}'"
52
+ end
53
+ end
54
+ end
55
+
56
+ ## Error for Flame::Router.find_path
57
+ class RouteNotFoundError < StandardError
58
+ def initialize(ctrl, method)
59
+ @ctrl = ctrl
60
+ @method = method
61
+ end
62
+
63
+ def message
64
+ "Route with controller '#{@ctrl}' and method '#{@method}'" \
65
+ ' not found in application routes'
66
+ end
67
+ end
68
+
69
+ ## Error for Flame::Controller.path_to
70
+ class ArgumentNotAssigned < StandardError
71
+ def initialize(path, path_part)
72
+ @path = path
73
+ @path_part = path_part
74
+ end
75
+
76
+ def message
77
+ "Argument '#{@path_part}' for '#{@path}' is not assigned"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,49 @@
1
+ require 'tilt'
2
+
3
+ module Flame
4
+ ## Helper for render functionality
5
+ module Render
6
+ def view(path, options = {})
7
+ ## Take options for rendering
8
+ scope = options.delete(:scope) || self
9
+ layout = options.delete(:layout) || 'layout.*'
10
+ ## And get the rest variables to locals
11
+ locals = options.merge(options.delete(:locals) || {})
12
+ ## Find filename
13
+ filename = find_file(path)
14
+ ## Compile Tilt to instance hash
15
+ @tilts ||= {}
16
+ @tilts[filename] ||= Tilt.new(filename)
17
+ ## Render Tilt from instance hash with new options
18
+ layout_render layout, @tilts[filename].render(scope, locals)
19
+ end
20
+
21
+ alias_method :render, :view
22
+
23
+ private
24
+
25
+ ## TODO: Add `views_dir` for Application and Controller
26
+ ## TODO: Add `layout` method for Controller
27
+ def find_file(path)
28
+ ## Build controller_dir
29
+ controller_dir = (
30
+ self.class.name.split(/(?=[A-Z])/) - ['Controller']
31
+ ).join('_').downcase
32
+ ## Get full filename
33
+ Dir[File.join(
34
+ config[:views_dir],
35
+ "{#{controller_dir},}",
36
+ "#{path}.*"
37
+ )].find do |file|
38
+ Tilt[file]
39
+ end
40
+ end
41
+
42
+ def layout_render(layout, result)
43
+ layout_filename = find_file(layout)
44
+ ## Compile layout to hash
45
+ @tilts[layout_filename] ||= Tilt.new(layout_filename)
46
+ @tilts[layout_filename] ? @tilts[layout_filename].render { result } : result
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,114 @@
1
+ module Flame
2
+ ## Class for Route in Router.routes
3
+ class Route
4
+ attr_reader :attributes
5
+
6
+ def initialize(attrs = {})
7
+ @attributes = attrs
8
+ end
9
+
10
+ def [](attribute)
11
+ @attributes[attribute]
12
+ end
13
+
14
+ ## Arguments in order as parameters of method of controller
15
+ def arrange_arguments(args = {})
16
+ self[:controller].instance_method(self[:action]).parameters
17
+ .each_with_object([]) do |par, arr|
18
+ arr << args[par[1]] if par[0] == :req || args[par[1]]
19
+ end
20
+ end
21
+
22
+ ## Compare attributes for `Router.find_route`
23
+ def compare_attributes(attrs)
24
+ attrs.each do |name, value|
25
+ next true if compare_attribute(name, value)
26
+ break false
27
+ end
28
+ end
29
+
30
+ ## Assign arguments to path for `Controller.path_to`
31
+ def assign_arguments(args = {})
32
+ extract_path_parts
33
+ .map { |path_part| assign_argument(path_part, args) }
34
+ .unshift('').join('/').gsub(%r{\/{2,}}, '/')
35
+ end
36
+
37
+ ## Execute by Application.call
38
+ def execute(app)
39
+ ctrl = self[:controller].new(app)
40
+ args = extract_arguments(app.request.path_info)
41
+ app.params.merge!(args)
42
+ ctrl.send(self[:action], *arrange_arguments(args))
43
+ end
44
+
45
+ private
46
+
47
+ ## Helpers for attributes accessors
48
+ def extract_path_parts(other_path = self[:path])
49
+ other_path.to_s.split('/').reject(&:empty?)
50
+ end
51
+
52
+ ## Extract arguments from request_path for `execute`
53
+ def extract_arguments(request_path)
54
+ args = {}
55
+ request_parts = extract_path_parts(request_path)
56
+ extract_path_parts.each_with_index do |path_part, i|
57
+ next unless path_part[0] == ':'
58
+ args[
59
+ path_part[(path_part[1] == '?' ? 2 : 1)..-1].to_sym
60
+ ] = URI.decode(request_parts[i])
61
+ end
62
+ args
63
+ end
64
+
65
+ ## Helpers for `compare_attributes`
66
+ def compare_attribute(name, value)
67
+ case name
68
+ when :method
69
+ compare_method(value)
70
+ when :path
71
+ compare_path(value)
72
+ else
73
+ self[name] == value
74
+ end
75
+ end
76
+
77
+ def compare_method(request_method)
78
+ self[:method].upcase.to_sym == request_method.upcase.to_sym
79
+ end
80
+
81
+ def compare_path(request_path)
82
+ path_parts = extract_path_parts
83
+ request_parts = extract_path_parts(request_path)
84
+ # p route_path
85
+ req_path_parts = path_parts.select { |part| part[1] != '?' }
86
+ return false if request_parts.count < req_path_parts.count
87
+ compare_parts(request_parts, path_parts)
88
+ end
89
+
90
+ def compare_parts(request_parts, path_parts)
91
+ request_parts.each_with_index do |request_part, i|
92
+ path_part = path_parts[i]
93
+ # p request_part, path_part
94
+ break false unless path_part
95
+ next if path_part[0] == ':'
96
+ break false unless request_part == path_part
97
+ end
98
+ end
99
+
100
+ ## Helpers for `assign_arguments`
101
+ def assign_argument(path_part, args = {})
102
+ ## Not argument
103
+ return path_part unless path_part[0] == ':'
104
+ ## Not required argument
105
+ return args[path_part[2..-1].to_sym] if path_part[1] == '?'
106
+ ## Required argument
107
+ param = args[path_part[1..-1].to_sym]
108
+ ## Required argument is nil
109
+ fail ArgumentNotAssigned.new(self[:path], path_part) if param.nil?
110
+ ## All is ok
111
+ param
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,126 @@
1
+ require_relative './route.rb'
2
+ require_relative './validators.rb'
3
+
4
+ module Flame
5
+ ## Router class for routing
6
+ class Router
7
+ attr_accessor :routes
8
+
9
+ def initialize
10
+ @routes = []
11
+ end
12
+
13
+ def add_controller(ctrl, path, block = nil)
14
+ ## TODO: Add Regexp paths
15
+ ## TODO: Add `before` and `after` methods
16
+
17
+ ## Add routes from controller to glob array
18
+ ctrl_routes = RouteRefine.new(ctrl, path, block).routes
19
+ ActionsValidator.new(ctrl_routes, ctrl).valid?
20
+ routes.concat(ctrl_routes)
21
+ end
22
+
23
+ ## Find route by any attributes
24
+ def find_route(attrs)
25
+ routes.find { |route| route.compare_attributes(attrs) }
26
+ end
27
+
28
+ private
29
+
30
+ ## Helper module for routing refine
31
+ class RouteRefine
32
+ attr_accessor :rest_routes
33
+ attr_reader :routes
34
+
35
+ def self.http_methods
36
+ [:GET, :POST, :PUT, :DELETE]
37
+ end
38
+
39
+ def rest_routes
40
+ @rest_routes ||= [
41
+ { method: :GET, path: '/', action: :index },
42
+ { method: :POST, path: '/', action: :create },
43
+ { method: :GET, path: '/', action: :show },
44
+ { method: :PUT, path: '/', action: :update },
45
+ { method: :DELETE, path: '/', action: :delete }
46
+ ]
47
+ end
48
+
49
+ def initialize(ctrl, path, block)
50
+ @ctrl = ctrl
51
+ @path = path || default_controller_path
52
+ @routes = []
53
+ block.nil? ? defaults : instance_exec(&block)
54
+ # p @routes
55
+ @routes.sort! { |a, b| b[:path] <=> a[:path] }
56
+ end
57
+
58
+ http_methods.each do |request_method|
59
+ define_method(request_method.downcase) do |path, action|
60
+ ArgumentsValidator.new(@ctrl, path, action).valid?
61
+ add_route(request_method, path, action)
62
+ end
63
+ end
64
+
65
+ def defaults
66
+ @ctrl.public_instance_methods(false).each do |action|
67
+ next if find_route_index(action: action)
68
+ add_route(:GET, nil, action)
69
+ end
70
+ end
71
+
72
+ def rest
73
+ rest_routes.each do |rest_route|
74
+ action = rest_route[:action]
75
+ if @ctrl.public_instance_methods.include?(action) &&
76
+ find_route_index(action: action).nil?
77
+ add_route(*rest_route.values, true)
78
+ end
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ using GorillaPatch::StringExt
85
+
86
+ def default_controller_path
87
+ @ctrl.name.underscore
88
+ .split('_')
89
+ .take_while { |part| part != 'controller' }
90
+ .unshift(nil)
91
+ .join('/')
92
+ end
93
+
94
+ def make_path(path, action = nil, force_params = false)
95
+ ## TODO: Add :arg:type support (:id:num, :name:str, etc.)
96
+ unshifted = force_params ? path : action_path(action)
97
+ if path.nil? || force_params
98
+ path = @ctrl.instance_method(action).parameters
99
+ .map { |par| ":#{par[0] == :req ? '' : '?'}#{par[1]}" }
100
+ .unshift(unshifted)
101
+ .join('/')
102
+ end
103
+ "#{@path}/#{path}".gsub(%r{\/{2,}}, '/')
104
+ end
105
+
106
+ def action_path(action)
107
+ action == :index ? '/' : action
108
+ end
109
+
110
+ def add_route(method, path, action, force_params = false)
111
+ route = Route.new(
112
+ method: method,
113
+ path: make_path(path, action, force_params),
114
+ controller: @ctrl,
115
+ action: action
116
+ )
117
+ index = find_route_index(action: action)
118
+ index ? @routes[index] = route : @routes.push(route)
119
+ end
120
+
121
+ def find_route_index(attrs)
122
+ @routes.find_index { |route| route.compare_attributes(attrs) }
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,89 @@
1
+ require_relative './errors.rb'
2
+
3
+ module Flame
4
+ ## Compare arguments from path and from controller's action
5
+ class ArgumentsValidator
6
+ def initialize(ctrl, path, action)
7
+ @ctrl = ctrl
8
+ @path = path
9
+ @action = action
10
+ end
11
+
12
+ def valid?
13
+ ## Break path for ':arg' arguments
14
+ @path_args = path_arguments(@path)
15
+ ## Take all and required arguments from Controller#action
16
+ @action_args = action_arguments(@action)
17
+ ## Compare arguments from path and arguments from method
18
+ no_extra_path_arguments? && no_extra_action_arguments?
19
+ end
20
+
21
+ private
22
+
23
+ ## Split path to args array
24
+ def path_arguments(path)
25
+ args = path.split('/').select { |part| part[0] == ':' }
26
+ args.map { |arg| arg[1..-1].to_sym }
27
+ end
28
+
29
+ ## Take args from controller's action
30
+ def action_arguments(action)
31
+ parameters = @ctrl.instance_method(action).parameters
32
+ req_parameters = parameters.select { |par| par[0] == :req }
33
+ {
34
+ all: parameters.map { |par| par[1] },
35
+ req: req_parameters.map { |par| par[1] }
36
+ }
37
+ end
38
+
39
+ def no_extra_path_arguments?
40
+ ## Subtraction action args from path args
41
+ extra_path_args = @path_args - @action_args[:all]
42
+ return true if extra_path_args.empty?
43
+ fail RouterError::ExtraPathArgumentsError.new(
44
+ @ctrl, @action, @path, extra_path_args
45
+ )
46
+ end
47
+
48
+ def no_extra_action_arguments?
49
+ ## Subtraction path args from action required args
50
+ extra_action_args = @action_args[:req] - @path_args
51
+ return true if extra_action_args.empty?
52
+ fail RouterError::ExtraActionArgumentsError.new(
53
+ @ctrl, @action, @path, extra_action_args
54
+ )
55
+ end
56
+ end
57
+
58
+ ## Compare actions from routes and from controller
59
+ class ActionsValidator
60
+ def initialize(routes, ctrl)
61
+ @routes = routes
62
+ @ctrl = ctrl
63
+ end
64
+
65
+ def valid?
66
+ @routes_actions = @routes.map { |route| route[:action] }
67
+ @ctrl_actions = @ctrl.public_instance_methods(false)
68
+ no_extra_routes_actions? && no_extra_controller_actions?
69
+ end
70
+
71
+ private
72
+
73
+ def no_extra_routes_actions?
74
+ extra_routes_actions = @routes_actions - @ctrl_actions
75
+ return true if extra_routes_actions.empty?
76
+ fail RouterError::ExtraRoutesActionsError.new(
77
+ @ctrl, extra_routes_actions
78
+ )
79
+ end
80
+
81
+ def no_extra_controller_actions?
82
+ extra_ctrl_actions = @ctrl_actions - @routes_actions
83
+ return true if extra_ctrl_actions.empty?
84
+ fail RouterError::ExtraControllerActionsError.new(
85
+ @ctrl, extra_ctrl_actions
86
+ )
87
+ end
88
+ end
89
+ end
Binary file
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flame
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Popov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.6.4
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.6'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.6.4
33
+ - !ruby/object:Gem::Dependency
34
+ name: gorilla-patch
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.0'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 0.0.5.3
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '0.0'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 0.0.5.3
53
+ description: Use controller's classes with instance methods as routing actions, mounting
54
+ its in application class.
55
+ email:
56
+ - alex.wayfer@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/flame.rb
62
+ - lib/flame/application.rb
63
+ - lib/flame/controller.rb
64
+ - lib/flame/dispatcher.rb
65
+ - lib/flame/errors.rb
66
+ - lib/flame/render.rb
67
+ - lib/flame/route.rb
68
+ - lib/flame/router.rb
69
+ - lib/flame/validators.rb
70
+ - public/favicon.ico
71
+ homepage: https://gitlab.com/AlexWayfer/flame
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.4.5.1
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Web-framework, based on MVC-pattern
95
+ test_files: []