appril 0.0.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/Rakefile +1 -0
  4. data/app/.gitignore +6 -0
  5. data/app/.pryrc +1 -0
  6. data/app/Gemfile +5 -0
  7. data/app/Rakefile +1 -0
  8. data/app/base/api/.ignore +0 -0
  9. data/app/base/base_controller.rb +3 -0
  10. data/app/base/boot.rb +2 -0
  11. data/app/base/core.coffee +91 -0
  12. data/app/base/helpers/application_helpers.rb +1 -0
  13. data/app/base/load.rb +0 -0
  14. data/app/base/load_controllers.rb +2 -0
  15. data/app/base/rtcp_controller.rb +22 -0
  16. data/app/config/config.rb +9 -0
  17. data/app/config/config.yml +18 -0
  18. data/app/config/env/development.yml +0 -0
  19. data/app/config/env/production.yml +0 -0
  20. data/app/config/env/stage.yml +0 -0
  21. data/app/config/env/test.yml +0 -0
  22. data/app/config.ru +3 -0
  23. data/app/core/Gemfile +4 -0
  24. data/app/core/boot.rb +16 -0
  25. data/app/core/generate_configs.rb +7 -0
  26. data/app/core/load.rb +7 -0
  27. data/app/core/load_controllers.rb +15 -0
  28. data/app/generators/api/.ignore +0 -0
  29. data/app/package.json +5 -0
  30. data/app/public/.ignore +0 -0
  31. data/app/webpack.config.js +48 -0
  32. data/appril.gemspec +22 -0
  33. data/bin/appril +5 -0
  34. data/docker/Dockerfile +5 -0
  35. data/docker/run +68 -0
  36. data/docker/skel/build.sh +1 -0
  37. data/docker/skel/cleanup.sh +7 -0
  38. data/docker/skel/config.yml +24 -0
  39. data/docker/skel/prepare_build.sh +5 -0
  40. data/docker/skel/start.sh +1 -0
  41. data/docker/start +7 -0
  42. data/lib/appril/base_controller.rb +5 -0
  43. data/lib/appril/cli/app/install.rb +42 -0
  44. data/lib/appril/cli/app/update.rb +26 -0
  45. data/lib/appril/cli/app.rb +10 -0
  46. data/lib/appril/cli/assertions.rb +60 -0
  47. data/lib/appril/cli/docker/build.rb +150 -0
  48. data/lib/appril/cli/docker/install.rb +21 -0
  49. data/lib/appril/cli/docker/update.rb +24 -0
  50. data/lib/appril/cli/docker.rb +16 -0
  51. data/lib/appril/cli/generator.rb +15 -0
  52. data/lib/appril/cli/helpers.rb +47 -0
  53. data/lib/appril/cli.rb +197 -0
  54. data/lib/appril/rtcp_controller.rb +103 -0
  55. data/lib/appril/version.rb +3 -0
  56. data/lib/appril.rb +124 -0
  57. metadata +128 -0
@@ -0,0 +1,150 @@
1
+ module Appril
2
+ class CLI
3
+ module Docker
4
+ class Build
5
+
6
+ def initialize dir, update_runner_only: false, push_opted: false
7
+ Dir.chdir dir do
8
+
9
+ config = load_config
10
+ validate_config(config)
11
+
12
+ build_dir = Pathname.new(File.expand_path('__tmpbuildir__'))
13
+
14
+ prepare_build_dir(build_dir)
15
+
16
+ install_files(build_dir)
17
+
18
+ app_dir = Pathname.new(File.expand_path(config['APP_DIR']))
19
+
20
+ if update_runner_only
21
+ puts "Skipping image building"
22
+ else
23
+ prepare_build(build_dir, app_dir)
24
+
25
+ image_built = build_image(build_dir, config['IMAGE_NAME'], config['BUILD_OPTS'])
26
+
27
+ if image_built && push_opted
28
+ push_image(config['IMAGE_NAME'])
29
+ end
30
+ end
31
+
32
+ puts "Installing run script"
33
+ install_run_script(app_dir, *config.values_at('IMAGE_NAME', 'CONTAINER_NAME', 'RUN_SCRIPT', 'RUN_OPTS'))
34
+
35
+ FileUtils.rm_rf(build_dir)
36
+ end
37
+ end
38
+
39
+ def load_config
40
+ YAML.load(File.read(CONFIG_FILE))
41
+ end
42
+
43
+ def validate_config config
44
+ {
45
+ 'IMAGE_NAME' => :validate_config__image_name,
46
+ 'CONTAINER_NAME' => :validate_config__container_name,
47
+ 'APP_DIR' => :validate_config__app_dir,
48
+ 'RUN_SCRIPT' => :validate_config__run_script,
49
+ }.each_pair do |key,validator|
50
+ next if send(validator, config[key])
51
+ puts "", "\t::: Please set #{key} in config.yml :::", ""
52
+ exit 1
53
+ end
54
+ end
55
+
56
+ def validate_config__image_name value
57
+ !value.nil? && !value.empty?
58
+ end
59
+
60
+ def validate_config__container_name value
61
+ !value.nil? && !value.empty? && value.values.any?
62
+ end
63
+
64
+ def validate_config__app_dir value
65
+ !value.nil? && !value.empty?
66
+ end
67
+
68
+ def validate_config__run_script value
69
+ !value.nil? && !value.empty?
70
+ end
71
+
72
+ def prepare_build_dir dir
73
+ FileUtils.rm_rf(dir)
74
+ FileUtils.mkdir_p(dir / 'build')
75
+ end
76
+
77
+ def install_files dir
78
+ install_dockerfile(dir)
79
+ install_start_file(dir)
80
+ install_build_files(dir)
81
+ install_cleanup_file(dir)
82
+ end
83
+
84
+ def install_dockerfile dir
85
+ FileUtils.cp(BASE_DIR / 'Dockerfile', dir)
86
+ end
87
+
88
+ def install_start_file dir
89
+ File.open dir / 'start', 'w' do |f|
90
+ f << File.read(BASE_DIR / 'start').sub('{start}', File.read(START_FILE))
91
+ end
92
+ end
93
+
94
+ def install_build_files dir
95
+ FileUtils.cp(PREPARE_BUILD_FILE, dir / 'build')
96
+ FileUtils.cp(BUILD_FILE, dir / 'build/build')
97
+ end
98
+
99
+ def install_cleanup_file dir
100
+ FileUtils.cp(CLEANUP_FILE, dir / 'build/cleanup')
101
+ end
102
+
103
+ def prepare_build dir, app_dir
104
+ Dir.chdir dir / 'build' do
105
+ CLI.run "APP_DIR=#{app_dir} #{PREPARE_BUILD_FILE}"
106
+ exit 1 unless $? && $?.success?
107
+ end
108
+ end
109
+
110
+ def build_image dir, image_name, build_opts
111
+ CLI.run "docker build -t #{image_name} #{build_opts} '#{dir}'"
112
+ $? && $?.success?
113
+ end
114
+
115
+ def push_image image_name
116
+ CLI.run "docker push #{image_name}"
117
+ $? && $?.success?
118
+ end
119
+
120
+ def install_run_script app_dir, image_name, container_name, run_script, run_opts
121
+ FileUtils.mkdir_p(app_dir / File.dirname(run_script))
122
+
123
+ script_path = app_dir / run_script
124
+
125
+ File.open script_path, 'w' do |f|
126
+ f << File.read(BASE_DIR / 'run').
127
+ gsub('{image}', image_name).
128
+ gsub('{script_path_traversal}', script_path_traversal(run_script)).
129
+ gsub('{run_opts}', run_opts).
130
+ gsub('{environments}', container_name.keys.join(' ')).
131
+ gsub('{container_definitions}', container_definitions(container_name))
132
+ end
133
+
134
+ FileUtils.chmod('+x', script_path)
135
+ end
136
+
137
+ def container_definitions container_name
138
+ container_name.map do |kv|
139
+ '[ "$APP_ENV" = "%s" ] && CONTAINER_NAME="%s"' % kv
140
+ end.join("\n")
141
+ end
142
+
143
+ def script_path_traversal run_script
144
+ run_script.gsub(/\A\/+|\/+\Z/, '').scan(/\/+/).map {'..'}*'/'
145
+ end
146
+
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,21 @@
1
+ module Appril
2
+ class CLI
3
+ module Docker
4
+ class Install
5
+ include Helpers
6
+
7
+ def initialize dir, working_dir_opted: false
8
+ install(dir, working_dir_opted)
9
+ make_executable(dir / PREPARE_BUILD_FILE)
10
+ puts "Done. All files installed into #{dir}"
11
+ end
12
+
13
+ def install dir, working_dir_opted
14
+ src = working_dir_opted ? BASE_DIR.to_path + '/skel/.' : BASE_DIR / 'skel'
15
+ FileUtils.cp_r(src, dir)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ module Appril
2
+ class CLI
3
+ module Docker
4
+ class Update
5
+ include Helpers
6
+
7
+ def initialize dir
8
+ Dir.chdir BASE_DIR / 'skel' do
9
+
10
+ Dir['**/*'].select {|e| File.file?(e)}.each do |file|
11
+ next if File.file?(dir / file)
12
+ create_dirname_for(dir / file)
13
+ puts "Installing #{File.basename(dir)}/#{file}"
14
+ FileUtils.cp(file, dir / file)
15
+ end
16
+
17
+ end
18
+ puts "Done"
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module Appril
2
+ class CLI
3
+ module Docker
4
+ BASE_DIR = (Appril::BASE_DIR / 'docker').freeze
5
+ CONFIG_FILE = './config.yml'.freeze
6
+ START_FILE = './start.sh'.freeze
7
+ PREPARE_BUILD_FILE = './prepare_build.sh'.freeze
8
+ BUILD_FILE = './build.sh'.freeze
9
+ CLEANUP_FILE = './cleanup.sh'.freeze
10
+ end
11
+ end
12
+ end
13
+
14
+ require 'appril/cli/docker/install'
15
+ require 'appril/cli/docker/build'
16
+ require 'appril/cli/docker/update'
@@ -0,0 +1,15 @@
1
+ module Appril
2
+ class CLI
3
+ class Generator
4
+ class API
5
+ include Helpers
6
+
7
+ def initialize gen_dir, api_dir, api_name
8
+ FileUtils.cp_r(gen_dir, api_dir)
9
+ puts "Done"
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ module Appril
2
+ class CLI
3
+ module Helpers
4
+
5
+ def display_error error
6
+ puts "", "\t::: #{error} :::", ""
7
+ end
8
+
9
+ def fatal_error! error, exit_code = 1
10
+ display_error(error)
11
+ exit exit_code
12
+ end
13
+
14
+ def expanded_path *path
15
+ Pathname.new(File.expand_path(File.join(*path.map(&:to_s))))
16
+ end
17
+
18
+ def create_dirname_for dir
19
+ FileUtils.mkdir_p(File.dirname(dir))
20
+ end
21
+
22
+ def make_executable *entries
23
+ entries.flatten.each {|e| FileUtils.chmod('+x', e)}
24
+ end
25
+
26
+ def working_dir_opted? opted_dir
27
+ opted_dir == '.'
28
+ end
29
+
30
+ def extract_namespace args
31
+ return unless index = args.index('-n')
32
+ return unless namespace = args[index + 1]
33
+ if namespace =~ /::/
34
+ fatal_error! "Nested namespaces not supported"
35
+ end
36
+ if namespace =~ /\W/
37
+ fatal_error! "Namespace may contain only alphanumerics"
38
+ end
39
+ unless namespace =~ /\A[A-Z]/
40
+ fatal_error! "Namespace should start with a capital letter"
41
+ end
42
+ namespace
43
+ end
44
+
45
+ end
46
+ end
47
+ end
data/lib/appril/cli.rb ADDED
@@ -0,0 +1,197 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require 'yaml'
4
+ require 'pty'
5
+ require 'appril/version'
6
+ require 'appril/cli/helpers'
7
+ require 'appril/cli/assertions'
8
+
9
+ module Appril
10
+ BASE_DIR = Pathname.new(File.expand_path('../../..', __FILE__)).freeze
11
+
12
+ class CLI
13
+ include Helpers
14
+ include Assertions
15
+
16
+ def initialize args
17
+ case command = args[0]
18
+ when 'a', 'app'
19
+ app(args)
20
+ when 'g', 'gen', 'generate'
21
+ generator(args)
22
+ when 'd', 'docker'
23
+ docker(args)
24
+ when 'v', '-v', '--version'
25
+ puts Appril::VERSION
26
+ when nil, '-h', '--help'
27
+ usage
28
+ else
29
+ display_error "Unknown command #{command}"
30
+ usage
31
+ end
32
+ end
33
+
34
+
35
+ def usage
36
+ puts "
37
+ === Install a new app ===
38
+ $ appril [app || a] [install || i] [dir || .]
39
+
40
+ === Update existing app ===
41
+ $ appril [app || a] [update || u] [dir || .]
42
+
43
+ === Generate a new API ===
44
+ $ appril [generate || g] [api || a] [api name] [dir || .]
45
+
46
+ === Install Docker recipes ===
47
+ $ appril [docker || d] [install || i] [dir || .]
48
+
49
+ === Update Docker recipes ===
50
+ $ appril [docker || d] [update || u] [dir || .]
51
+
52
+ === Build Docker image and install run script ===
53
+ $ appril [docker || d] [build || b] [dir || .] [-u] [-p]
54
+ If -u option provided it will only update the run script without building the image.
55
+ If -p option provided it will try to push the image to Docker registry after successful build.
56
+
57
+ === Usage ===
58
+ $ appril [-h || --help]
59
+ ".split("\n").map {|l| "\t" + l.strip}.join("\n")
60
+ end
61
+
62
+
63
+ begin # App
64
+ def app args
65
+ opted_dir = args[2]
66
+ assert_directory_provided(opted_dir)
67
+ dir = expanded_path(opted_dir)
68
+
69
+ case instruction = args[1]
70
+ when 'i', 'install'
71
+
72
+ app_install(dir, {
73
+ working_dir_opted: working_dir_opted?(opted_dir),
74
+ namespace: extract_namespace(args)
75
+ })
76
+
77
+ when 'u', 'update'
78
+
79
+ app_update(dir)
80
+
81
+ else
82
+ unknown_instruction_error!(instruction, 'install (or i)', 'update (or u)')
83
+ end
84
+ end
85
+
86
+ def app_install dir, opts
87
+ create_dirname_for(dir)
88
+ assert_installable_dir(dir, opts[:working_dir_opted])
89
+ App::Install.new(dir, opts)
90
+ end
91
+
92
+ def app_update dir
93
+ assert_is_app_dir(dir)
94
+ App::Update.new(dir)
95
+ end
96
+ end
97
+
98
+
99
+ begin # Generator
100
+ def generator args
101
+
102
+ case instruction = args[1]
103
+ when 'api', 'a'
104
+ api_name = args[2]
105
+ assert_valid_api_name_given(api_name)
106
+
107
+ app_dir = args[3]
108
+ assert_directory_provided(app_dir)
109
+ app_dir = expanded_path(app_dir)
110
+ assert_is_app_dir(app_dir)
111
+
112
+ gen_dir = app_dir / 'generators/api'
113
+ assert_directory_exists(gen_dir)
114
+
115
+ api_dir = app_dir / "base/api/#{api_name}"
116
+ assert_directory_does_not_exists(api_dir)
117
+
118
+ Generator::API.new(gen_dir, api_dir, api_name)
119
+ else
120
+ unknown_instruction_error!(instruction, 'api (or a)')
121
+ end
122
+ end
123
+ end
124
+
125
+
126
+ begin # Docker
127
+ def docker args
128
+ opted_dir = args[2]
129
+ assert_directory_provided(opted_dir)
130
+ dir = expanded_path(opted_dir)
131
+
132
+ case instruction = args[1]
133
+ when 'i', 'install'
134
+
135
+ docker_install(dir, working_dir_opted: working_dir_opted?(opted_dir))
136
+
137
+ when 'b', 'build'
138
+
139
+ docker_build(dir, {
140
+ update_runner_only: args.find {|a| a == '-u'},
141
+ push_opted: args.find {|a| a == '-p'}
142
+ })
143
+
144
+ when 'u', 'update'
145
+
146
+ docker_update(dir)
147
+
148
+ else
149
+ unknown_instruction_error!(instruction, 'install (or i)', 'build (or b)', 'update (or u)')
150
+ end
151
+ end
152
+
153
+ def docker_install dir, opts
154
+ create_dirname_for(dir)
155
+ assert_installable_dir(dir, opts[:working_dir_opted])
156
+ Docker::Install.new(dir, opts)
157
+ end
158
+
159
+ def docker_build dir, opts
160
+ assert_directory_exists(dir)
161
+ assert_config_file_exists(dir)
162
+ Docker::Build.new(dir, opts)
163
+ end
164
+
165
+ def docker_update dir
166
+ assert_is_docker_dir(dir)
167
+ Docker::Update.new(dir)
168
+ end
169
+ end
170
+
171
+
172
+ def unknown_instruction_error! instruction, *available_instructions
173
+ fatal_error! "Unknown instruction #{instruction}. Use one of #{available_instructions*', '}"
174
+ end
175
+
176
+
177
+ def self.run cmd
178
+ puts "", "$ #{cmd}"
179
+ PTY.spawn cmd do |r, w, pid|
180
+ begin
181
+ r.sync
182
+ r.each_char do |char|
183
+ print(char)
184
+ end
185
+ rescue Errno::EIO => e
186
+ # simply ignoring this
187
+ ensure
188
+ Process.wait(pid)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ require 'appril/cli/app'
196
+ require 'appril/cli/generator'
197
+ require 'appril/cli/docker'
@@ -0,0 +1,103 @@
1
+ module Appril
2
+ class RTCPController < BaseController
3
+ attr_reader :socket
4
+
5
+ def get
6
+ return unless websocket?
7
+
8
+ @router = RocketIO::Router.new(*RocketIO.controllers)
9
+
10
+ @socket = Tubesock.hijack(env)
11
+ @socket.onopen(&method(:on_open))
12
+ @socket.onmessage(&method(:on_message))
13
+ @socket.onclose(&method(:on_close))
14
+ @socket.listen
15
+
16
+ halt websocket_response
17
+ end
18
+
19
+ private
20
+
21
+ def on_open
22
+ connected
23
+ write(serial: 0, data: __initialization_data__.update(initialization_data))
24
+ end
25
+
26
+ def __initialization_data__
27
+ {
28
+ client_url: Cfg.client_url
29
+ }
30
+ end
31
+
32
+ def on_message msg
33
+
34
+ msg = indifferent_params(JSON.parse(msg))
35
+ if controller = resolve_controller(msg[:controller])
36
+ status, _, body = call_controller(controller, msg[:method], msg[:arguments], msg[:serial])
37
+ if body.is_a?(Proc)
38
+ body.call
39
+ return
40
+ end
41
+ if status == 200
42
+ write(serial: msg[:serial], data: body) if msg[:reply]
43
+ return
44
+ end
45
+ else
46
+ body = '404: Not Found'
47
+ end
48
+
49
+ error = if body.is_a?(Array) && body.size == 1
50
+ body[0]
51
+ else
52
+ body
53
+ end
54
+
55
+ if msg && msg[:serial]
56
+ write(serial: msg[:serial], error: error)
57
+ else
58
+ write(error: error)
59
+ end
60
+
61
+ rescue Errno::EPIPE => e
62
+ close
63
+ rescue Exception => e
64
+ __error__(500, e)
65
+ end
66
+
67
+ def on_close
68
+ @socket.close! if @socket
69
+ @socket, @user, @router = nil
70
+ disconnected
71
+ end
72
+
73
+ def resolve_controller url
74
+ @router.resolve_path(url)[0]
75
+ end
76
+
77
+ def call_controller controller, method, arguments, serial
78
+ controller.initialize_controller(method, arguments).call(env.merge(rtcp_serial: serial).update(rtcp_env))
79
+ end
80
+
81
+ def write data
82
+ @socket.send_data(data.to_json)
83
+ end
84
+
85
+ # called after socket connection established
86
+ def connected
87
+ end
88
+
89
+ # data sent to client after connection established
90
+ def initialization_data
91
+ {}
92
+ end
93
+
94
+ # merged into original env when calling a controller
95
+ def rtcp_env
96
+ {}
97
+ end
98
+
99
+ # called when socket connection closed
100
+ def disconnected
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,3 @@
1
+ module Appril
2
+ VERSION = '0.0.0'.freeze
3
+ end
data/lib/appril.rb ADDED
@@ -0,0 +1,124 @@
1
+ require 'rocketio'
2
+
3
+ module Appril
4
+ extend self
5
+
6
+ def load_config dir, env = RocketIO.environment
7
+
8
+ config = load_config_file("#{dir}/config.yml")
9
+ config.update(load_config_file("#{dir}/env/#{env}.yml"))
10
+ config[:environment] = env.to_s.freeze
11
+
12
+ Dir["#{dir}/**/*.yml"].each do |file|
13
+ next if File.dirname(file) == './env'
14
+
15
+ key = File.basename(file, '.yml')
16
+ next if key == 'config' || key == 'appril'
17
+
18
+ key_config = load_config_file(file)
19
+ key_config_keys = key_config.keys.map(&:to_s)
20
+
21
+ config[key] = if key_config_keys.include?(env.to_s)
22
+ # current environment found, use it
23
+ key_config[env]
24
+ else
25
+ if RocketIO::ENVIRONMENTS.keys.find {|k| key_config_keys.include?(k)}
26
+ # there are some environment(s), but no current one so set current environment to nil
27
+ nil
28
+ else
29
+ # there are no environments, so this config is available on any environment
30
+ key_config
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ def config.method_missing key
37
+ self[key]
38
+ end
39
+
40
+ config
41
+ end
42
+
43
+
44
+ def load_config_file file
45
+ RocketIO.indifferent_params(YAML.load(File.read(file)) || {})
46
+ end
47
+
48
+
49
+ def controllers_map dir
50
+ path_to_api = File.expand_path('base/api', dir)
51
+ RocketIO.controllers.each_with_object([]) do |controller,o|
52
+ next unless controller.dirname[path_to_api]
53
+
54
+ o << {
55
+ path: controller.dirname.sub(path_to_api, '').gsub(/\A\/|\/\Z/, ''),
56
+ url: controller.url,
57
+ url_pattern: url_pattern(controller),
58
+ name: controller.name.gsub('::', '__'),
59
+ api: controller.api
60
+ }
61
+ end.sort do |a,b|
62
+ b[:url].split('/').size <=> a[:url].split('/').size
63
+ end
64
+ end
65
+
66
+
67
+ def webpack_entries dir, controllers
68
+ entries = controllers.each_with_object({}) do |controller,o|
69
+
70
+ next unless entry = %w[
71
+ ./base/api/%s/client.js
72
+ ./base/api/%s/client.coffee
73
+ ].map {|p| p % controller[:path]}.find {|f| File.file?(File.expand_path(f, dir))}
74
+
75
+ o[controller[:path]] = File.join('./base/api', controller[:path], File.basename(entry))
76
+ end
77
+
78
+ if core = %w[
79
+ ./base/core.js
80
+ ./base/core.coffee
81
+ ].find {|f| File.file?(File.expand_path(f, dir))}
82
+ entries[:core] = core
83
+ end
84
+
85
+ entries
86
+ end
87
+
88
+
89
+ def generate_configs dir
90
+ config = load_config("#{dir}/config", :development)
91
+
92
+ controllers = controllers_map(dir)
93
+ webpack_entries = webpack_entries(dir, controllers)
94
+
95
+ File.open File.expand_path('config.json', dir), 'w' do |f|
96
+ f << JSON.pretty_generate({
97
+ controllers: controllers,
98
+ webpack: {
99
+ path: config[:client_path],
100
+ url: config[:client_url],
101
+ entries: webpack_entries
102
+ }
103
+ })
104
+ end
105
+ end
106
+
107
+
108
+ def url_pattern controller
109
+ controller.url *controller.instance_method(:get).parameters.each_with_object([]) {|param,o|
110
+ pattern = if param[0] == :rest
111
+ "*"
112
+ elsif param[0] == :req
113
+ ":#{param[1]}"
114
+ elsif param[0] == :opt
115
+ ":#{param[1]}?"
116
+ end
117
+ o << pattern if pattern
118
+ }
119
+ end
120
+
121
+ end
122
+
123
+ require 'appril/base_controller'
124
+ require 'appril/rtcp_controller'