Skip to content

Commit

Permalink
refactor: split validation and formatting (#1101)
Browse files Browse the repository at this point in the history
  • Loading branch information
ninoseki authored Jul 14, 2024
1 parent e3d6910 commit 30c0b78
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 49 deletions.
5 changes: 3 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,14 @@ See [Database](./emitters/database.md) for detailed database configuration.
$ mihari rule
Commands:
mihari rule delete ID # Delete a rule
mihari rule format PATH # format a rule
mihari rule get ID # Get a rule
mihari rule help [COMMAND] # Describe subcommands or one specific subcommand
mihari rule init PATH # Initialize a new rule file
mihari rule init PATH # Initialize a new rule
mihari rule list QUERY # List/search rules
mihari rule list-transform QUERY -t, --template=TEMPLATE # List/search rules with transformation
mihari rule search PATH_OR_ID # Search by a rule
mihari rule validate PATH # Validate a rule file
mihari rule validate PATH # Validate rule(s)
```

### `mihari search`
Expand Down
31 changes: 27 additions & 4 deletions lib/mihari/commands/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,37 @@ def _search(q, page: 1, limit: 10)
end
end

desc "validate PATH", "Validate a rule"
desc "validate PATH", "Validate rule(s)"
#
# Validate format of a rule
# Validate rule(s)
#
# @param [Array<String>] paths
#
def validate(*paths)
# @type [Array<Mihari::ValidationError>]
errors = paths.flat_map { |path| Dir.glob(path) }.map do |path|
Dry::Monads::Try[ValidationError] { Mihari::Rule.from_file(path) }
end.filter_map do |result|
result.exception if result.error?
end
return if errors.empty?

errors.each do |error|
data = Entities::ErrorMessage.represent(message: error.message, detail: error.detail)
warn JSON.pretty_generate(data.as_json)
end

exit 1
end

desc "format PATH", "format a rule"
#
# Format a rule file
#
# @param [String] path
#
def validate(path)
rule = Dry::Monads::Try[ValidationError] { Mihari::Rule.from_yaml File.read(path) }.value!
def format(path)
rule = Dry::Monads::Try[ValidationError] { Mihari::Rule.from_file(path) }.value!
puts rule.data.to_yaml
end

Expand Down
28 changes: 23 additions & 5 deletions lib/mihari/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class Rule < Service
include Concerns::FalsePositiveNormalizable
include Concerns::FalsePositiveValidatable

# @return [String, nil]
attr_reader :path_or_id

# @return [Hash]
attr_reader :data

Expand All @@ -19,9 +22,11 @@ class Rule < Service
#
# @param [Hash] data
#
def initialize(**data)
# @param [Object] path_or_id
def initialize(path_or_id: nil, **data)
super()

@path_or_id = path_or_id
@data = data.deep_symbolize_keys
@errors = nil
@base_time = Time.now.utc
Expand Down Expand Up @@ -251,16 +256,29 @@ def update_or_create
end

class << self
#
# Load rule from YAML file
#
# @param [String] path
#
# @return [Mihari::Rule]
#
def from_file(path)
yaml = File.read(path)
from_yaml(yaml, path: path)
end

#
# Load rule from YAML string
#
# @param [String] yaml
# @param [String, nil] path
#
# @return [Mihari::Rule]
#
def from_yaml(yaml)
def from_yaml(yaml, path: nil)
data = YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
new(**data)
new(path_or_id: path, **data)
end

#
Expand All @@ -269,7 +287,7 @@ def from_yaml(yaml)
# @return [Mihari::Rule]
#
def from_model(model)
new(**model.data)
new(path_or_id: model.id, **model.data)
end
end

Expand Down Expand Up @@ -409,7 +427,7 @@ def validate!
@data = result.to_h
@errors = result.errors

raise ValidationError.new("Validation failed", errors) if errors?
raise ValidationError.new("#{path_or_id}: validation failed", errors) if errors?
end
end
end
2 changes: 1 addition & 1 deletion mihari.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "binding_of_caller", "~> 1.0.1"
spec.add_development_dependency "bundler", "~> 2.5"
spec.add_development_dependency "capybara", "~> 3.40"
spec.add_development_dependency "coveralls_reborn", "~> 0.28"
spec.add_development_dependency "factory_bot", "~> 6.4.6"
spec.add_development_dependency "fakefs", "~> 2.5"
spec.add_development_dependency "faker", "~> 3.4.1"
Expand All @@ -60,6 +59,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "rubocop-rspec", "~> 3.0.2"
spec.add_development_dependency "rubocop-yard", "~> 0.9.3"
spec.add_development_dependency "simplecov-lcov", "~> 0.8"
spec.add_development_dependency "simplecov", "~> 0.22"
spec.add_development_dependency "standard", "~> 1.39.1"
spec.add_development_dependency "test-prof", "~> 1.3.3"
spec.add_development_dependency "timecop", "~> 0.9.10"
Expand Down
9 changes: 4 additions & 5 deletions spec/cli/rule_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,18 @@

describe "#validate" do
let!(:path) { File.expand_path("../fixtures/rules/valid_rule.yml", __dir__) }
let!(:data) { File.read path }
let!(:rule_id) { YAML.safe_load(data)["id"] }

it do
expect { described_class.new.invoke(:validate, [path]) }.to output(include(rule_id)).to_stdout
expect { described_class.new.invoke(:validate, [path]) }.not_to output.to_stdout
end

context "with invalid rule" do
let!(:path) { File.expand_path("../fixtures/rules/invalid_rule.yml", __dir__) }

it do
# TODO: assert UnwrapError
expect { described_class.new.invoke(:validate, [path]) }.to raise_error(Dry::Monads::UnwrapError)
expect do
expect { described_class.new.invoke(:validate, [path]) }.to output(include(path)).to_stderr
end.to raise_error(SystemExit) # ref. https://pocke.hatenablog.com/entry/2016/07/17/085928
end
end
end
Expand Down
35 changes: 3 additions & 32 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,48 +21,19 @@ def ci_env?
ENV["CI"]
end

# setup simplecov formatter for coveralls
class InceptionFormatter
def format(result)
Coveralls::SimpleCov::Formatter.new.format(result)
end
end

def formatter
if ENV["CI"] || ENV["COVERALLS_REPO_TOKEN"]
if ENV["GITHUB_ACTIONS"]
SimpleCov::Formatter::MultiFormatter.new([InceptionFormatter, SimpleCov::Formatter::LcovFormatter])
else
InceptionFormatter
end
else
SimpleCov::Formatter::HTMLFormatter
end
end

def setup_formatter
if ENV["GITHUB_ACTIONS"]
SimpleCov.start "rails" do
if ENV["CI"]
require "simplecov-lcov"

SimpleCov::Formatter::LcovFormatter.config do |c|
c.report_with_single_file = true
c.single_report_path = "coverage/lcov.info"
end
end
SimpleCov.formatter = formatter
end

setup_formatter

SimpleCov.start do
add_filter do |source_file|
source_file.filename.include?("spec") && !source_file.filename.include?("fixture")
formatter SimpleCov::Formatter::LcovFormatter
end
add_filter %r{/.bundle/}
end

require "coveralls"

# for Rack app / Sinatra controllers
ENV["APP_ENV"] = "test"
# Use in-memory SQLite in local test
Expand Down

0 comments on commit 30c0b78

Please sign in to comment.