-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extract bootstrap/framework code from dev #5
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
require 'cli/kit' | ||
|
||
module CLI | ||
module Kit | ||
class BaseCommand | ||
def self.defined? | ||
true | ||
end | ||
|
||
def self.statsd_increment(metric, **kwargs) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll probably migrate |
||
nil | ||
end | ||
|
||
def self.statsd_time(metric, **kwargs) | ||
yield | ||
end | ||
|
||
def self.call(args, command_name) | ||
cmd = new | ||
stats_tags = ["task:#{cmd.class}"] | ||
stats_tags << "subcommand:#{args.first}" if args && args.first && cmd.has_subcommands? | ||
begin | ||
statsd_increment("cli.command.invoked", tags: stats_tags) | ||
statsd_time("cli.command.time", tags: stats_tags) do | ||
cmd.call(args, command_name) | ||
end | ||
statsd_increment("cli.command.success", tags: stats_tags) | ||
rescue => e | ||
statsd_increment("cli.command.exception", tags: stats_tags + ["exception:#{e.class}"]) | ||
raise e | ||
end | ||
end | ||
|
||
def call(args, command_name) | ||
raise NotImplementedError | ||
end | ||
|
||
def has_subcommands? | ||
false | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
require 'cli/kit' | ||
|
||
module CLI | ||
module Kit | ||
module CommandRegistry | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some modest gymnastics here to manage variables on this command vs. access the module it's extended in. |
||
attr_accessor :commands, :aliases | ||
class << self | ||
attr_accessor :registry_target | ||
end | ||
|
||
def resolve_contextual_command | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The concept of "Project-local" and "Type" commands was generalized a bit into "Contextual" commands. The interface to add contextual commands to a command registry is:
This is a pretty niche concern and I don't really expect to build too many tools other than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is only included for stuff from a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well it's "included" everywhere depending on what you mean, but, well: https://github.com/Shopify/dev/blob/4eea78118cc0f6fe4180f37aef97ab676101915c/lib/dev/commands.rb#L7-L19 |
||
nil | ||
end | ||
|
||
def contextual_aliases | ||
{} | ||
end | ||
|
||
def contextual_command_class(_name) | ||
raise NotImplementedError | ||
end | ||
|
||
def self.extended(base) | ||
raise "multiple registries unsupported" if self.registry_target | ||
self.registry_target = base | ||
base.commands = {} | ||
base.aliases = {} | ||
end | ||
|
||
def register(const, name, path) | ||
autoload(const, path) | ||
commands[name] = const | ||
end | ||
|
||
def lookup_command(name) | ||
return default_command if name.to_s == "" | ||
resolve_command(name) | ||
end | ||
|
||
def register_alias(from, to) | ||
aliases[from] = to unless aliases[from] | ||
end | ||
|
||
def resolve_command(name) | ||
resolve_global_command(name) || \ | ||
resolve_contextual_command(name) || \ | ||
[nil, resolve_alias(name)] | ||
end | ||
|
||
def resolve_alias(name) | ||
aliases[name] || contextual_aliases.fetch(name, name) | ||
end | ||
|
||
def resolve_global_command(name) | ||
name = aliases.fetch(name, name) | ||
command_class = const_get(commands.fetch(name, "")) | ||
return nil unless command_class.defined? | ||
[command_class, name] | ||
rescue NameError | ||
nil | ||
end | ||
|
||
def resolve_contextual_command(name) | ||
name = resolve_alias(name) | ||
found = contextual_command_names.include?(name) | ||
return nil unless found | ||
[contextual_command_class(name), name] | ||
end | ||
|
||
def command_names | ||
contextual_command_names + commands.keys | ||
end | ||
|
||
def exist?(name) | ||
!resolve_command(name).first.nil? | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
require 'cli/kit' | ||
require 'fileutils' | ||
|
||
module CLI | ||
module Kit | ||
class Config | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Quick follow up here is to make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. I have bad timing but there wasn't much transformation here so it's an easy follow-up |
||
XDG_CONFIG_HOME = 'XDG_CONFIG_HOME' | ||
|
||
# Returns the config corresponding to `name` from the config file | ||
# `false` is returned if it doesn't exist | ||
# | ||
# #### Parameters | ||
# `section` : the section of the config value you are looking for | ||
# `name` : the name of the config value you are looking for | ||
# | ||
# #### Returns | ||
# `value` : the value of the config variable (false if none) | ||
# | ||
# #### Example Usage | ||
# `config.get('name.of.config')` | ||
# | ||
def get(section, name = nil) | ||
section, name = section.split('.', 2) if name.nil? | ||
# TODO: Remove this and all global configs | ||
return get("global", section) if name.nil? | ||
all_configs.dig("[#{section}]", name) || false | ||
end | ||
|
||
# Sets the config value in the config file | ||
# | ||
# #### Parameters | ||
# `section` : the section of the config you are setting | ||
# `name` : the name of the config you are setting | ||
# `value` : the value of the config you are setting | ||
# | ||
# #### Example Usage | ||
# `config.set('section', 'name.of.config', 'value')` | ||
# | ||
def set(section, name = nil, value) | ||
section, name = section.split('.', 2) if name.nil? | ||
# TODO: Remove this and all global configs | ||
return set("global", section, value) if name.nil? | ||
all_configs["[#{section}]"] ||= {} | ||
all_configs["[#{section}]"][name] = value.nil? ? nil : value.to_s | ||
write_config | ||
end | ||
|
||
def get_section(section) | ||
(all_configs["[#{section}]"] || {}).dup | ||
end | ||
|
||
# Returns a path from config in expanded form | ||
# e.g. shopify corresponds to ~/src/shopify, but is expanded to /Users/name/src/shopify | ||
# | ||
# #### Example Usage | ||
# `config.get_path('srcpath', 'shopify')` | ||
# | ||
# #### Returns | ||
# `path` : the expanded path to the corrsponding value | ||
# | ||
def get_path(section, name = nil) | ||
v = get(section, name) | ||
false == v ? v : File.expand_path(v) | ||
end | ||
|
||
def to_s | ||
ini.to_s | ||
end | ||
|
||
# The path on disk at which the configuration is stored: | ||
# `$XDG_CONFIG_HOME/<toolname>/config` | ||
# if ENV['XDG_CONFIG_HOME'] is not set, we default to ~/.config, e.g.: | ||
# ~/.config/tool/config | ||
# | ||
def file | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is different. I've overridden it back to the current behaviour in |
||
config_home = ENV.fetch(XDG_CONFIG_HOME, '~/.config') | ||
File.expand_path(File.join(CLI::Kit.tool_name, 'config'), config_home) | ||
end | ||
|
||
private | ||
|
||
def all_configs | ||
ini.ini | ||
end | ||
|
||
def ini | ||
@ini ||= CLI::Kit::Ini | ||
.new(file, default_section: "[global]", convert_types: false) | ||
.tap(&:parse) | ||
end | ||
|
||
def write_config | ||
all_configs.each do |section, sub_config| | ||
all_configs[section] = sub_config.reject { |_, value| value.nil? } | ||
all_configs.delete(section) if all_configs[section].empty? | ||
end | ||
FileUtils.mkdir_p(File.dirname(file)) | ||
File.write(file, to_s) | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is
Dev::Command
, more or less. I was able to extract a reasonable amount of it to here and subclass this over there.