appril 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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'