apipie-rails 0.0.12 → 0.0.13

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.
@@ -3,129 +3,131 @@ require 'oauth'
3
3
  require 'json'
4
4
  require 'apipie/client/rest_client_oauth'
5
5
 
6
- module Apipie::Client
7
-
8
- class Base
9
- attr_reader :client, :config
10
-
11
- def initialize(config, options = { })
12
- @client = RestClient::Resource.new config[:base_url],
13
- :user => config[:username],
14
- :password => config[:password],
15
- :oauth => config[:oauth],
16
- :headers => { :content_type => 'application/json',
17
- :accept => 'application/json' }
18
- @config = config
19
- end
6
+ module Apipie
7
+ module Client
8
+
9
+ class Base
10
+ attr_reader :client, :config
11
+
12
+ def initialize(config, options = { })
13
+ @client = RestClient::Resource.new config[:base_url],
14
+ :user => config[:username],
15
+ :password => config[:password],
16
+ :oauth => config[:oauth],
17
+ :headers => { :content_type => 'application/json',
18
+ :accept => 'application/json' }
19
+ @config = config
20
+ end
20
21
 
21
- def call(method, path, params = { }, headers = { })
22
- headers ||= { }
22
+ def call(method, path, params = { }, headers = { })
23
+ headers ||= { }
23
24
 
24
- args = [method]
25
- if [:post, :put].include?(method)
26
- args << params.to_json
27
- else
28
- headers[:params] = params if params
29
- end
25
+ args = [method]
26
+ if [:post, :put].include?(method)
27
+ args << params.to_json
28
+ else
29
+ headers[:params] = params if params
30
+ end
30
31
 
31
- args << headers if headers
32
- process_data client[path].send(*args)
33
- end
32
+ args << headers if headers
33
+ process_data client[path].send(*args)
34
+ end
34
35
 
35
- def self.doc
36
- raise NotImplementedError
37
- end
36
+ def self.doc
37
+ raise NotImplementedError
38
+ end
38
39
 
39
- def self.validation_hash(method)
40
- validation_hashes[method.to_s]
41
- end
40
+ def self.validation_hash(method)
41
+ validation_hashes[method.to_s]
42
+ end
42
43
 
43
- def self.method_doc(method)
44
- method_docs[method.to_s]
45
- end
44
+ def self.method_doc(method)
45
+ method_docs[method.to_s]
46
+ end
46
47
 
47
- def validate_params!(params, rules)
48
- return unless params.is_a?(Hash)
48
+ def validate_params!(params, rules)
49
+ return unless params.is_a?(Hash)
49
50
 
50
- invalid_keys = params.keys.map(&:to_s) - (rules.is_a?(Hash) ? rules.keys : rules)
51
- raise ArgumentError, "Invalid keys: #{invalid_keys.join(", ")}" unless invalid_keys.empty?
51
+ invalid_keys = params.keys.map(&:to_s) - (rules.is_a?(Hash) ? rules.keys : rules)
52
+ raise ArgumentError, "Invalid keys: #{invalid_keys.join(", ")}" unless invalid_keys.empty?
52
53
 
53
- if rules.is_a? Hash
54
- rules.each do |key, sub_keys|
55
- validate_params!(params[key], sub_keys) if params[key]
54
+ if rules.is_a? Hash
55
+ rules.each do |key, sub_keys|
56
+ validate_params!(params[key], sub_keys) if params[key]
57
+ end
56
58
  end
57
59
  end
58
- end
59
60
 
60
- protected
61
+ protected
61
62
 
62
- def process_data(response)
63
- data = begin
64
- JSON.parse(response.body)
65
- rescue JSON::ParserError
66
- response.body
63
+ def process_data(response)
64
+ data = begin
65
+ JSON.parse(response.body)
66
+ rescue JSON::ParserError
67
+ response.body
68
+ end
69
+ return data, response
67
70
  end
68
- return data, response
69
- end
70
-
71
- def check_params(params, options = { })
72
- raise ArgumentError unless (method = options[:method])
73
- return unless config[:enable_validations]
74
71
 
75
- case options[:allowed]
76
- when true
77
- validate_params!(params, self.class.validation_hash(method))
78
- when false
79
- raise ArgumentError, "this method '#{method}' does not support params" if params && !params.empty?
80
- else
81
- raise ArgumentError, "options :allowed should be true or false, it was #{options[:allowed]}"
72
+ def check_params(params, options = { })
73
+ raise ArgumentError unless (method = options[:method])
74
+ return unless config[:enable_validations]
75
+
76
+ case options[:allowed]
77
+ when true
78
+ validate_params!(params, self.class.validation_hash(method))
79
+ when false
80
+ raise ArgumentError, "this method '#{method}' does not support params" if params && !params.empty?
81
+ else
82
+ raise ArgumentError, "options :allowed should be true or false, it was #{options[:allowed]}"
83
+ end
82
84
  end
83
- end
84
85
 
85
- # @return url and rest of the params
86
- def fill_params_in_url(url, params)
87
- params ||= { }
88
- # insert param values
89
- url_param_names = params_in_path(url)
90
- url = params_in_path(url).inject(url) do |url, param_name|
91
- param_value = params[param_name] or
92
- raise ArgumentError, "missing param '#{param_name}' in parameters"
93
- url.sub(":#{param_name}", param_value.to_s)
94
- end
86
+ # @return url and rest of the params
87
+ def fill_params_in_url(url, params)
88
+ params ||= { }
89
+ # insert param values
90
+ url_param_names = params_in_path(url)
91
+ url = params_in_path(url).inject(url) do |url, param_name|
92
+ param_value = params[param_name] or
93
+ raise ArgumentError, "missing param '#{param_name}' in parameters"
94
+ url.sub(":#{param_name}", param_value.to_s)
95
+ end
95
96
 
96
- return url, params.reject { |param_name, _| url_param_names.include? param_name }
97
- end
97
+ return url, params.reject { |param_name, _| url_param_names.include? param_name }
98
+ end
98
99
 
99
- private
100
+ private
100
101
 
101
- def self.method_docs
102
- @method_docs ||= doc['methods'].inject({ }) do |hash, method|
103
- hash[method['name']] = method
104
- hash
102
+ def self.method_docs
103
+ @method_docs ||= doc['methods'].inject({ }) do |hash, method|
104
+ hash[method['name']] = method
105
+ hash
106
+ end
105
107
  end
106
- end
107
108
 
108
- def self.validation_hashes
109
- @validation_hashes ||= method_docs.inject({ }) do |hash, pair|
110
- name, method_doc = pair
111
- hash[name] = construct_validation_hash method_doc
112
- hash
109
+ def self.validation_hashes
110
+ @validation_hashes ||= method_docs.inject({ }) do |hash, pair|
111
+ name, method_doc = pair
112
+ hash[name] = construct_validation_hash method_doc
113
+ hash
114
+ end
113
115
  end
114
- end
115
116
 
116
- def self.construct_validation_hash(method)
117
- if method['params'].any? { |p| p['params'] }
118
- method['params'].reduce({ }) do |h, p|
119
- h.update(p['name'] => (p['params'] ? p['params'].map { |pp| pp['name'] } : nil))
117
+ def self.construct_validation_hash(method)
118
+ if method['params'].any? { |p| p['params'] }
119
+ method['params'].reduce({ }) do |h, p|
120
+ h.update(p['name'] => (p['params'] ? p['params'].map { |pp| pp['name'] } : nil))
121
+ end
122
+ else
123
+ method['params'].map { |p| p['name'] }
120
124
  end
121
- else
122
- method['params'].map { |p| p['name'] }
123
125
  end
124
- end
125
126
 
126
- def params_in_path(url)
127
- url.scan(/:([^\/]*)/).map { |m| m.first }
128
- end
127
+ def params_in_path(url)
128
+ url.scan(/:([^\/]*)/).map { |m| m.first }
129
+ end
129
130
 
131
+ end
130
132
  end
131
133
  end
@@ -1,127 +1,129 @@
1
- module Apipie::Client
2
- class CliCommand < Thor
3
- no_tasks do
4
- def client
5
- resource_class = apipie_options[:client_module]::Resources.const_get(self.class.name[/[^:]*$/])
6
- @client ||= resource_class.new(apipie_options[:config])
7
- end
1
+ module Apipie
2
+ module Client
3
+ class CliCommand < Thor
4
+ no_tasks do
5
+ def client
6
+ resource_class = apipie_options[:client_module]::Resources.const_get(self.class.name[/[^:]*$/])
7
+ @client ||= resource_class.new(apipie_options[:config])
8
+ end
9
+
10
+ def transform_options(inline_params, transform_hash = { })
11
+ # we use not mentioned params without change
12
+ transformed_options = (options.keys - transform_hash.values.flatten - inline_params).reduce({ }) { |h, k| h.update(k => options[k]) }
8
13
 
9
- def transform_options(inline_params, transform_hash = { })
10
- # we use not mentioned params without change
11
- transformed_options = (options.keys - transform_hash.values.flatten - inline_params).reduce({ }) { |h, k| h.update(k => options[k]) }
14
+ inline_params.each { |p| transformed_options[p] = options[p] }
12
15
 
13
- inline_params.each { |p| transformed_options[p] = options[p] }
16
+ transform_hash.each do |sub_key, params|
17
+ transformed_options[sub_key] = { }
18
+ params.each { |p| transformed_options[sub_key][p] = options[p] if options.has_key?(p) }
19
+ end
14
20
 
15
- transform_hash.each do |sub_key, params|
16
- transformed_options[sub_key] = { }
17
- params.each { |p| transformed_options[sub_key][p] = options[p] if options.has_key?(p) }
21
+ return transformed_options
18
22
  end
19
23
 
20
- return transformed_options
21
- end
24
+ def print_data(data)
25
+ case data
26
+ when Array
27
+ print_big_table(table_from_array(data))
28
+ when Hash
29
+ print_table(table_from_hash(data))
30
+ else
31
+ print_unknown(data)
32
+ end
33
+ end
22
34
 
23
- def print_data(data)
24
- case data
25
- when Array
26
- print_big_table(table_from_array(data))
27
- when Hash
28
- print_table(table_from_hash(data))
35
+ # unifies the data for further processing. e.g.
36
+ #
37
+ # { "user" => {"username" => "test", "password" => "changeme" }
38
+ #
39
+ # becomes:
40
+ #
41
+ # { "username" => "test", "password" => "changeme" }
42
+ def normalize_item_data(item)
43
+ if item.size == 1 && item.values.first.is_a?(Hash)
44
+ item.values.first
29
45
  else
30
- print_unknown(data)
46
+ item
47
+ end
31
48
  end
32
- end
33
49
 
34
- # unifies the data for further processing. e.g.
35
- #
36
- # { "user" => {"username" => "test", "password" => "changeme" }
37
- #
38
- # becomes:
39
- #
40
- # { "username" => "test", "password" => "changeme" }
41
- def normalize_item_data(item)
42
- if item.size == 1 && item.values.first.is_a?(Hash)
43
- item.values.first
44
- else
45
- item
50
+ def table_from_array(data)
51
+ return [] if data.empty?
52
+ table = []
53
+ items = data.map { |item| normalize_item_data(item) }
54
+ columns = items.first.keys
55
+ table << columns
56
+ items.each do |item|
57
+ row = columns.map { |c| item[c] }
58
+ table << row.map(&:to_s)
59
+ end
60
+ return table
46
61
  end
47
- end
48
62
 
49
- def table_from_array(data)
50
- return [] if data.empty?
51
- table = []
52
- items = data.map { |item| normalize_item_data(item) }
53
- columns = items.first.keys
54
- table << columns
55
- items.each do |item|
56
- row = columns.map { |c| item[c] }
57
- table << row.map(&:to_s)
63
+ def table_from_hash(data)
64
+ return [] if data.empty?
65
+ table = []
66
+ normalize_item_data(data).each do |k, v|
67
+ table << ["#{k}:", v].map(&:to_s)
68
+ end
69
+ table
58
70
  end
59
- return table
60
- end
61
71
 
62
- def table_from_hash(data)
63
- return [] if data.empty?
64
- table = []
65
- normalize_item_data(data).each do |k, v|
66
- table << ["#{k}:", v].map(&:to_s)
72
+ def print_unknown(data)
73
+ say data
67
74
  end
68
- table
69
- end
70
75
 
71
- def print_unknown(data)
72
- say data
73
- end
76
+ def print_big_table(table, options={ })
77
+ return if table.empty?
74
78
 
75
- def print_big_table(table, options={ })
76
- return if table.empty?
79
+ formats, ident, colwidth = [], options[:ident].to_i, options[:colwidth]
80
+ options[:truncate] = terminal_width if options[:truncate] == true
77
81
 
78
- formats, ident, colwidth = [], options[:ident].to_i, options[:colwidth]
79
- options[:truncate] = terminal_width if options[:truncate] == true
82
+ formats << "%-#{colwidth + 2}s" if colwidth
83
+ start = colwidth ? 1 : 0
80
84
 
81
- formats << "%-#{colwidth + 2}s" if colwidth
82
- start = colwidth ? 1 : 0
85
+ start.upto(table.first.length - 2) do |i|
86
+ maxima ||= table.max { |a, b| a[i].size <=> b[i].size }[i].size
87
+ formats << "%-#{maxima + 2}s"
88
+ end
83
89
 
84
- start.upto(table.first.length - 2) do |i|
85
- maxima ||= table.max { |a, b| a[i].size <=> b[i].size }[i].size
86
- formats << "%-#{maxima + 2}s"
87
- end
90
+ formats << "%s"
91
+ formats[0] = formats[0].insert(0, " " * ident)
88
92
 
89
- formats << "%s"
90
- formats[0] = formats[0].insert(0, " " * ident)
93
+ header_printed = false
94
+ table.each do |row|
95
+ sentence = ""
91
96
 
92
- header_printed = false
93
- table.each do |row|
94
- sentence = ""
97
+ row.each_with_index do |column, i|
98
+ sentence << formats[i] % column.to_s
99
+ end
95
100
 
96
- row.each_with_index do |column, i|
97
- sentence << formats[i] % column.to_s
101
+ sentence = truncate(sentence, options[:truncate]) if options[:truncate]
102
+ $stdout.puts sentence
103
+ say(set_color("-" * sentence.size, :green)) unless header_printed
104
+ header_printed = true
98
105
  end
99
-
100
- sentence = truncate(sentence, options[:truncate]) if options[:truncate]
101
- $stdout.puts sentence
102
- say(set_color("-" * sentence.size, :green)) unless header_printed
103
- header_printed = true
104
106
  end
107
+
105
108
  end
106
109
 
107
- end
110
+ class << self
111
+ def help(shell, subcommand = true)
112
+ list = self.printable_tasks(true, subcommand)
113
+ Thor::Util.thor_classes_in(self).each do |klass|
114
+ list += printable_tasks(false)
115
+ end
116
+ list.sort! { |a, b| a[0] <=> b[0] }
108
117
 
109
- class << self
110
- def help(shell, subcommand = true)
111
- list = self.printable_tasks(true, subcommand)
112
- Thor::Util.thor_classes_in(self).each do |klass|
113
- list += printable_tasks(false)
118
+ shell.say
119
+ shell.print_table(list, :indent => 2, :truncate => true)
120
+ shell.say
121
+ Thor.send(:class_options_help, shell)
114
122
  end
115
- list.sort! { |a, b| a[0] <=> b[0] }
116
123
 
117
- shell.say
118
- shell.print_table(list, :indent => 2, :truncate => true)
119
- shell.say
120
- Thor.send(:class_options_help, shell)
121
- end
122
-
123
- def banner(task, namespace = nil, subcommand = false)
124
- task.name
124
+ def banner(task, namespace = nil, subcommand = false)
125
+ task.name
126
+ end
125
127
  end
126
128
  end
127
129
  end
@@ -1,97 +1,99 @@
1
1
  require "apipie/client/thor"
2
2
  require "apipie/client/cli_command"
3
3
 
4
- module Apipie::Client
5
- class Main < Thor
6
-
7
- def help(meth = nil)
8
- if meth && !self.respond_to?(meth)
9
- initialize_thorfiles(meth)
10
- klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
11
- self.class.handle_no_task_error(task, false) if klass.nil?
12
- klass.start(["-h", task].compact, :shell => self.shell)
13
- else
14
- say "#{apipie_options[:name].capitalize} CLI"
15
- say
16
- invoke :commands
4
+ module Apipie
5
+ module Client
6
+ class Main < Thor
7
+
8
+ def help(meth = nil)
9
+ if meth && !self.respond_to?(meth)
10
+ initialize_thorfiles(meth)
11
+ klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
12
+ self.class.handle_no_task_error(task, false) if klass.nil?
13
+ klass.start(["-h", task].compact, :shell => self.shell)
14
+ else
15
+ say "#{apipie_options[:name].capitalize} CLI"
16
+ say
17
+ invoke :commands
18
+ end
17
19
  end
18
- end
19
20
 
20
- desc "commands [SEARCH]", "List the available commands"
21
- def commands(search="")
22
- initialize_thorfiles
23
- klasses = Thor::Base.subclasses
24
- display_klasses(false, false, klasses)
25
- end
21
+ desc "commands [SEARCH]", "List the available commands"
22
+ def commands(search="")
23
+ initialize_thorfiles
24
+ klasses = Thor::Base.subclasses
25
+ display_klasses(false, false, klasses)
26
+ end
26
27
 
27
- class << self
28
- private
29
- def dispatch(task, given_args, given_options, config)
30
- parser = Thor::Options.new :auth => Thor::Option.parse(%w[auth -a], :string)
31
- opts = parser.parse(given_args)
32
- if opts['auth']
33
- username, password = opts['auth'].split(':')
34
- apipie_options[:config][:username] = username
35
- apipie_options[:config][:password] = password
28
+ class << self
29
+ private
30
+ def dispatch(task, given_args, given_options, config)
31
+ parser = Thor::Options.new :auth => Thor::Option.parse(%w[auth -a], :string)
32
+ opts = parser.parse(given_args)
33
+ if opts['auth']
34
+ username, password = opts['auth'].split(':')
35
+ apipie_options[:config][:username] = username
36
+ apipie_options[:config][:password] = password
37
+ end
38
+ remaining = parser.remaining
39
+
40
+ super(task, remaining, given_options, config)
36
41
  end
37
- remaining = parser.remaining
38
-
39
- super(task, remaining, given_options, config)
40
42
  end
41
- end
42
43
 
43
- private
44
+ private
44
45
 
45
- def method_missing(meth, *args)
46
- meth = meth.to_s
47
- initialize_thorfiles(meth)
48
- klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
49
- args.unshift(task) if task
50
- klass.start(args, :shell => self.shell)
51
- end
46
+ def method_missing(meth, *args)
47
+ meth = meth.to_s
48
+ initialize_thorfiles(meth)
49
+ klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
50
+ args.unshift(task) if task
51
+ klass.start(args, :shell => self.shell)
52
+ end
52
53
 
53
- # Load the thorfiles. If relevant_to is supplied, looks for specific files
54
- # in the thor_root instead of loading them all.
55
- #
56
- # By default, it also traverses the current path until find Thor files, as
57
- # described in thorfiles. This look up can be skipped by suppliying
58
- # skip_lookup true.
59
- #
60
- def initialize_thorfiles(relevant_to=nil, skip_lookup=false)
61
- thorfiles.each do |f|
62
- Thor::Util.load_thorfile(f, nil, options[:debug])
54
+ # Load the thorfiles. If relevant_to is supplied, looks for specific files
55
+ # in the thor_root instead of loading them all.
56
+ #
57
+ # By default, it also traverses the current path until find Thor files, as
58
+ # described in thorfiles. This look up can be skipped by suppliying
59
+ # skip_lookup true.
60
+ #
61
+ def initialize_thorfiles(relevant_to=nil, skip_lookup=false)
62
+ thorfiles.each do |f|
63
+ Thor::Util.load_thorfile(f, nil, options[:debug])
64
+ end
63
65
  end
64
- end
65
66
 
66
- def thorfiles
67
- Dir[File.expand_path("*/commands/*.thor", apipie_options[:root])]
68
- end
67
+ def thorfiles
68
+ Dir[File.expand_path("*/commands/*.thor", apipie_options[:root])]
69
+ end
69
70
 
70
- # Display information about the given klasses. If with_module is given,
71
- # it shows a table with information extracted from the yaml file.
72
- #
73
- def display_klasses(with_modules=false, show_internal=false, klasses=Thor::Base.subclasses)
74
- klasses -= [Thor, Main, ::Apipie::Client::CliCommand, ::Thor] unless show_internal
71
+ # Display information about the given klasses. If with_module is given,
72
+ # it shows a table with information extracted from the yaml file.
73
+ #
74
+ def display_klasses(with_modules=false, show_internal=false, klasses=Thor::Base.subclasses)
75
+ klasses -= [Thor, Main, ::Apipie::Client::CliCommand, ::Thor] unless show_internal
75
76
 
76
- show_modules if with_modules && !thor_yaml.empty?
77
+ show_modules if with_modules && !thor_yaml.empty?
77
78
 
78
- list = Hash.new { |h, k| h[k] = [] }
79
- groups = []
79
+ list = Hash.new { |h, k| h[k] = [] }
80
+ groups = []
80
81
 
81
- # Get classes which inherit from Thor
82
- (klasses - groups).each { |k| list[k.namespace.split(":").first] += k.printable_tasks(false) }
82
+ # Get classes which inherit from Thor
83
+ (klasses - groups).each { |k| list[k.namespace.split(":").first] += k.printable_tasks(false) }
83
84
 
84
- # Get classes which inherit from Thor::Base
85
- groups.map! { |k| k.printable_tasks(false).first }
86
- list["root"] = groups
85
+ # Get classes which inherit from Thor::Base
86
+ groups.map! { |k| k.printable_tasks(false).first }
87
+ list["root"] = groups
87
88
 
88
- # Order namespaces with default coming first
89
- list = list.sort { |a, b| a[0].sub(/^default/, '') <=> b[0].sub(/^default/, '') }
90
- list.each { |n, tasks| display_tasks(n, tasks) unless tasks.empty? }
91
- end
89
+ # Order namespaces with default coming first
90
+ list = list.sort { |a, b| a[0].sub(/^default/, '') <=> b[0].sub(/^default/, '') }
91
+ list.each { |n, tasks| display_tasks(n, tasks) unless tasks.empty? }
92
+ end
92
93
 
93
- def display_tasks(namespace, list) #:nodoc:
94
- say namespace
94
+ def display_tasks(namespace, list) #:nodoc:
95
+ say namespace
96
+ end
95
97
  end
96
98
  end
97
99
 
@@ -1,19 +1,21 @@
1
- module Apipie::Client
2
- class Thor < ::Thor
1
+ module Apipie
2
+ module Client
3
+ class Thor < ::Thor
3
4
 
4
- def self.apipie_options
5
- Apipie::Client::Thor.instance_variable_get :@apipie_options
6
- end
5
+ def self.apipie_options
6
+ Apipie::Client::Thor.instance_variable_get :@apipie_options
7
+ end
7
8
 
8
- no_tasks do
9
- def apipie_options
10
- self.class.apipie_options
9
+ no_tasks do
10
+ def apipie_options
11
+ self.class.apipie_options
12
+ end
11
13
  end
12
- end
13
14
 
14
- def self.apipie_options=(options)
15
- Apipie::Client::Thor.instance_variable_set :@apipie_options, options
16
- end
15
+ def self.apipie_options=(options)
16
+ Apipie::Client::Thor.instance_variable_set :@apipie_options, options
17
+ end
17
18
 
19
+ end
18
20
  end
19
21
  end
@@ -1,3 +1,3 @@
1
1
  module Apipie
2
- VERSION = '0.0.12'
2
+ VERSION = '0.0.13'
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apipie-rails
3
3
  version: !ruby/object:Gem::Version
4
- hash: 7
4
+ hash: 5
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 12
10
- version: 0.0.12
9
+ - 13
10
+ version: 0.0.13
11
11
  platform: ruby
12
12
  authors:
13
13
  - Pavel Pokorny
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2012-10-09 00:00:00 Z
19
+ date: 2012-10-25 00:00:00 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
22
  name: rspec-rails