mission_control-web 0.0.1 → 0.2.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +122 -15
  4. data/Rakefile +21 -3
  5. data/app/assets/config/mission_control_web_manifest.js +2 -0
  6. data/app/assets/stylesheets/mission_control/web/application.css +15 -0
  7. data/app/assets/stylesheets/mission_control/web/vendor/bulma.min.css +1 -0
  8. data/app/controllers/concerns/mission_control/web/application_scoped.rb +17 -0
  9. data/app/controllers/mission_control/web/application_controller.rb +4 -0
  10. data/app/controllers/mission_control/web/errors_controller.rb +6 -0
  11. data/app/controllers/mission_control/web/routes_controller.rb +52 -0
  12. data/app/helpers/mission_control/web/applications_helper.rb +9 -0
  13. data/app/helpers/mission_control/web/routes_helper.rb +9 -0
  14. data/app/javascript/mission_control/web/application.js +1 -0
  15. data/app/models/concerns/mission_control/web/route/applications.rb +17 -0
  16. data/app/models/mission_control/web/application/routes.rb +24 -0
  17. data/app/models/mission_control/web/application.rb +34 -0
  18. data/app/models/mission_control/web/application_record.rb +3 -0
  19. data/app/models/mission_control/web/current.rb +8 -0
  20. data/app/models/mission_control/web/route.rb +14 -0
  21. data/app/views/layouts/mission_control/web/application.html.erb +23 -0
  22. data/app/views/mission_control/web/application/_current_application_selector.html.erb +15 -0
  23. data/app/views/mission_control/web/application/_flash.html.erb +3 -0
  24. data/app/views/mission_control/web/application/_form_errors.html.erb +11 -0
  25. data/app/views/mission_control/web/application/_navbar.html.erb +14 -0
  26. data/app/views/mission_control/web/errors/disallowed.html.erb +8 -0
  27. data/app/views/mission_control/web/routes/_form.html.erb +26 -0
  28. data/app/views/mission_control/web/routes/_route.html.erb +13 -0
  29. data/app/views/mission_control/web/routes/edit.html.erb +8 -0
  30. data/app/views/mission_control/web/routes/index.html.erb +27 -0
  31. data/app/views/mission_control/web/routes/new.html.erb +8 -0
  32. data/app/views/mission_control/web/routes/show.html.erb +42 -0
  33. data/config/importmap.rb +2 -0
  34. data/config/routes.rb +7 -0
  35. data/db/migrate/20221025093008_create_mission_control_web_routes.rb +14 -0
  36. data/lib/generators/mission_control/web/install/admin_generator.rb +38 -0
  37. data/lib/generators/mission_control/web/install/middleware_generator.rb +26 -0
  38. data/lib/generators/mission_control/web/install_generator.rb +6 -0
  39. data/lib/mission_control/web/action_dispatch_request.rb +7 -0
  40. data/lib/mission_control/web/bulma_form_builder.rb +50 -0
  41. data/lib/mission_control/web/configuration.rb +21 -0
  42. data/lib/mission_control/web/engine.rb +45 -0
  43. data/lib/mission_control/web/errors/resource_not_found.rb +2 -0
  44. data/lib/mission_control/web/request_filter.rb +17 -0
  45. data/lib/mission_control/web/request_filter_middleware.rb +26 -0
  46. data/lib/mission_control/web/routes_cache.rb +52 -0
  47. data/lib/mission_control/web/version.rb +1 -3
  48. data/lib/mission_control/web.rb +18 -4
  49. data/lib/tasks/mission_control/web_tasks.rake +4 -0
  50. metadata +147 -8
  51. data/LICENSE.txt +0 -21
  52. data/mission_control-web.gemspec +0 -35
  53. data/sig/mission_control/web.rbs +0 -6
@@ -0,0 +1,24 @@
1
+ module MissionControl::Web::Application::Routes
2
+ extend ActiveSupport::Concern
3
+
4
+ def routes
5
+ MissionControl::Web::Route.where(application_id: id)
6
+ end
7
+
8
+ def route_was_updated(route)
9
+ routes_cache.put(route)
10
+ end
11
+
12
+ def route_was_deleted(route)
13
+ routes_cache.remove(route)
14
+ end
15
+
16
+ def route_disabled?(path)
17
+ routes_cache.disabled?(path)
18
+ end
19
+
20
+ private
21
+ def routes_cache
22
+ @routes_cache ||= MissionControl::Web::RoutesCache.new(self)
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ class MissionControl::Web::Application
2
+ include ActiveModel::Model
3
+ include Routes
4
+
5
+ attr_accessor :name, :redis
6
+
7
+ class << self
8
+ def all
9
+ MissionControl::Web.configuration.administered_applications.map { |app| new(**app) }
10
+ end
11
+
12
+ def find(id)
13
+ all.find { |application| application.id == id }
14
+ end
15
+
16
+ def find!(id)
17
+ find(id) or raise MissionControl::Web::Errors::ResourceNotFound
18
+ end
19
+
20
+ def find_or_initialize_by_name(name)
21
+ find(name.parameterize) || new(name: name)
22
+ end
23
+
24
+ def default
25
+ all.first or raise MissionControl::Web::Errors::ResourceNotFound
26
+ end
27
+ end
28
+
29
+ def id
30
+ name.parameterize
31
+ end
32
+
33
+ alias to_param id
34
+ end
@@ -0,0 +1,3 @@
1
+ class MissionControl::Web::ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,8 @@
1
+ class MissionControl::Web::Current < ActiveSupport::CurrentAttributes
2
+ attribute :application
3
+
4
+ def application=(application)
5
+ super
6
+ MissionControl::Web.current_redis = application.redis
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ class MissionControl::Web::Route < ApplicationRecord
2
+ include Applications
3
+
4
+ validates :name, :pattern, presence: true
5
+ validates :pattern, uniqueness: { scope: :application_id }
6
+
7
+ def application
8
+ @application ||= MissionControl::Web::Application.find!(application_id)
9
+ end
10
+
11
+ def disabled?
12
+ !enabled?
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Mission Control - Web</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "mission_control/web/application", "data-turbo-track": "reload" %>
9
+ <%= javascript_importmap_tags %>
10
+ </head>
11
+
12
+ <body>
13
+ <div class="container">
14
+ <section class="section">
15
+ <%= render "navbar" %>
16
+ <%= render "flash" %>
17
+
18
+ <%= yield %>
19
+
20
+ </section>
21
+ </div>
22
+ </body>
23
+ </html>
@@ -0,0 +1,15 @@
1
+ <% if selectable_applications.any? %>
2
+ <div class="application-selector navbar-item has-dropdown is-hoverable">
3
+ <a class="navbar-link">
4
+ <%= MissionControl::Web::Current.application.name %>
5
+ </a>
6
+
7
+ <div class="navbar-dropdown">
8
+ <% selectable_applications.each do |application| %>
9
+ <%= link_to application_routes_path(application), class: "navbar-item" do %>
10
+ <span><%= application.name %></span>
11
+ <% end %>
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <% if notice %>
2
+ <div class="notification"><%= notice %></div>
3
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <% if model.errors.any? %>
2
+ <div class="notification content">
3
+ <h3 class="subtitle is-6"><%= pluralize(model.errors.count, "error") %> prohibited this <%= model.class.name.demodulize %> from being saved:</h3>
4
+
5
+ <ul>
6
+ <% model.errors.each do |error| %>
7
+ <li><%= error.full_message %></li>
8
+ <% end %>
9
+ </ul>
10
+ </div>
11
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <nav class="navbar" role="navigation" aria-label="main navigation">
2
+ <div class="navbar-brand">
3
+ <h1 class="title">Mission Control - Web</h1>
4
+ </div>
5
+
6
+ <div class="navbar-menu is-active">
7
+ <div class="navbar-start">
8
+ </div>
9
+
10
+ <div class="navbar-end">
11
+ <%= render "current_application_selector" %>
12
+ </div>
13
+ </div>
14
+ </nav>
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <title>Service temporarily unavailable</title>
4
+ </head>
5
+ <body>
6
+ <p>Service temporarily unavailable. Try again in a few minutes.</p>
7
+ </body>
8
+ </html>
@@ -0,0 +1,26 @@
1
+ <%= form_with(model: [ route.application, route] ) do |form| %>
2
+ <%= render partial: "form_errors", locals: { model: route } %>
3
+
4
+ <div class="field">
5
+ <%= form.label :name %>
6
+ <%= form.text_field :name %>
7
+ </div>
8
+
9
+ <div class="field">
10
+ <%= form.label :pattern %>
11
+ <%= form.text_field :pattern %>
12
+ </div>
13
+
14
+ <div class="field">
15
+ <%= form.label_check_box :enabled, "Traffic allowed" %>
16
+ <%= form.check_box :enabled %>
17
+ </div>
18
+
19
+ <div class="field is-grouped">
20
+ <%= form.submit_primary %>
21
+
22
+ <%= form.div_control do %>
23
+ <%= link_to "Cancel", application_routes_path(route.application), class: "button is-link is-light" %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <tr>
2
+ <td>
3
+ <%= link_to route.name, [ application, route ] %>
4
+ </td>
5
+
6
+ <td>
7
+ <%= route.pattern %>
8
+ </td>
9
+
10
+ <td>
11
+ <%= route_status_tag route.enabled %>
12
+ </td>
13
+ </tr>
@@ -0,0 +1,8 @@
1
+ <nav class="breadcrumb" aria-label="breadcrumbs">
2
+ <ul>
3
+ <li><%= link_to @application.name, application_routes_path(@application) %></li>
4
+ <li class="is-active"><a>Editing <%= @route.name %></a></li>
5
+ </ul>
6
+ </nav>
7
+
8
+ <%= render "form", route: @route %>
@@ -0,0 +1,27 @@
1
+ <div class="level">
2
+ <div class="level-left">
3
+ <h2 class="subtitle level-item"><%= @application.name %> - Routes</h2>
4
+ </div>
5
+ <div class="level-right">
6
+ <%= link_to "New Route", new_application_route_path(@application), class: "button is-primary level-item" %>
7
+ </div>
8
+ </div>
9
+
10
+ <div class="content">
11
+ <% if @routes.any? %>
12
+ <table class="table">
13
+ <thead>
14
+ <tr>
15
+ <th>Name</th>
16
+ <th>Pattern</th>
17
+ <th>Traffic Status</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <%= render @routes, application: @application %>
22
+ </tbody>
23
+ </table>
24
+ <% else %>
25
+ <p>No Routes have yet been configured.</p>
26
+ <% end %>
27
+ </div>
@@ -0,0 +1,8 @@
1
+ <nav class="breadcrumb" aria-label="breadcrumbs">
2
+ <ul>
3
+ <li><%= link_to @application.name, application_routes_path(@application) %></li>
4
+ <li class="is-active"><a>New Route</a></li>
5
+ </ul>
6
+ </nav>
7
+
8
+ <%= render "form", route: @route %>
@@ -0,0 +1,42 @@
1
+ <nav class="breadcrumb" aria-label="breadcrumbs">
2
+ <ul>
3
+ <li><%= link_to @application.name, application_routes_path(@application) %></li>
4
+ <li class="is-active"><%= link_to @route.name, [ @application, @route ] %></li>
5
+ </ul>
6
+ </nav>
7
+
8
+ <div class="my-4">
9
+ <div class="columns">
10
+ <div class="column is-one-fifth">
11
+ <p class="has-text-weight-semibold">Name</p>
12
+ </div>
13
+ <div class="column">
14
+ <p><%= @route.name %></p>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="columns">
19
+ <div class="column is-one-fifth">
20
+ <p class="has-text-weight-semibold">Pattern</p>
21
+ </div>
22
+ <div class="column">
23
+ <p><%= @route.pattern %></p>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="columns">
28
+ <div class="column is-one-fifth">
29
+ <p class="has-text-weight-semibold">Traffic Status</p>
30
+ </div>
31
+ <div class="column">
32
+ <p><%= route_status_tag @route.enabled %></p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="level">
38
+ <div class="level-left">
39
+ <%= link_to "Edit", edit_application_route_path(@application, @route), class: "button level-item" %>
40
+ <%= button_to "Delete", [ @application, @route ], method: :delete, class: "button is-danger level-item" %>
41
+ </div>
42
+ </div>
@@ -0,0 +1,2 @@
1
+ pin "application", to: "mission_control/web/application.js", preload: true
2
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ MissionControl::Web::Engine.routes.draw do
2
+ resources :applications, only: [] do
3
+ resources :routes
4
+ end
5
+
6
+ root to: "routes#index"
7
+ end
@@ -0,0 +1,14 @@
1
+ class CreateMissionControlWebRoutes < ActiveRecord::Migration[7.0]
2
+ def up
3
+ create_table :mission_control_web_routes do |t|
4
+ t.string :name, null: false
5
+ t.string :pattern, null: false
6
+ t.boolean :enabled, default: true
7
+ t.string :application_id, null: false, index: true
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :mission_control_web_routes, [ :application_id, :pattern ], unique: true
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ module MissionControl::Web::Install
2
+ class AdminGenerator < Rails::Generators::Base
3
+ INITIALIZER_FILE_PATH = "config/initializers/mission_control_web.rb"
4
+
5
+ def create_initializer_file
6
+ create_file INITIALIZER_FILE_PATH, <<~RUBY, skip: true
7
+ Rails.application.configure do
8
+ end
9
+ RUBY
10
+ end
11
+
12
+ def insert_admin_configuration
13
+ initializer = <<~RUBY
14
+ config.mission_control.web.middleware_enabled = false
15
+
16
+ # Admin
17
+ config.mission_control.web.administered_applications = [
18
+ { name: "My App", redis: Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0")) }
19
+ ]
20
+ RUBY
21
+
22
+ insert_into_file INITIALIZER_FILE_PATH, indent(initializer), after: "Rails.application.configure do\n"
23
+ end
24
+
25
+ def add_engine_routes
26
+ route "mount MissionControl::Web::Engine => '/mission_control-web'"
27
+ end
28
+
29
+ def copy_migrations
30
+ rake "mission_control_web:install:migrations"
31
+ end
32
+
33
+ def run_migrations
34
+ say "Running migrations..."
35
+ rails_command "db:migrate"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ module MissionControl::Web::Install
2
+ class MissionControl::Web::Install::MiddlewareGenerator < Rails::Generators::Base
3
+ INITIALIZER_FILE_PATH = "config/initializers/mission_control_web.rb"
4
+
5
+ def create_initializer_file
6
+ create_file INITIALIZER_FILE_PATH, <<~RUBY, skip: true
7
+ Rails.application.configure do
8
+ end
9
+ RUBY
10
+ end
11
+
12
+ def insert_middleware_configuration
13
+ initializer = <<~RUBY
14
+ # Middleware
15
+ config.mission_control.web.host_application_name = "My App"
16
+ config.mission_control.web.redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
17
+ RUBY
18
+
19
+ insert_into_file INITIALIZER_FILE_PATH, indent(initializer), after: "Rails.application.configure do\n"
20
+ end
21
+
22
+ def toggle_middleware_enabled
23
+ gsub_file INITIALIZER_FILE_PATH, "middleware_enabled = false", "middleware_enabled = true"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,6 @@
1
+ class MissionControl::Web::InstallGenerator < Rails::Generators::Base
2
+ def install_components
3
+ rails_command "generate mission_control:web:install:admin"
4
+ rails_command "generate mission_control:web:install:middleware"
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module MissionControl::Web
2
+ module ActionDispatchRequest
3
+ def mission_control
4
+ MissionControl::Web::RequestFilter.new(self)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ # Inspired by https://medium.com/@dyanagi/a-bulma-form-builder-for-ruby-on-rails-applications-aef780808bab
2
+
3
+ class MissionControl::Web::BulmaFormBuilder < ActionView::Helpers::FormBuilder
4
+ def label_default(method, text = nil, options = {}, &block)
5
+ label(method, text, merge_class(options, "label"), &block)
6
+ end
7
+
8
+ def label_check_box(method, text = nil, options = {}, &block)
9
+ label(method, text, merge_class(options, "checkbox"), &block)
10
+ end
11
+
12
+ def text_field(method, options = {})
13
+ super(method, merge_class(options, "input"))
14
+ end
15
+
16
+ def submit(value = nil, options = {})
17
+ div_control do
18
+ super(value, merge_class(options, "button"))
19
+ end
20
+ end
21
+
22
+ def submit_primary(value = nil, options = {})
23
+ submit(value, merge_class(options, "is-primary"))
24
+ end
25
+
26
+ def div_control
27
+ @template.content_tag(:div, class: "control") do
28
+ yield
29
+ end
30
+ end
31
+
32
+ def select_with_label(method, choices = nil, options = {}, html_options = {}, &block)
33
+ label_default(method) + select(method, choices, options, html_options, &block)
34
+ end
35
+
36
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
37
+ label(method, class: "select") do
38
+ super
39
+ end
40
+ end
41
+
42
+ private
43
+ def merge_class_attribute_value(options, value)
44
+ new_options = options.clone
45
+ new_options[:class] = [ value, new_options[:class] ].join(" ")
46
+ new_options
47
+ end
48
+
49
+ alias_method :merge_class, :merge_class_attribute_value
50
+ end
@@ -0,0 +1,21 @@
1
+ class MissionControl::Web::Configuration
2
+ include ActiveModel::Attributes, ActiveModel::Dirty
3
+
4
+ attribute :host_application_name, :string
5
+ attribute :middleware_enabled, :boolean, default: true
6
+ attribute :routes_cache_ttl, :integer, default: 10.seconds
7
+ attribute :base_controller_class, :string, default: "::ApplicationController"
8
+
9
+ attr_writer :redis, :administered_applications
10
+ attr_accessor :errors_controller
11
+
12
+ alias :middleware_enabled? :middleware_enabled
13
+
14
+ def redis
15
+ @redis || raise("Redis client not configured. Configure with MissionControl::Web.configuration.redis = Redis.new")
16
+ end
17
+
18
+ def administered_applications
19
+ @administered_applications || []
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ require "importmap-rails"
2
+ require "turbo-rails"
3
+
4
+ module MissionControl
5
+ module Web
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace MissionControl::Web
8
+
9
+ config.mission_control = ActiveSupport::OrderedOptions.new unless config.try(:mission_control)
10
+ config.mission_control.web = ActiveSupport::OrderedOptions.new
11
+
12
+ initializer "mission_control-web.config" do
13
+ config.mission_control.web.each do |key, value|
14
+ MissionControl::Web.configuration.public_send("#{key}=", value)
15
+ end
16
+ end
17
+
18
+ initializer "mission_control-web.request" do
19
+ ActiveSupport.on_load :action_dispatch_request do
20
+ include MissionControl::Web::ActionDispatchRequest
21
+ end
22
+ end
23
+
24
+ initializer "mission_control-web.add_middleware", after: "mission_control-web.config" do |app|
25
+ if MissionControl::Web.configuration.middleware_enabled?
26
+ app.middleware.use MissionControl::Web::RequestFilterMiddleware
27
+ end
28
+ end
29
+
30
+ initializer "mission_control-web.assets" do |app|
31
+ app.config.assets.paths << root.join("app/javascript")
32
+ app.config.assets.precompile += %w[ mission_control_web_manifest ]
33
+ end
34
+
35
+ initializer "mission_control-web.importmap", before: "importmap" do |app|
36
+ app.config.importmap.paths << root.join("config/importmap.rb")
37
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
38
+ end
39
+
40
+ config.after_initialize do
41
+ MissionControl::Web.configuration.host_application_name ||= ::Rails.application.class.module_parent.to_s
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,2 @@
1
+ class MissionControl::Web::Errors::ResourceNotFound < StandardError
2
+ end
@@ -0,0 +1,17 @@
1
+ module MissionControl::Web
2
+ class RequestFilter
3
+ attr_reader :request
4
+
5
+ def initialize(request)
6
+ @request = request
7
+ end
8
+
9
+ def action
10
+ request.fetch_header("mission_control.action") do
11
+ if MissionControl::Web.host_application.route_disabled?(request.path)
12
+ :disallowed
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module MissionControl::Web
2
+ class RequestFilterMiddleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ filter(env) || @app.call(env)
9
+ end
10
+
11
+ def filter(env)
12
+ return unless MissionControl::Web.configuration.middleware_enabled?
13
+
14
+ request = ActionDispatch::Request.new(env)
15
+
16
+ if action = request.mission_control.action
17
+ errors_controller.action(action).call(request.env)
18
+ end
19
+ end
20
+
21
+ private
22
+ def errors_controller
23
+ MissionControl::Web.configuration.errors_controller || MissionControl::Web::ErrorsController
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,52 @@
1
+ class MissionControl::Web::RoutesCache
2
+ delegate :redis, to: MissionControl::Web
3
+
4
+ def initialize(application)
5
+ @application = application
6
+ end
7
+
8
+ def put(route)
9
+ remove(route)
10
+
11
+ if route.disabled?
12
+ add(route)
13
+ end
14
+ end
15
+
16
+ def remove(route)
17
+ redis.srem redis_key, [ route.pattern_previously_was.to_s, route.pattern.to_s ]
18
+ end
19
+
20
+ def disabled?(path)
21
+ all_disabled_patterns.any? { |pattern| Regexp.new(pattern) =~ path }
22
+ end
23
+
24
+ private
25
+ attr_reader :application
26
+
27
+ def add(route)
28
+ redis.sadd redis_key, route.pattern.to_s
29
+ end
30
+
31
+ def redis_key
32
+ :"mission_control_web_#{application.id}_disabled_patterns"
33
+ end
34
+
35
+ def all_disabled_patterns
36
+ memoize(ttl: MissionControl::Web.configuration.routes_cache_ttl) do
37
+ # Using Redis client rather than Kredis as request interception with a middleware is performance-critical.
38
+ redis.smembers redis_key
39
+ end
40
+ rescue Redis::BaseConnectionError
41
+ []
42
+ end
43
+
44
+ def memoize(ttl:)
45
+ if !@patterns.nil? && @patterns_expires_at > Time.now
46
+ @patterns
47
+ else
48
+ @patterns_expires_at = Time.now + ttl
49
+ @patterns = yield
50
+ end
51
+ end
52
+ end
@@ -1,7 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
1
  module MissionControl
4
2
  module Web
5
- VERSION = "0.0.1"
3
+ VERSION = "0.2.0"
6
4
  end
7
5
  end