nexmo-oas-renderer 0.11.2 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +135 -0
  3. data/.travis.yml +1 -0
  4. data/CHANGELOG.md +9 -0
  5. data/Gemfile +2 -2
  6. data/Gemfile.lock +91 -71
  7. data/Rakefile +2 -2
  8. data/bin/console +3 -3
  9. data/lib/nexmo/oas/engine.rb +2 -0
  10. data/lib/nexmo/oas/renderer.rb +5 -5
  11. data/lib/nexmo/oas/renderer/app.rb +71 -46
  12. data/lib/nexmo/oas/renderer/config.ru +4 -2
  13. data/lib/nexmo/oas/renderer/decorators/response_parser_decorator.rb +18 -18
  14. data/lib/nexmo/oas/renderer/helpers/navigation.rb +2 -2
  15. data/lib/nexmo/oas/renderer/helpers/render.rb +2 -1
  16. data/lib/nexmo/oas/renderer/helpers/summary.rb +4 -1
  17. data/lib/nexmo/oas/renderer/helpers/url.rb +2 -0
  18. data/lib/nexmo/oas/renderer/presenters/api_specification.rb +12 -3
  19. data/lib/nexmo/oas/renderer/presenters/endpoint.rb +2 -0
  20. data/lib/nexmo/oas/renderer/presenters/groups.rb +4 -0
  21. data/lib/nexmo/oas/renderer/presenters/navigation.rb +2 -0
  22. data/lib/nexmo/oas/renderer/presenters/open_api_specification.rb +5 -2
  23. data/lib/nexmo/oas/renderer/presenters/request_body_raw.rb +141 -0
  24. data/lib/nexmo/oas/renderer/presenters/response_format.rb +4 -2
  25. data/lib/nexmo/oas/renderer/presenters/response_tab/link.rb +3 -0
  26. data/lib/nexmo/oas/renderer/presenters/response_tab/panel.rb +8 -5
  27. data/lib/nexmo/oas/renderer/presenters/response_tabs.rb +6 -2
  28. data/lib/nexmo/oas/renderer/presenters/versions.rb +11 -10
  29. data/lib/nexmo/oas/renderer/public/assets/javascripts/components/format.js +12 -7
  30. data/lib/nexmo/oas/renderer/public/assets/javascripts/nexmo-oas-renderer.js +61 -27
  31. data/lib/nexmo/oas/renderer/public/assets/javascripts/popper.min.js +5 -0
  32. data/lib/nexmo/oas/renderer/public/assets/javascripts/tooltip.min.js +5 -0
  33. data/lib/nexmo/oas/renderer/public/assets/javascripts/volta.accordion.js +301 -243
  34. data/lib/nexmo/oas/renderer/public/assets/javascripts/volta.tooltip.js +76 -0
  35. data/lib/nexmo/oas/renderer/public/assets/stylesheets/nexmo-oas-renderer.css +255 -823
  36. data/lib/nexmo/oas/renderer/public/assets/stylesheets/nexmo-oas-renderer.css.map +2 -2
  37. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/api.scss +287 -90
  38. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/style.scss +2 -6
  39. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/themes/dark.scss +89 -0
  40. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/themes/light.scss +68 -0
  41. data/lib/nexmo/oas/renderer/public/assets/stylesheets/volta-prism.min.css +1 -1
  42. data/lib/nexmo/oas/renderer/public/assets/stylesheets/volta.min.css +1 -1
  43. data/lib/nexmo/oas/renderer/services/oas_parser.rb +2 -0
  44. data/lib/nexmo/oas/renderer/services/open_api_definition_resolver.rb +3 -1
  45. data/lib/nexmo/oas/renderer/version.rb +3 -1
  46. data/lib/nexmo/oas/renderer/views/layouts/_head.erb +1 -0
  47. data/lib/nexmo/oas/renderer/views/layouts/_javascripts.erb +5 -4
  48. data/lib/nexmo/oas/renderer/views/layouts/open_api.erb +3 -1
  49. data/lib/nexmo/oas/renderer/views/open_api/_auth.erb +74 -0
  50. data/lib/nexmo/oas/renderer/views/open_api/_available_endpoints.erb +25 -0
  51. data/lib/nexmo/oas/renderer/views/open_api/_callback_endpoint.erb +18 -27
  52. data/lib/nexmo/oas/renderer/views/open_api/_callbacks.erb +5 -0
  53. data/lib/nexmo/oas/renderer/views/open_api/_endpoint.erb +39 -124
  54. data/lib/nexmo/oas/renderer/views/open_api/_header.erb +71 -0
  55. data/lib/nexmo/oas/renderer/views/open_api/_model.erb +31 -26
  56. data/lib/nexmo/oas/renderer/views/open_api/_navigation.erb +54 -78
  57. data/lib/nexmo/oas/renderer/views/open_api/_parameter_groups.erb +2 -5
  58. data/lib/nexmo/oas/renderer/views/open_api/_parameters.erb +98 -169
  59. data/lib/nexmo/oas/renderer/views/open_api/_request_json.erb +4 -0
  60. data/lib/nexmo/oas/renderer/views/open_api/_request_one_of.erb +70 -0
  61. data/lib/nexmo/oas/renderer/views/open_api/_request_single.erb +53 -0
  62. data/lib/nexmo/oas/renderer/views/open_api/_requests.erb +22 -0
  63. data/lib/nexmo/oas/renderer/views/open_api/_response_description_parameters.erb +88 -90
  64. data/lib/nexmo/oas/renderer/views/open_api/_response_descriptions.erb +32 -12
  65. data/lib/nexmo/oas/renderer/views/open_api/_response_fields.erb +1 -16
  66. data/lib/nexmo/oas/renderer/views/open_api/_response_tabs.erb +2 -2
  67. data/lib/nexmo/oas/renderer/views/open_api/_responses.erb +51 -0
  68. data/lib/nexmo/oas/renderer/views/open_api/_tabbed_parameters.erb +15 -4
  69. data/lib/nexmo/oas/renderer/views/open_api/_tabbed_single_parameter.erb +56 -0
  70. data/lib/nexmo/oas/renderer/views/open_api/_webhooks.erb +30 -0
  71. data/lib/nexmo/oas/renderer/views/open_api/show.erb +10 -115
  72. data/nexmo-oas-renderer.gemspec +26 -26
  73. metadata +59 -48
  74. data/lib/nexmo/oas/renderer/public/assets/javascripts/components/scroll.js +0 -21
  75. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/core.scss +0 -137
  76. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/navigation.scss +0 -102
  77. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/nexmo.scss +0 -61
  78. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/syntax.scss +0 -63
  79. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/typography.scss +0 -248
  80. data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/volta-templates.scss +0 -119
data/Rakefile CHANGED
@@ -1,2 +1,2 @@
1
- require "bundler/gem_tasks"
2
- task :default => :spec
1
+ require 'bundler/gem_tasks'
2
+ task default: :spec
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "nexmo/oas/renderer"
3
+ require 'bundler/setup'
4
+ require 'nexmo/oas/renderer'
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +10,5 @@ require "nexmo/oas/renderer"
10
10
  # require "pry"
11
11
  # Pry.start
12
12
 
13
- require "irb"
13
+ require 'irb'
14
14
  IRB.start(__FILE__)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Nexmo
2
4
  module OAS
3
5
  module Renderer
@@ -1,9 +1,9 @@
1
- require "nexmo/oas/renderer/version"
2
- require "nexmo/oas/renderer/app"
1
+ # frozen_string_literal: true
3
2
 
4
- if defined?(NexmoDeveloper::Application)
5
- require "nexmo/oas/engine"
6
- end
3
+ require 'nexmo/oas/renderer/version'
4
+ require 'nexmo/oas/renderer/app'
5
+
6
+ require 'nexmo/oas/engine' if defined?(NexmoDeveloper::Application)
7
7
 
8
8
  module Nexmo
9
9
  module OAS
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sinatra/base'
2
4
  require 'active_support'
3
5
  require 'active_support/core_ext/array/conversions'
@@ -5,15 +7,16 @@ require 'active_support/core_ext/string/output_safety'
5
7
  require 'active_model'
6
8
  require 'nexmo_markdown_renderer'
7
9
 
8
- require_relative'./decorators/response_parser_decorator'
9
- require_relative'./presenters/api_specification'
10
- require_relative'./presenters/open_api_specification'
11
- require_relative'./presenters/navigation'
12
- require_relative'./presenters/response_tabs'
13
- require_relative'./helpers/render'
14
- require_relative'./helpers/navigation'
15
- require_relative'./helpers/summary'
16
- require_relative'./helpers/url'
10
+ require_relative './decorators/response_parser_decorator'
11
+ require_relative './presenters/api_specification'
12
+ require_relative './presenters/open_api_specification'
13
+ require_relative './presenters/navigation'
14
+ require_relative './presenters/request_body_raw'
15
+ require_relative './presenters/response_tabs'
16
+ require_relative './helpers/render'
17
+ require_relative './helpers/navigation'
18
+ require_relative './helpers/summary'
19
+ require_relative './helpers/url'
17
20
 
18
21
  require 'dotenv/load'
19
22
 
@@ -21,18 +24,17 @@ module Nexmo
21
24
  module OAS
22
25
  module Renderer
23
26
  class API < Sinatra::Base
24
-
25
27
  Tilt.register Tilt::ERBTemplate, 'html.erb'
26
28
 
27
29
  if defined?(NexmoDeveloper::Application)
28
- view_paths = [views, NexmoDeveloper::Application.root.join("app", "views")]
30
+ view_paths = [views, NexmoDeveloper::Application.root.join('app', 'views')]
29
31
  set :views, view_paths
30
32
  end
31
33
 
32
34
  set :mustermann_opts, { type: :rails }
33
35
  set :oas_path, (ENV['OAS_PATH'] || './')
34
36
  set :bind, '0.0.0.0'
35
- set :github_path, Proc.new { load_business_yaml }
37
+ set :github_path, (proc { load_business_yaml })
36
38
 
37
39
  helpers do
38
40
  include Helpers::Render
@@ -45,9 +47,9 @@ module Nexmo
45
47
  extensions = extension.split('.')
46
48
  case extensions.size
47
49
  when 1
48
- { definition: extensions.first}
50
+ { definition: extensions.first }
49
51
  when 2
50
- if extensions.second.match? /v\d+/
52
+ if extensions.second.match?(/v\d+/)
51
53
  { definition: extensions.first, version: extensions.second }
52
54
  else
53
55
  { definition: extensions.first, format: extensions.second }
@@ -60,29 +62,29 @@ module Nexmo
60
62
  end
61
63
 
62
64
  def self.load_business_yaml
63
- if defined?(NexmoDeveloper::Application) && !File.exist?("#{Rails.configuration.docs_base_path}/config/business_info.yml")
64
- raise "Application requires a 'config/business_info.yml' file to be defined inside the documentation path."
65
- elsif defined?(NexmoDeveloper::Application) && File.exist?("#{Rails.configuration.docs_base_path}/config/business_info.yml")
65
+ raise "Application requires a 'config/business_info.yml' file to be defined inside the documentation path." if defined?(NexmoDeveloper::Application) && !File.exist?("#{Rails.configuration.docs_base_path}/config/business_info.yml")
66
+
67
+ if defined?(NexmoDeveloper::Application) && File.exist?("#{Rails.configuration.docs_base_path}/config/business_info.yml")
66
68
  @url ||= begin
67
69
  config = YAML.load_file("#{Rails.configuration.docs_base_path}/config/business_info.yml")
68
- "https://www.github.com/#{config['oas_repo']}/blob/master/definitions"
70
+ config['oas_url']
69
71
  end
70
72
  else
71
- "https://www.github.com/nexmo/api-specification/blob/master/definitions"
73
+ 'https://www.github.com/nexmo/api-specification/blob/master/definitions'
72
74
  end
73
75
  end
74
76
 
75
77
  def check_redirect!
76
- if defined?(NexmoDeveloper::Application)
77
- redirect_path = Redirector.find(request)
78
- redirect(redirect_path) if redirect_path
79
- end
78
+ return unless defined?(NexmoDeveloper::Application)
79
+
80
+ redirect_path = Redirector.find(request)
81
+ redirect(redirect_path) if redirect_path
80
82
  end
81
83
 
82
84
  def check_oas_constraints!(definition)
83
- if defined?(NexmoDeveloper::Application)
84
- pass unless OpenApiConstraint.match?(definition)
85
- end
85
+ return unless defined?(NexmoDeveloper::Application)
86
+
87
+ pass unless OpenApiConstraint.match?(definition)
86
88
  end
87
89
 
88
90
  error Errno::ENOENT do
@@ -96,21 +98,44 @@ module Nexmo
96
98
 
97
99
  unless defined?(NexmoDeveloper::Application)
98
100
  get '/' do
99
- prefix = "#{API.oas_path}"
101
+ prefix = API.oas_path.to_s
100
102
  @definitions = Dir.glob("#{prefix}/**/*.yml").map do |d|
101
103
  d.gsub("#{prefix}/", '').gsub('.yml', '')
102
- end.sort.reject { |d| d.include? 'common/' }
104
+ end
105
+
106
+ @definitions = @definitions.sort.reject { |d| d.include? 'common/' }
103
107
  erb :'api/index', layout: false
104
108
  end
105
109
  end
106
110
 
107
111
  def set_code_language
108
112
  return if params[:code_language] == 'templates'
113
+
109
114
  @code_language = params[:code_language]
110
115
  end
111
116
 
117
+ def set_theme
118
+ persisted_theme = nil
119
+
120
+ if defined?(NexmoDeveloper::Application)
121
+ session[:persisted_theme] = params[:theme] if params[:theme]
122
+ persisted_theme = session[:persisted_theme]
123
+ end
124
+
125
+ @theme = params[:theme] || persisted_theme
126
+
127
+ @theme = 'light' unless %w[light dark].include?(@theme)
128
+
129
+ @theme_light = @theme == 'light'
130
+
131
+ alternate_theme = @theme == 'light' ? 'dark' : 'light'
132
+ @theme_link = "#{request.path_info}?theme=#{alternate_theme}"
133
+ @theme_link = "/api#{@theme_link}" if defined?(NexmoDeveloper::Application)
134
+ end
135
+
112
136
  before do
113
137
  set_code_language
138
+ set_theme
114
139
  end
115
140
 
116
141
  get '(/api)/*definition' do
@@ -122,30 +147,30 @@ module Nexmo
122
147
 
123
148
  @specification = Presenters::OpenApiSpecification.new(
124
149
  definition_name: definition,
125
- expand_responses: params.fetch(:expandResponses, nil),
150
+ expand_responses: params.fetch(:expandResponses, nil)
126
151
  )
127
152
 
128
- if ['yml', 'json'].include?(parameters[:format])
129
- send_file @specification.definition.path, disposition: :attachment
153
+ if %w[yml json].include?(parameters[:format])
154
+ next send_file @specification.definition.path, disposition: :attachment
155
+ end
156
+
157
+ if defined?(NexmoDeveloper::Application)
158
+ erb :'open_api/show', layout: :'layouts/open-api.html'
130
159
  else
131
- if defined?(NexmoDeveloper::Application)
132
- erb :'open_api/show', layout: :'layouts/page-full.html'
133
- else
134
- erb :'open_api/show', layout: :'layouts/open_api'
135
- end
160
+ erb :'open_api/show', layout: :'layouts/open_api'
136
161
  end
137
162
  end
138
163
 
139
164
  def set_document
140
- if params[:code_language] == 'templates'
141
- @document = 'verify/templates'
142
- elsif params[:code_language] == 'ncco'
143
- @document = 'voice/ncco'
144
- elsif ::Nexmo::Markdown::CodeLanguage.exists?(params[:code_language])
145
- @document = params[:document]
146
- else
147
- @document = "#{params[:document]}/#{params[:code_language]}"
148
- end
165
+ @document = if params[:code_language] == 'templates'
166
+ 'verify/templates'
167
+ elsif params[:code_language] == 'ncco'
168
+ 'voice/ncco'
169
+ elsif ::Nexmo::Markdown::CodeLanguage.exists?(params[:code_language])
170
+ params[:document]
171
+ else
172
+ "#{params[:document]}/#{params[:code_language]}"
173
+ end
149
174
  end
150
175
 
151
176
  get '(/api)/*document(/:code_language)' do
@@ -158,7 +183,7 @@ module Nexmo
158
183
 
159
184
  @navigation = Presenters::Navigation.new(
160
185
  content: @specification.content,
161
- title: @specification.side_navigation_title,
186
+ title: @specification.side_navigation_title
162
187
  )
163
188
 
164
189
  if defined?(NexmoDeveloper::Application)
@@ -1,5 +1,7 @@
1
- require_relative "./app"
2
- require "sass/plugin/rack"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './app'
4
+ require 'sass/plugin/rack'
3
5
 
4
6
  Sass::Plugin.options[:style] = :compressed
5
7
  use Sass::Plugin::Rack
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rouge'
2
4
  require 'neatjson'
3
5
  require_relative '../services/oas_parser'
@@ -8,43 +10,41 @@ module Nexmo
8
10
  class ResponseParserDecorator < ::OasParser::ResponseParser
9
11
  def formatted_json
10
12
  JSON.neat_generate(parse, {
11
- wrap: true,
12
- after_colon: 1,
13
- })
13
+ wrap: true,
14
+ after_colon: 1,
15
+ })
14
16
  end
15
17
 
16
18
  def formatted_xml(xml_options = {})
17
19
  xml_options[:root] = xml_options['name'] if xml_options
18
20
  xml_string = xml(xml_options)
19
- xml_string.gsub!(%r{^(\s+?)(<(?:\w|\=|\"|\_|\s)+?\>)(.+?)(</.+?>)}).each do |s|
20
- indentation = $1
21
- indentation_plus_one = "#{$1} "
22
- opening_tag = $2
23
- content = $3
24
- closing_tag = $4
21
+ xml_string.gsub!(%r{^(\s+?)(<(?:\w|=|"|_|\s)+?>)(.+?)(</.+?>)}).each do |s|
22
+ indentation = Regexp.last_match(1)
23
+ indentation_plus_one = "#{Regexp.last_match(1)} "
24
+ opening_tag = Regexp.last_match(2)
25
+ content = Regexp.last_match(3)
26
+ closing_tag = Regexp.last_match(4)
25
27
 
26
28
  next(s) if (indentation.size + opening_tag.size + content.size) < 60
27
29
 
28
30
  next "#{indentation}#{opening_tag}\n#{indentation_plus_one}#{content}\n#{indentation}#{closing_tag}"
29
31
  end
30
32
 
31
- xml_string
33
+ xml_string.gsub('<', '&lt;')
32
34
  end
33
35
 
34
- def html(format = 'application/json', xml_options: nil)
35
- formatter = Rouge::Formatters::HTML.new
36
-
36
+ def html(format = 'application/json', xml_options: nil, theme_light: nil)
37
37
  case format
38
38
  when 'application/json'
39
- lexer = Rouge::Lexer.find('json')
40
- highlighted_response = formatter.format(lexer.lex(formatted_json))
39
+ language = 'json'
40
+ response = formatted_json
41
41
  when 'text/xml', 'application/xml'
42
- lexer = Rouge::Lexer.find('xml')
43
- highlighted_response = formatter.format(lexer.lex(formatted_xml(xml_options)))
42
+ language = 'xml'
43
+ response = formatted_xml(xml_options)
44
44
  end
45
45
 
46
46
  output = <<~HEREDOC
47
- <pre class="language-#{lexer && lexer.tag || 'json'} Vlt-prism--dark Vlt-prism--copy-disabled"><code>#{highlighted_response}</code></pre>
47
+ <pre class="pre-wrap language-#{language} #{theme_light ? 'Vlt-prism--dark' : ''} Vlt-prism--copy-disabled"><code>#{response}</code></pre>
48
48
  HEREDOC
49
49
 
50
50
  output
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Nexmo
2
4
  module OAS
3
5
  module Renderer
4
6
  module Helpers
5
7
  module Navigation
6
-
7
8
  HEADING_TAG_DEPTHS = {
8
9
  'h0' => 0,
9
10
  'h1' => 1,
@@ -58,7 +59,6 @@ module Nexmo
58
59
  def build_document(content)
59
60
  Nokogiri::HTML::DocumentFragment.parse(content)
60
61
  end
61
-
62
62
  end
63
63
  end
64
64
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Nexmo
2
4
  module OAS
3
5
  module Renderer
4
6
  module Helpers
5
7
  module Render
6
-
7
8
  def find_template(views, name, engine, &block)
8
9
  Array(views).each do |v|
9
10
  super(v, name, engine, &block)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Nexmo
2
4
  module OAS
3
5
  module Renderer
@@ -17,9 +19,10 @@ module Nexmo
17
19
  operation_id = operation_id.gsub(/(_|-)/, ' ').titleize
18
20
 
19
21
  # Some terms need to be capitalised all the time
20
- uppercase_array = ['SMS', 'DTMF']
22
+ uppercase_array = %w[SMS DTMF]
21
23
  operation_id.split(' ').map do |c|
22
24
  next c.upcase if uppercase_array.include?(c.upcase)
25
+
23
26
  c
24
27
  end.join(' ')
25
28
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Nexmo
2
4
  module OAS
3
5
  module Renderer
@@ -1,20 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Nexmo
2
4
  module OAS
3
5
  module Renderer
4
6
  module Presenters
5
7
  class ApiSpecification
6
-
7
8
  def initialize(document_name:, code_language: nil)
8
9
  @document_name = document_name
9
10
  @code_language = code_language
10
11
  end
11
12
 
12
13
  def side_navigation
13
- "api/#{@document_name}"
14
+ if defined?(NexmoDeveloper::Application)
15
+ "#{Rails.configuration.docs_base_path}/api/#{@document_name}"
16
+ else
17
+ "api/#{@document_name}"
18
+ end
14
19
  end
15
20
 
16
21
  def document_path
17
- "_api/#{@document_name}.md"
22
+ if defined?(NexmoDeveloper::Application)
23
+ "#{Rails.configuration.docs_base_path}/_api/#{@document_name}.md"
24
+ else
25
+ "_api/#{@document_name}.md"
26
+ end
18
27
  end
19
28
 
20
29
  def document
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './response_format'
2
4
 
3
5
  module Nexmo
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Nexmo
2
4
  module OAS
3
5
  module Renderer
@@ -12,6 +14,7 @@ module Nexmo
12
14
  # For now we only use the first tag in the list as an equivalent for the old x-group functionality
13
15
  @groups = @definition.endpoints.group_by do |endpoint|
14
16
  next nil unless tags
17
+
15
18
  endpoint.raw['tags']&.first
16
19
  end
17
20
 
@@ -25,6 +28,7 @@ module Nexmo
25
28
  # Sort by the order in which they're defined in the definition
26
29
  @groups = @groups.sort_by do |name, _|
27
30
  next -1 if name.nil?
31
+
28
32
  ordering[name.capitalize] || 999
29
33
  end
30
34
  end