From 5413b4a929f6bb0716d4cb6585b6e39e9135ae59 Mon Sep 17 00:00:00 2001 From: Oleksii Leonov Date: Tue, 2 May 2023 04:45:20 +0000 Subject: [PATCH] feat: initial commit --- .devcontainer/devcontainer.json | 73 +++++ .dockerignore | 5 + .editorconfig | 12 + .github/CODEOWNERS | 3 + .github/dependabot.yml | 6 + .github/workflows/main.yml | 75 +++++ .gitignore | 47 +++ .overcommit.yml | 29 ++ .rspec | 3 + .rubocop.yml | 27 ++ CHANGELOG.md | 5 + CODE_OF_CONDUCT.md | 133 ++++++++ CONTRIBUTING.md | 63 ++++ Dockerfile | 11 + Gemfile | 20 ++ LICENSE | 21 ++ README.md | 297 ++++++++++++++++++ Rakefile | 12 + bin/console | 11 + bin/rspec | 18 ++ bin/setup | 8 + exe/nomius | 6 + lib/nomius.rb | 13 + lib/nomius/bulk_checker.rb | 39 +++ lib/nomius/checker.rb | 30 ++ lib/nomius/cli.rb | 24 ++ lib/nomius/cli/command.rb | 106 +++++++ lib/nomius/cli/parser.rb | 22 ++ lib/nomius/cli/parser/file_parser.rb | 27 ++ .../cli/parser/file_parser/csv_parser.rb | 31 ++ .../cli/parser/file_parser/txt_parser.rb | 40 +++ lib/nomius/cli/parser/strings_parser.rb | 37 +++ lib/nomius/cli/runner.rb | 42 +++ lib/nomius/cli/writer/console_writer.rb | 68 ++++ lib/nomius/cli/writer/csv_writer.rb | 51 +++ lib/nomius/detector.rb | 28 ++ .../detector/base_domain_name_detector.rb | 86 +++++ lib/nomius/detector/base_url_detector.rb | 46 +++ lib/nomius/detector/dockerhub_detector.rb | 26 ++ lib/nomius/detector/domain_com_detector.rb | 15 + lib/nomius/detector/domain_org_detector.rb | 15 + lib/nomius/detector/github_detector.rb | 26 ++ lib/nomius/detector/npmjs_detector.rb | 26 ++ lib/nomius/detector/pypi_detector.rb | 26 ++ lib/nomius/detector/rubygems_detector.rb | 26 ++ lib/nomius/detector/util/http_requester.rb | 78 +++++ lib/nomius/logger.rb | 18 ++ lib/nomius/logger/silent.rb | 31 ++ lib/nomius/logger/verbose.rb | 47 +++ lib/nomius/name.rb | 26 ++ lib/nomius/status.rb | 5 + lib/nomius/status/available.rb | 22 ++ lib/nomius/status/base.rb | 16 + lib/nomius/status/formatter/ascii_mark.rb | 24 ++ lib/nomius/status/formatter/mark.rb | 24 ++ lib/nomius/status/unavailable.rb | 22 ++ lib/nomius/status/unresolved.rb | 22 ++ lib/nomius/version.rb | 5 + nomius.gemspec | 46 +++ spec/nomius/cli/command_spec.rb | 95 ++++++ spec/nomius/cli_spec.rb | 20 ++ spec/nomius_spec.rb | 7 + spec/sample_data/expected_help_output.txt | 32 ++ spec/sample_data/system/console_output.txt | 6 + spec/sample_data/system/input.csv | 2 + spec/sample_data/system/output.csv | 3 + spec/spec_helper.rb | 88 ++++++ spec/system/nomius_cli_spec.rb | 54 ++++ 68 files changed, 2428 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .overcommit.yml create mode 100644 .rspec create mode 100644 .rubocop.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Rakefile create mode 100755 bin/console create mode 100755 bin/rspec create mode 100755 bin/setup create mode 100755 exe/nomius create mode 100644 lib/nomius.rb create mode 100644 lib/nomius/bulk_checker.rb create mode 100644 lib/nomius/checker.rb create mode 100644 lib/nomius/cli.rb create mode 100644 lib/nomius/cli/command.rb create mode 100644 lib/nomius/cli/parser.rb create mode 100644 lib/nomius/cli/parser/file_parser.rb create mode 100644 lib/nomius/cli/parser/file_parser/csv_parser.rb create mode 100644 lib/nomius/cli/parser/file_parser/txt_parser.rb create mode 100644 lib/nomius/cli/parser/strings_parser.rb create mode 100644 lib/nomius/cli/runner.rb create mode 100644 lib/nomius/cli/writer/console_writer.rb create mode 100644 lib/nomius/cli/writer/csv_writer.rb create mode 100644 lib/nomius/detector.rb create mode 100644 lib/nomius/detector/base_domain_name_detector.rb create mode 100644 lib/nomius/detector/base_url_detector.rb create mode 100644 lib/nomius/detector/dockerhub_detector.rb create mode 100644 lib/nomius/detector/domain_com_detector.rb create mode 100644 lib/nomius/detector/domain_org_detector.rb create mode 100644 lib/nomius/detector/github_detector.rb create mode 100644 lib/nomius/detector/npmjs_detector.rb create mode 100644 lib/nomius/detector/pypi_detector.rb create mode 100644 lib/nomius/detector/rubygems_detector.rb create mode 100644 lib/nomius/detector/util/http_requester.rb create mode 100644 lib/nomius/logger.rb create mode 100644 lib/nomius/logger/silent.rb create mode 100644 lib/nomius/logger/verbose.rb create mode 100644 lib/nomius/name.rb create mode 100644 lib/nomius/status.rb create mode 100644 lib/nomius/status/available.rb create mode 100644 lib/nomius/status/base.rb create mode 100644 lib/nomius/status/formatter/ascii_mark.rb create mode 100644 lib/nomius/status/formatter/mark.rb create mode 100644 lib/nomius/status/unavailable.rb create mode 100644 lib/nomius/status/unresolved.rb create mode 100644 lib/nomius/version.rb create mode 100644 nomius.gemspec create mode 100644 spec/nomius/cli/command_spec.rb create mode 100644 spec/nomius/cli_spec.rb create mode 100644 spec/nomius_spec.rb create mode 100644 spec/sample_data/expected_help_output.txt create mode 100644 spec/sample_data/system/console_output.txt create mode 100644 spec/sample_data/system/input.csv create mode 100644 spec/sample_data/system/output.csv create mode 100644 spec/spec_helper.rb create mode 100644 spec/system/nomius_cli_spec.rb diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..04f2d79 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,73 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ruby +{ + "name": "Ruby", + + "image": "mcr.microsoft.com/devcontainers/ruby:0-3.2", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "bundle && bundle exec overcommit --install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "root", + + "customizations": { + "vscode": { + "settings": { + // Basic formatting settings. + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + + // Colorize and highlight bracket pairs. + "editor.guides.bracketPairs": true, + "editor.bracketPairColorization.enabled": true, + + // Require to confirm exit when the terminal is active (helps a lot when accidentally Cmd-Q pressed). + "terminal.integrated.confirmOnExit": "hasChildProcesses", + + // Controls the maximum amount of lines the terminal keeps in its buffer. + // Increase to 50000. Default 1000 is too low. + "terminal.integrated.scrollback": 50000, + + // Ruby LSP settings + "rubyLsp.formatter": "rubocop", + "rubyLsp.enableExperimentalFeatures": true, + + // Ruby LSP advanced semantic highlighting. + "workbench.colorTheme": "Spinel", + + // Ruby settings. + "[ruby]": { + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.defaultFormatter": "Shopify.ruby-lsp", + "editor.rulers": [ + 120 + ], + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.semanticHighlighting.enabled": true + }, + }, + + // Add the IDs of extensions you want installed when the container is created in the array below. + "extensions": [ + "shopify.ruby-extensions-pack", + "eamodio.gitlens", + "ms-azuretools.vscode-docker", + "davidanson.vscode-markdownlint", + "bierner.markdown-preview-github-styles", + "github.vscode-github-actions" + ] + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6732568 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.bundle/* +.idea* +log/* +tmp/* +.ruby-lsp/* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f41463 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# Defines the coding style for different editors and IDEs. +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..50b891a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +* @oleksii-leonov diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a310fbf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: bundler + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..03ab3ab --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,75 @@ +name: Test Suite + +on: + pull_request: + push: + branches: + - main + +jobs: + overcommit: + runs-on: ubuntu-latest + name: Overcommit checks + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - name: Overcommit checks + # NOTE: Set to make Overcommit's AuthorName and AuthorEmail checks happy. + env: + GIT_AUTHOR_NAME: example + GIT_AUTHOR_EMAIL: example@example.com + run: | + bundle exec overcommit --sign --force + SKIP=ForbiddenBranches bundle exec overcommit --run + specs: + needs: [overcommit] + runs-on: ${{ matrix.os }} + name: Ruby ${{ matrix.ruby }} on ${{ matrix.os }} + strategy: + matrix: + os: ["ubuntu-22.04", "macos-12", "windows-2022"] + ruby: ["2.6", "2.7", "3.0", "3.1", "3.2"] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run the default task + env: + # NOTE: Using fixed console table width. + # TTY width in GitHub actions could be as small as "1" for Windows machines. + TTY_TABLE_WIDTH: fixed + run: bundle exec rake + build_and_test_docker_image: + needs: [overcommit] + runs-on: ubuntu-latest + name: Build and test docker image + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build docker image + uses: docker/build-push-action@v4 + with: + load: true + tags: test + - name: Run image + run: | + mkdir -p /tmp/sample_data + cp spec/sample_data/system/input.csv /tmp/sample_data/input.csv + docker run -t --rm \ + -v /tmp/sample_data:/tmp/sample_data \ + test \ + --input /tmp/sample_data/input.csv \ + --output /tmp/sample_data/output.csv + - name: Check results + run: diff /tmp/sample_data/output.csv spec/sample_data/system/output.csv diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c36210a --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +*.gem +*.rbc +/.config +/coverage/ +/spec/reports/ +/spec/examples.txt +/tmp/ + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ + +## Environment normalization: +/.bundle/ +/vendor/bundle +/lib/bundler/man/ + +# for a library or gem, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +Gemfile.lock +.ruby-version +.ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc + +# RSpec +/.rspec_status + +# Checksums folder +/checksums + +# MacOS files +.DS_Store +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes + +# Ruby-LSP VSCode extension +/.ruby-lsp + +# VSCode workspace settings +/.vscode diff --git a/.overcommit.yml b/.overcommit.yml new file mode 100644 index 0000000..a182af0 --- /dev/null +++ b/.overcommit.yml @@ -0,0 +1,29 @@ +PreCommit: + ALL: + on_warn: fail + BundleCheck: + enabled: true + FileSize: + enabled: true + size_limit_bytes: 1_000_000 + ForbiddenBranches: + enabled: true + LineEndings: + enabled: true + RuboCop: + enabled: true + TrailingWhitespace: + enabled: true + YamlSyntax: + enabled: true + +CommitMsg: + ALL: + on_warn: fail + # Disable some default Overcommit checks to avoid collisions with commitlint. + TextWidth: + enabled: false + CapitalizedSubject: + enabled: false + SingleLineSubject: + enabled: false diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..deaef21 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,27 @@ +require: + - rubocop-rspec + +AllCops: + NewCops: enable + TargetRubyVersion: 2.6 + +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + Enabled: true + EnforcedStyle: double_quotes + +Layout/LineLength: + Max: 120 + +# Relax RSpec metrics +RSpec/ExampleLength: + Max: 20 # Defaut is 5 + +RSpec/MultipleExpectations: + Max: 3 # Defaut is 1 + +RSpec/MultipleMemoizedHelpers: + Max: 10 # Defaut is 5 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..08a27be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +## [Unreleased] + +## [0.1.0] - 2023-05-02 + +- Initial release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f253d56 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +ospo@syngenta.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..65fe749 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing + +- [Introduction](#introduction) +- [Reporting issues](#reporting-issues) +- [Contributing to code](#contributing-to-code) + - [Workflow for local development](#workflow-for-local-development) + - [Workflow for GitHub Codespaces in-browser development](#workflow-for-github-codespaces-in-browser-development) + - [About using VSCode](#about-using-vscode) + +## Introduction + +Contribution is welcome! + +This project adheres to the [Code of Conduct](CODE_OF_CONDUCT.md). We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Reporting issues + +Please, report any issues or bugs you find in the project's [Issues](https://github.com/syngenta/nomius/issues) section on GitHub. + +## Contributing to code + +### Workflow for local development + +1. [Fork the repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo#forking-a-repository). +2. [Clone the forked repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo#cloning-your-forked-repository) to your local machine. +3. Create a new branch for your changes: + `git checkout -b "feat/my-new-feature"`. +4. Make your changes. We recommend using [VSCode + Docker](#about-using-vscode) locally. +5. Commit your changes: + `git commit -m "feat: my new feature"`. +6. Push your changes to your forked repo: + `git push origin feat/my-new-feature`. +7. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) from your forked repo to the main repo. + +### Workflow for GitHub Codespaces in-browser development + +1. [Fork the repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo#forking-a-repository). +2. Open Codespaces by clicking the green button **Code** on the top right corner of the repo and then **Open with Codespaces**. +GitHub Codespaces will create and open a full-featured dev environment in the browser. You don't need to install anything locally. +3. Create a new branch for your changes: + `git checkout -b "feat/my-new-feature"` +4. Make your changes. +5. Commit your changes: + `git commit -m "feat: my new feature"` +6. Push your changes to your forked repo: + `git push origin feat/my-new-feature` +7. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) from your forked repo to the main repo. + +### About using VSCode + +The recommended way for local development is to use VSCode + Docker Desktop. + +- [VSCode](https://code.visualstudio.com/) +- [Docker Desktop](https://www.docker.com/) + +VSCode supports `.devcontainer` configuration that will automatically build a full-featured dev environment for you. All plugins and hooks are already configured for you. + +```shell +git clone git@github.com:syngenta/nomius.git +cd nomius + +code . +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5ea08f6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM ruby:3.2.2 +LABEL maintainer="Oleksii Leonov " + +RUN bundle config --global without development test + +WORKDIR /usr/src/app + +COPY . . +RUN bundle install + +ENTRYPOINT ["bundle", "exec", "nomius"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..736ebef --- /dev/null +++ b/Gemfile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Gem's dependencies are in nomius.gemspec. +gemspec + +group :development do + gem "overcommit", "0.60.0" + gem "rubocop", "1.50.2" + gem "rubocop-rspec", "2.20.0" +end + +group :test do + gem "rspec", "~> 3.12.0" +end + +group :development, :test do + gem "rake" +end diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ef442b8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Syngenta Group Co. Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b054779 --- /dev/null +++ b/README.md @@ -0,0 +1,297 @@ +# Nomius — bulk domain & package name availability checker + +- [Description](#description) +- [Ratio](#ratio) +- [Installation](#installation) + - [Ruby gem](#ruby-gem) + - [Docker image](#docker-image) +- [Usage](#usage) + - [Basic usage](#basic-usage) + - [Built-in help](#built-in-help) + - [Using TXT file as input](#using-txt-file-as-input) + - [Using CSV file as input](#using-csv-file-as-input) + - [Using CSV file as input and output](#using-csv-file-as-input-and-output) +- [Using as a Ruby library](#using-as-a-ruby-library) +- [Contributing](#contributing) +- [Notes](#notes) + +## Description + +`nomius` takes a list of names as input and check domain name (`.com`, `.org`) and package name (RubyGems, PyPi, NPMjs, etc.) availability. + +The very basic usage example: + +```shell +user@home:~$ nomius biochem biochemio biochemus + +┌───────────┬──────┬──────┬────┬────────┬─────┬─────┬─────┐ +│ Name │ .com │ .org │ GH │ Docker │ npm │ pip │ gem │ +├───────────┼──────┼──────┼────┼────────┼─────┼─────┼─────┤ +│ biochem │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ +│ biochemio │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ biochemus │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ +└───────────┴──────┴──────┴────┴────────┴─────┴─────┴─────┘ +``` + +- `nomius` is a console utility. Could be installed & used as: + - [Ruby gem](#ruby-gem) (Ruby 2.6+); + - [Docker image](#docker-image). +- Availability checks supported: + - domain name (`.com`,`.org`); + - [RubyGems](https://rubygems.org/) package name; + - [PyPi](https://pypi.org/) package name; + - [NPMjs](https://www.npmjs.com/) package name; + - [GitHub](https://github.com/) user/org name; + - [DockerHub](https://hub.docker.com/) user/org name. +- Input is a name, list of names or CSV file with a list of names. +- Output is a table with check results for each name. You could choose output to console or CSV file. + +## Ratio + +For example, you have created a new biochemistry project. Now you need to find a short and memorable name, with a domain and package name available to register. + +You may brainstorm dozens of names (or use a script to generate hundreds of names in different combinations): + +```txt +liber, chimeia, bchem, chemb, biochem, chembio, biochemio, biochemus +``` + +Now you need to filter this list to names that have a domain name and package name available to register. But in popular domain zones (`.com`, `.org`) most of the short and memoizable names are already registered. Also, a good package name may be hard to find, especially if you want the name to be available across different languages and package managers. + +Manually checking all those names to have available domains (`.com`, `.org`) and package names (`pip`, `npm`, `gem`) is a tedious manual task. + +`nomius` will check all your names in a minute 🚀 + +```shell +user@home:~$ nomius liber chimeia bchem chemb biochem chembio biochemio biochemus + +┌───────────┬──────┬──────┬────┬────────┬─────┬─────┬─────┐ +│ Name │ .com │ .org │ GH │ Docker │ npm │ pip │ gem │ +├───────────┼──────┼──────┼────┼────────┼─────┼─────┼─────┤ +│ bchem │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ biochem │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ +│ biochemio │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ biochemus │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ chemb │ ❌ │ ✅ │ ❌ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ chembio │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ chimeia │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ liber │ ❌ │ ❌ │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ │ +└───────────┴──────┴──────┴────┴────────┴─────┴─────┴─────┘ +``` + +## Installation + +### Ruby gem + +```shell +# Install nomius gem. +user@home:~$ gem install nomius + +# Run nomius. +user@home:~$ nomius biochem + +┌─────────┬──────┬──────┬────┬────────┬─────┬─────┬─────┐ +│ Name │ .com │ .org │ GH │ Docker │ npm │ pip │ gem │ +├─────────┼──────┼──────┼────┼────────┼─────┼─────┼─────┤ +│ biochem │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ +└─────────┴──────┴──────┴────┴────────┴─────┴─────┴─────┘ +``` + +### Docker image + +All the options are the same as for the Ruby gem. + +> Docker image is not published yet. But you could build it yourself. + +```shell +# 1. Clone the repository. +user@home:~$ git clone git@github.com:syngenta/nomius.git +user@home:~$ cd nomius + +# 2. Build the Docker image. +user@home:~$ docker build -t nomius . + +# 3. Run the Docker container. +user@home:~$ docker run -t --rm nomius biochem + +┌─────────┬──────┬──────┬────┬────────┬─────┬─────┬─────┐ +│ Name │ .com │ .org │ GH │ Docker │ npm │ pip │ gem │ +├─────────┼──────┼──────┼────┼────────┼─────┼─────┼─────┤ +│ biochem │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ +└─────────┴──────┴──────┴────┴────────┴─────┴─────┴─────┘ +``` + +## Usage + +### Basic usage + +```shell +# Run nomius with a list of names to check. +user@home:~$ nomius biochem biochemio biochemus + +┌───────────┬──────┬──────┬────┬────────┬─────┬─────┬─────┐ +│ Name │ .com │ .org │ GH │ Docker │ npm │ pip │ gem │ +├───────────┼──────┼──────┼────┼────────┼─────┼─────┼─────┤ +│ biochem │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ +│ biochemio │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ biochemus │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ +└───────────┴──────┴──────┴────┴────────┴─────┴─────┴─────┘ +``` + +### Built-in help + +```shell +# Run built-in help. +user@home:~$ nomius --help + +Usage: nomius [OPTIONS] [NAMES...] + +Nomius — bulk domain & package name availability checker. + +Options: + -h, --help Print usage + -i, --input string Input file. Could be: + - TXT with each name on a separate line; + - CSV file with 2 columns: "name","comment" ("comment" + is optional). + + -o, --output string Output CSV file + -s, --silent Print less output + --version Print version + +Examples: + Basic usage + Check "firstname" and "othername" names. + $ nomius firstname othername + + Usage with a TXT file + $ nomius --input names.txt + or + $ cat names.txt | nomius + or + $ nomius < names.txt + + Usage with a CSV file + $ nomius --input names.csv + + Usage with a CSV file and output to a CSV file + $ nomius --input names.csv --output results.csv +``` + +### Using TXT file as input + +Use a TXT file with each name on a separate line. +Wrap strings in quotes if it contains any non-alphanumeric characters. + +```txt +biochem +biochemio +biochemus +``` + +```shell +user@home:~$ nomius --input names.txt + +┌───────────┬──────┬──────┬────┬────────┬─────┬─────┬─────┐ +│ Name │ .com │ .org │ GH │ Docker │ npm │ pip │ gem │ +├───────────┼──────┼──────┼────┼────────┼─────┼─────┼─────┤ +│ biochem │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ +│ biochemio │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ +│ biochemus │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ +└───────────┴──────┴──────┴────┴────────┴─────┴─────┴─────┘ +``` + +Also, you could use shell pipe: + +```shell +user@home:~$ cat names.txt | nomius +# or +user@home:~$ nomius < names.txt +``` + +### Using CSV file as input + +Input CSV file with 2 columns: _name_, _comment_. +_Name_ is required, _comment_ is optional: + +```CSV +biochem,"short of bio+chemistry" +biochemio,"short of biochemistry + fancy ending" +biochemus,"short of biochemistry + fancy ending" +``` + +```shell +user@home:~$ nomius --input names.csv + +┌───────────┬──────┬──────┬────┬────────┬─────┬─────┬─────┬──────────────────────────────────────┐ +│ Name │ .com │ .org │ GH │ Docker │ npm │ pip │ gem │ Comment │ +├───────────┼──────┼──────┼────┼────────┼─────┼─────┼─────┼──────────────────────────────────────┤ +│ biochem │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ │ ✅ │ ✅ │ short of bio+chemistry │ +│ biochemio │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ short of biochemistry + fancy ending │ +│ biochemus │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ short of biochemistry + fancy ending │ +└───────────┴──────┴──────┴────┴────────┴─────┴─────┴─────┴──────────────────────────────────────┘ +``` + +### Using CSV file as input and output + +```shell +user@home:~$ nomius --input names.csv --output results.csv +``` + +Output in `results.csv` CSV file: + +```csv +Name,Comment,bchem.com,bchem.org,GitHub.com,hub.docker.com,NPMjs.com,PyPi.org,RubyGems.org +biochem,short of bio+chemistry,-,-,-,-,+,+,+ +biochemio,short of biochemistry + fancy ending,+,+,+,+,+,+,+ +biochemus,short of biochemistry + fancy ending,+,+,+,+,+,+,+ +``` + +## Using as a Ruby library + +`nomius` is designed to be a CLI tool, but you could use it as a Ruby library. + +```shell +# Install nomius gem. +user@home:~$ gem install nomius +``` + +```ruby +require 'nomius' + +# Run all checks: +results = Nomius::BulkChecker.check( + names: ["biochem", "biochemio", "biochemus"] +) + +# Run only specific checks: +results = Nomius::BulkChecker.check( + names: ["biochem", "biochemio", "biochemus"], + detectors: [Nomius::Detector::DomainComDetector] +) + +# Run with verbose logger: +results = Nomius::BulkChecker.check( + names: ["biochem", "biochemio", "biochemus"], + logger: Nomius::Logger::Verbose.new +) + +# Use names with comments +names = [ + Nomius::Name.new(name: "biochem", comment: 'short of bio+chemistry'), + Nomius::Name.new(name: "biochemio", comment: 'short of biochemistry + fancy ending') +] +results = Nomius::BulkChecker.check(names: names) +``` + +## Contributing + +Bug reports and pull requests are welcome on GitHub at [https://github.com/syngenta/nomius](https://github.com/syngenta/nomius). + +Please, check our [Contribution guide](CONTRIBUTING.md) for more details. + +This project adheres to the [Code of Conduct](CODE_OF_CONDUCT.md). We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Notes + +- `nomius` uses DNS and WHOIS checks to verify domain name availability. Results may not be 100% precise. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..156fd6e --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: %i[spec] diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..ccb72ed --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "nomius" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000..f62cd80 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/exe/nomius b/exe/nomius new file mode 100755 index 0000000..e773788 --- /dev/null +++ b/exe/nomius @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "nomius/cli" + +Nomius::CLI.run diff --git a/lib/nomius.rb b/lib/nomius.rb new file mode 100644 index 0000000..daa0a00 --- /dev/null +++ b/lib/nomius.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "nomius/bulk_checker" +require_relative "nomius/checker" +require_relative "nomius/detector" +require_relative "nomius/logger" +require_relative "nomius/name" +require_relative "nomius/status" +require_relative "nomius/version" + +# Nomius +module Nomius +end diff --git a/lib/nomius/bulk_checker.rb b/lib/nomius/bulk_checker.rb new file mode 100644 index 0000000..a58bd3a --- /dev/null +++ b/lib/nomius/bulk_checker.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require_relative "checker" +require_relative "detector" +require_relative "name" +require_relative "logger/silent" +require_relative "status/formatter/mark" + +module Nomius + # BulkChecker + class BulkChecker + RESULT = Struct.new(:name, :results, keyword_init: true) + + attr_reader :names, :detectors, :logger + + def self.check(*args, **kwargs) + new(*args, **kwargs).check + end + + def initialize(names: [], detectors: Detector.all, logger: Logger::Silent.new) + @names = names.compact.map { |name| Name.for(name) } + @detectors = detectors + @logger = logger + end + + def check + logger.start_batch_processing(names.count) + + names.map do |name| + logger.batch_record_processing(name) do + RESULT.new( + name: name, + results: Checker.check(name: name, detectors: detectors, logger: logger) + ) + end + end + end + end +end diff --git a/lib/nomius/checker.rb b/lib/nomius/checker.rb new file mode 100644 index 0000000..e562fb2 --- /dev/null +++ b/lib/nomius/checker.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative "detector" +require_relative "name" +require_relative "logger/silent" + +module Nomius + # Checker + class Checker + attr_reader :name, :detectors, :logger + + def self.check(*args, **kwargs) + new(*args, **kwargs).check + end + + def initialize(name:, detectors: Detector.all, logger: Logger::Silent.new) + @name = Name.for(name) + @detectors = detectors + @logger = logger + end + + def check + detectors.map do |detector| + logger.log_detector_status do + detector.status(name: name, logger: logger) + end + end + end + end +end diff --git a/lib/nomius/cli.rb b/lib/nomius/cli.rb new file mode 100644 index 0000000..141c918 --- /dev/null +++ b/lib/nomius/cli.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "cli/command" +require_relative "cli/runner" + +module Nomius + # CLI + class CLI + attr_reader :names, :silent, :input, :output + + def self.run(args = ARGV) + cmd = Command.new.parse(args) + cmd.run + params = cmd.params + + Runner.run( + names: params[:names], + silent: params[:silent], + input: params[:input], + output: params[:output] + ) + end + end +end diff --git a/lib/nomius/cli/command.rb b/lib/nomius/cli/command.rb new file mode 100644 index 0000000..ed5aeb7 --- /dev/null +++ b/lib/nomius/cli/command.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require "tty-option" +require_relative "../version" + +module Nomius + class CLI + # Console options parser + class Command + include TTY::Option + + usage do + program "nomius" + no_command + desc "Nomius — bulk domain & package name availability checker." + example <<~DESCRIPTION + Basic usage + Check "firstname" and "othername" names. + $ nomius firstname othername + + Usage with a TXT file + $ nomius --input names.txt + or + $ cat names.txt | nomius + or + $ nomius < names.txt + + Usage with a CSV file + $ nomius --input names.csv + + Usage with a CSV file and output to a CSV file + $ nomius --input names.csv --output results.csv + DESCRIPTION + end + + flag :help do + short "-h" + long "--help" + desc "Print usage" + end + + flag :silent do + short "-s" + long "--silent" + desc "Print less output" + end + + flag :version do + long "--version" + desc "Print version" + end + + option :input do + short "-i" + long "--input string" + desc <<~DESCRIPTION + Input file. Could be: + - TXT with each name on a separate line; + - CSV file with 2 columns: "name","comment" ("comment" is optional). + DESCRIPTION + end + + option :output do + short "-o" + long "--output string" + desc "Output CSV file" + end + + argument :names do + arity zero_or_more + # Read from STDIN to support piping. + default -> { $stdin.tty? ? [] : $stdin.each_line.to_a } + convert :list + end + + def run + check_help! + check_version! + check_input! + end + + private + + def check_help! + return unless params[:help] + + print help + exit + end + + def check_version! + return unless params[:version] + + puts Nomius::VERSION + exit + end + + def check_input! + return unless params[:names].empty? && params[:input].nil? + + print help + exit(1) + end + end + end +end diff --git a/lib/nomius/cli/parser.rb b/lib/nomius/cli/parser.rb new file mode 100644 index 0000000..c550c6d --- /dev/null +++ b/lib/nomius/cli/parser.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative "parser/file_parser" +require_relative "parser/strings_parser" + +module Nomius + class CLI + # ParserChooser + class Parser + PARSER_BY_INPUT_FILE_PRESENCE = { + true => FileParser, + false => StringsParser + }.freeze + + def self.names(file_name: nil, strings: []) + PARSER_BY_INPUT_FILE_PRESENCE + .fetch(!file_name.nil?) + .names(file_name: file_name, strings: strings) + end + end + end +end diff --git a/lib/nomius/cli/parser/file_parser.rb b/lib/nomius/cli/parser/file_parser.rb new file mode 100644 index 0000000..38a7b96 --- /dev/null +++ b/lib/nomius/cli/parser/file_parser.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative "../../name" +require_relative "file_parser/txt_parser" +require_relative "file_parser/csv_parser" + +module Nomius + class CLI + class Parser + # Parser for files + class FileParser + FALLBACK_PARSER = TXTParser + + PARSER_BY_FILE_EXTENSION = { + ".csv" => CSVParser + }.freeze + + def self.names(file_name:, **_kwargs) + PARSER_BY_FILE_EXTENSION + .fetch(File.extname(file_name), FALLBACK_PARSER) + .new(file_name: file_name) + .names + end + end + end + end +end diff --git a/lib/nomius/cli/parser/file_parser/csv_parser.rb b/lib/nomius/cli/parser/file_parser/csv_parser.rb new file mode 100644 index 0000000..c70ce4d --- /dev/null +++ b/lib/nomius/cli/parser/file_parser/csv_parser.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "csv" +require_relative "../../../name" + +module Nomius + class CLI + class Parser + class FileParser + # Parser for CSV files + class CSVParser + attr_reader :file_name + + def self.names(file_name:) + new(file_name: file_name).names + end + + def initialize(file_name:) + @file_name = file_name + end + + def names + CSV.read(file_name, skip_blanks: true, liberal_parsing: true).map do |name, comment| + Name.new(name: name, comment: comment) + end + end + end + end + end + end +end diff --git a/lib/nomius/cli/parser/file_parser/txt_parser.rb b/lib/nomius/cli/parser/file_parser/txt_parser.rb new file mode 100644 index 0000000..9775e42 --- /dev/null +++ b/lib/nomius/cli/parser/file_parser/txt_parser.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "../../../name" + +module Nomius + class CLI + class Parser + class FileParser + # Parser for TXT files + class TXTParser + attr_reader :file_name + + def self.names(file_name:) + new(file_name: file_name).names + end + + def initialize(file_name:) + @file_name = file_name + end + + def names + cleared_names.map do |name| + Name.new(name: name) + end + end + + private + + def cleared_names + File + .readlines(file_name) + .reject { |name| name.strip.empty? } + .sort + .uniq + end + end + end + end + end +end diff --git a/lib/nomius/cli/parser/strings_parser.rb b/lib/nomius/cli/parser/strings_parser.rb new file mode 100644 index 0000000..bd7994c --- /dev/null +++ b/lib/nomius/cli/parser/strings_parser.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative "../../name" + +module Nomius + class CLI + class Parser + # Parser for array of strings + class StringsParser + def self.names(strings:, **_kwargs) + new(strings: strings).names + end + + def initialize(strings:, **_kwargs) + @strings = strings + end + + def names + cleared_names.map do |name| + Name.new(name: name) + end + end + + private + + attr_reader :strings + + def cleared_names + strings + .reject { |name| name.strip.empty? } + .sort + .uniq + end + end + end + end +end diff --git a/lib/nomius/cli/runner.rb b/lib/nomius/cli/runner.rb new file mode 100644 index 0000000..976ae89 --- /dev/null +++ b/lib/nomius/cli/runner.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require_relative "parser" +require_relative "writer/console_writer" +require_relative "writer/csv_writer" +require_relative "../bulk_checker" +require_relative "../logger" + +module Nomius + class CLI + # CLI Runner + class Runner + attr_reader :names, :silent, :input, :output + + def self.run(*args, **kwargs) + new(*args, **kwargs).run + end + + def initialize(names: [], silent: false, input: nil, output: nil) + @names = names + @silent = silent + @input = input + @output = output + end + + def run + logger = Logger.for(silent: silent) + writers = [Writer::ConsoleWriter] + writers << Writer::CSVWriter if output + + results = BulkChecker.check( + names: Parser.names(file_name: input, strings: names), + logger: logger + ) + + writers.each do |writer| + writer.write!(results: results, file_name: output) + end + end + end + end +end diff --git a/lib/nomius/cli/writer/console_writer.rb b/lib/nomius/cli/writer/console_writer.rb new file mode 100644 index 0000000..f92557c --- /dev/null +++ b/lib/nomius/cli/writer/console_writer.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require "tty-table" +require_relative "../../status/formatter/mark" + +module Nomius + class CLI + class Writer + # ConsoleWriter + class ConsoleWriter + attr_reader :results + + TTY_TABLE_WIDTH = { + "fixed" => 80, + "flexible" => nil + }.freeze + + TTY_TABLE_OPTIONS = { + padding: [0, 1], + multiline: true, + width: TTY_TABLE_WIDTH.fetch(ENV.fetch("TTY_TABLE_WIDTH", "flexible")) + }.compact.freeze + + def self.write!(*args, **kwargs) + new(*args, **kwargs).write! + end + + def initialize(results: [], **_kwargs) + @results = results + end + + def write! + table = TTY::Table.new(headers, rows) + renderer = TTY::Table::Renderer::Unicode.new(table, TTY_TABLE_OPTIONS) + + puts renderer.render + end + + private + + def headers + [ + "Name", + *results.first.results.map(&:detector).map(&:detector_short_name), + "Comment" + ] + end + + def rows + results.map do |result| + [ + result.name.name, + *result.results.map { |status| status_mark_cell(status) }, + result.name.comment + ] + end + end + + def status_mark_cell(status) + { + value: Status::Formatter::Mark.for(status), + alignment: :center + } + end + end + end + end +end diff --git a/lib/nomius/cli/writer/csv_writer.rb b/lib/nomius/cli/writer/csv_writer.rb new file mode 100644 index 0000000..fd16238 --- /dev/null +++ b/lib/nomius/cli/writer/csv_writer.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "csv" +require_relative "../../status/formatter/ascii_mark" + +module Nomius + class CLI + class Writer + # CSVWriter + class CSVWriter + attr_reader :results, :file_name + + def self.write!(*args, **kwargs) + new(*args, **kwargs).write! + end + + def initialize(file_name:, results: [], **_kwargs) + @results = results + @file_name = file_name + end + + def write! + CSV.open(file_name, "w") do |f| + f << headers + rows.each { |row| f << row } + end + end + + private + + def headers + [ + "Name", + "Comment", + *results.first.results.map(&:detector).map(&:detector_name) + ] + end + + def rows + results.map do |result| + [ + result.name.name, + result.name.comment, + *result.results.map { |status| Status::Formatter::ASCIIMark.for(status) } + ] + end + end + end + end + end +end diff --git a/lib/nomius/detector.rb b/lib/nomius/detector.rb new file mode 100644 index 0000000..5ff0690 --- /dev/null +++ b/lib/nomius/detector.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require_relative "detector/dockerhub_detector" +require_relative "detector/domain_com_detector" +require_relative "detector/domain_org_detector" +require_relative "detector/github_detector" +require_relative "detector/npmjs_detector" +require_relative "detector/pypi_detector" +require_relative "detector/rubygems_detector" + +module Nomius + # Detectors + class Detector + DETECTORS = [ + DomainComDetector, + DomainOrgDetector, + GithubDetector, + DockerhubDetector, + NpmjsDetector, + PypiDetector, + RubygemsDetector + ].freeze + + def self.all + DETECTORS + end + end +end diff --git a/lib/nomius/detector/base_domain_name_detector.rb b/lib/nomius/detector/base_domain_name_detector.rb new file mode 100644 index 0000000..69c85e9 --- /dev/null +++ b/lib/nomius/detector/base_domain_name_detector.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "resolv" +require "retriable" +require "whois" +require "whois-parser" +require_relative "../status" + +module Nomius + class Detector + # Detects domain is taken. + # Works in 2 steps: + # 1. Resolve DNS A record. + # 2. If there is no A record, check WHOIS record. + class BaseDomainNameDetector + # NOTE: OpenDNS servers (https://www.opendns.com/) + NANE_SERVERS = ["208.67.222.222", "208.67.220.220"].freeze + + STATUS_RESOLVER = { + true => Status::Available, + false => Status::Unavailable + }.freeze + + attr_reader :name, :logger + + def self.status(*args, **kwargs) + new(*args, **kwargs).status + end + + def initialize(name:, logger: Logger::Silent) + @name = name + @logger = logger + end + + def tld + ".org" + end + + def detector_name + "#{name.name}#{tld}" + end + + def detector_short_name + tld + end + + def status + status_class.new(name: name, detector: self) + end + + private + + def status_class + STATUS_RESOLVER.fetch(availabile_by_dns? && availabile_by_whois?) + rescue Whois::ConnectionError, Whois::ParserError, Timeout::Error => e + logger.log_error(message: "Can't resolve: #{full_domain_name}", detalis: e.message) + Status::Unresolved + end + + def full_domain_name + "#{name.name}#{tld}" + end + + def availabile_by_dns? + Resolv::DNS + .new(nameserver: NANE_SERVERS) + .getresources(Resolv::DNS::Name.create(full_domain_name), Resolv::DNS::Resource::IN::SOA) + .empty? + end + + def availabile_by_whois? + Retriable.retriable( + on: [Whois::ConnectionError, Timeout::Error], + tries: 6, multiplier: 2, base_interval: 1 + ) do + Whois.whois(full_domain_name).parser.available? + end + rescue Whois::ParserError => e + # NOTE: Hotfix for whois-parser broken parser for .org. + return true if e.message.include?("Domain not found") + + raise e + end + end + end +end diff --git a/lib/nomius/detector/base_url_detector.rb b/lib/nomius/detector/base_url_detector.rb new file mode 100644 index 0000000..235a8f9 --- /dev/null +++ b/lib/nomius/detector/base_url_detector.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "../status" +require_relative "util/http_requester" +module Nomius + class Detector + # Base class for detectors. + class BaseURLDetector + STATUS_RESOLVER = { + Util::HTTPRequester::NotFound => Status::Available, + Util::HTTPRequester::OK => Status::Unavailable, + Util::HTTPRequester::Unresolved => Status::Unresolved + }.freeze + + attr_reader :name, :logger + + def self.status(*args, **kwargs) + new(*args, **kwargs).status + end + + def initialize(name:, logger: Logger::Silent, http_requester: Util::HTTPRequester) + @name = name + @logger = logger + @http_requester = http_requester + end + + def detector_name + "Undefined" + end + + def detector_short_name + "Undefined" + end + + def status + STATUS_RESOLVER + .fetch(http_requester.response_status(uri: uri, logger: logger)) + .new(name: name, detector: self) + end + + private + + attr_reader :http_requester + end + end +end diff --git a/lib/nomius/detector/dockerhub_detector.rb b/lib/nomius/detector/dockerhub_detector.rb new file mode 100644 index 0000000..a4da3c0 --- /dev/null +++ b/lib/nomius/detector/dockerhub_detector.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "base_url_detector" + +module Nomius + class Detector + # Check name availability for https://hub.docker.com/ + class DockerhubDetector < BaseURLDetector + BASE_URL = "https://hub.docker.com/v2/orgs" + + def detector_name + "hub.docker.com" + end + + def detector_short_name + "Docker" + end + + private + + def uri + "#{BASE_URL}/#{name.name}" + end + end + end +end diff --git a/lib/nomius/detector/domain_com_detector.rb b/lib/nomius/detector/domain_com_detector.rb new file mode 100644 index 0000000..8be0fa5 --- /dev/null +++ b/lib/nomius/detector/domain_com_detector.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "uri" +require_relative "base_domain_name_detector" + +module Nomius + class Detector + # Check .com domain name availability + class DomainComDetector < BaseDomainNameDetector + def tld + ".com" + end + end + end +end diff --git a/lib/nomius/detector/domain_org_detector.rb b/lib/nomius/detector/domain_org_detector.rb new file mode 100644 index 0000000..408a82d --- /dev/null +++ b/lib/nomius/detector/domain_org_detector.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "uri" +require_relative "base_domain_name_detector" + +module Nomius + class Detector + # Check .org domain name availability + class DomainOrgDetector < BaseDomainNameDetector + def tld + ".org" + end + end + end +end diff --git a/lib/nomius/detector/github_detector.rb b/lib/nomius/detector/github_detector.rb new file mode 100644 index 0000000..4d6841e --- /dev/null +++ b/lib/nomius/detector/github_detector.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "base_url_detector" + +module Nomius + class Detector + # Check name availability for https://github.com/ + class GithubDetector < BaseURLDetector + BASE_URL = "https://github.com" + + def detector_name + "GitHub.com" + end + + def detector_short_name + "GH" + end + + private + + def uri + "#{BASE_URL}/#{name.name}" + end + end + end +end diff --git a/lib/nomius/detector/npmjs_detector.rb b/lib/nomius/detector/npmjs_detector.rb new file mode 100644 index 0000000..5bc3c7f --- /dev/null +++ b/lib/nomius/detector/npmjs_detector.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "base_url_detector" + +module Nomius + class Detector + # Check name availability for https://www.npmjs.com/ + class NpmjsDetector < BaseURLDetector + BASE_URL = "https://registry.npmjs.org" + + def detector_name + "NPMjs.com" + end + + def detector_short_name + "npm" + end + + private + + def uri + "#{BASE_URL}/#{name.name}" + end + end + end +end diff --git a/lib/nomius/detector/pypi_detector.rb b/lib/nomius/detector/pypi_detector.rb new file mode 100644 index 0000000..b471812 --- /dev/null +++ b/lib/nomius/detector/pypi_detector.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "base_url_detector" + +module Nomius + class Detector + # Check name availability for https://pypi.org/ + class PypiDetector < BaseURLDetector + BASE_URL = "https://pypi.org/project" + + def detector_name + "PyPi.org" + end + + def detector_short_name + "pip" + end + + private + + def uri + "#{BASE_URL}/#{name.name}" + end + end + end +end diff --git a/lib/nomius/detector/rubygems_detector.rb b/lib/nomius/detector/rubygems_detector.rb new file mode 100644 index 0000000..5e3ba31 --- /dev/null +++ b/lib/nomius/detector/rubygems_detector.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "base_url_detector" + +module Nomius + class Detector + # Check name availability for https://rubygems.org/ + class RubygemsDetector < BaseURLDetector + BASE_URL = "https://rubygems.org/api/v1/gems" + + def detector_name + "RubyGems.org" + end + + def detector_short_name + "gem" + end + + private + + def uri + "#{BASE_URL}/#{name.name}" + end + end + end +end diff --git a/lib/nomius/detector/util/http_requester.rb b/lib/nomius/detector/util/http_requester.rb new file mode 100644 index 0000000..cf9d56d --- /dev/null +++ b/lib/nomius/detector/util/http_requester.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "faraday" +require "faraday/retry" +require "faraday/follow_redirects" + +require_relative "../../version" + +module Nomius + class Detector + class Util + # Encapsulates HTTP request handling. + # Using Faraday gem. But could be easily replaced with any other HTTP client. + class HTTPRequester + # rubocop:disable Lint/EmptyClass + class OK; end + class NotFound; end + class Unresolved; end + # rubocop:enable Lint/EmptyClass + + HEADERS = { + "User-Agent" => "Nomius/#{Nomius::VERSION}" + }.freeze + + FALLBACK_STATUS = Unresolved + + RESPONSE_STATUS_RESOLVER = { + 200 => OK, + 404 => NotFound + }.freeze + + RETRY_STATUSES = [ + 400, 401, 403, 408, 409, 418, 425, 429, + 500, 502, 503, 504 + ].freeze + + RETRY_OPTIONS = { + max: 5, + interval: 1, + interval_randomness: 0.5, + backoff_factor: 2, + retry_statuses: RETRY_STATUSES + }.freeze + + attr_reader :uri, :logger + + def self.response_status(uri:, logger:) + new(uri: uri, logger: logger).response_status + end + + def initialize(uri:, logger:) + @uri = uri.to_s + @logger = logger + end + + def response_status + # HEAD request is used to avoid downloading the whole page. + response = connection.head + + unless RESPONSE_STATUS_RESOLVER.key?(response.status) + logger.log_error(message: uri, details: response.to_hash.to_json) + end + + RESPONSE_STATUS_RESOLVER.fetch(response.status, FALLBACK_STATUS) + end + + private + + def connection + @connection ||= Faraday.new(url: uri, headers: HEADERS) do |faraday| + faraday.request :retry, RETRY_OPTIONS + faraday.response :follow_redirects + end + end + end + end + end +end diff --git a/lib/nomius/logger.rb b/lib/nomius/logger.rb new file mode 100644 index 0000000..ab5d6a0 --- /dev/null +++ b/lib/nomius/logger.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative "logger/silent" +require_relative "logger/verbose" + +module Nomius + # Logger factory + class Logger + LOGGER_BY_SILENT = { + true => Silent, + false => Verbose + }.freeze + + def self.for(silent: false) + LOGGER_BY_SILENT.fetch(silent).new + end + end +end diff --git a/lib/nomius/logger/silent.rb b/lib/nomius/logger/silent.rb new file mode 100644 index 0000000..3dd9a4f --- /dev/null +++ b/lib/nomius/logger/silent.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Nomius + class Logger + # Silent logger + class Silent + def start_batch_processing(_count) + # Intentionally do nothing + end + + def batch_record_processing(_name) + # Intentionally do nothing + yield + end + + def log_detector_status + # Intentionally do nothing + yield + end + + def log_info(_message) + # Intentionally do nothing + end + + def log_error(message: "", details: "") + warn message + warn details + end + end + end +end diff --git a/lib/nomius/logger/verbose.rb b/lib/nomius/logger/verbose.rb new file mode 100644 index 0000000..01ef599 --- /dev/null +++ b/lib/nomius/logger/verbose.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "tty-progressbar" +require_relative "../status/formatter/mark" + +module Nomius + class Logger + # Verbose logger + class Verbose + def start_batch_processing(count) + @progress_bar = TTY::ProgressBar.new("[:bar] :current/:total ET::elapsed ETA::eta") do |config| + config.total = count + config.interval = 5 + end + end + + def batch_record_processing(name) + log_info("Processing #{name.name}...") + result = yield + log_info("") + progress_bar.advance + + result + end + + def log_detector_status + status = yield + log_info("#{status.detector.detector_short_name} #{Status::Formatter::Mark.for(status)}") + + status + end + + def log_info(message) + progress_bar.log(message) + end + + def log_error(message: "", details: "") + warn message + warn details + end + + private + + attr_reader :progress_bar + end + end +end diff --git a/lib/nomius/name.rb b/lib/nomius/name.rb new file mode 100644 index 0000000..a2560cf --- /dev/null +++ b/lib/nomius/name.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Nomius + # Name object + class Name + attr_reader :name, :comment + + def self.for(name) + return name if name.is_a?(self) + + new(name: name.to_s) + end + + def initialize(name:, comment: "") + @name = name.to_s.strip.downcase + @comment = comment.to_s.strip.downcase + validate! + end + + private + + def validate! + raise ArgumentError, "Name cannot be blank" if name.empty? + end + end +end diff --git a/lib/nomius/status.rb b/lib/nomius/status.rb new file mode 100644 index 0000000..9ecb246 --- /dev/null +++ b/lib/nomius/status.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative "status/available" +require_relative "status/unavailable" +require_relative "status/unresolved" diff --git a/lib/nomius/status/available.rb b/lib/nomius/status/available.rb new file mode 100644 index 0000000..899f6f0 --- /dev/null +++ b/lib/nomius/status/available.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative "base" + +module Nomius + module Status + # Status for name available. + class Available < Base + def available? + true + end + + def unavailalbe? + false + end + + def unresolved? + false + end + end + end +end diff --git a/lib/nomius/status/base.rb b/lib/nomius/status/base.rb new file mode 100644 index 0000000..135e1a9 --- /dev/null +++ b/lib/nomius/status/base.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Nomius + module Status + # Status for available domains. + class Base + attr_reader :name, :detector, :exception + + def initialize(name:, detector:, exception: nil) + @name = name + @detector = detector + @exception = exception + end + end + end +end diff --git a/lib/nomius/status/formatter/ascii_mark.rb b/lib/nomius/status/formatter/ascii_mark.rb new file mode 100644 index 0000000..eabaf05 --- /dev/null +++ b/lib/nomius/status/formatter/ascii_mark.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "../available" +require_relative "../unavailable" +require_relative "../unresolved" + +module Nomius + module Status + module Formatter + # Generate status text for Status + class ASCIIMark + STATUS_MAPPER = { + Status::Available => "+", + Status::Unavailable => "-", + Status::Unresolved => "?" + }.freeze + + def self.for(status) + STATUS_MAPPER.fetch(status.class) + end + end + end + end +end diff --git a/lib/nomius/status/formatter/mark.rb b/lib/nomius/status/formatter/mark.rb new file mode 100644 index 0000000..0911649 --- /dev/null +++ b/lib/nomius/status/formatter/mark.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "../available" +require_relative "../unavailable" +require_relative "../unresolved" + +module Nomius + module Status + module Formatter + # Generate status mark for Status + class Mark + STATUS_MAPPER = { + Status::Available => "✅", + Status::Unavailable => "❌", + Status::Unresolved => "❓" + }.freeze + + def self.for(status) + STATUS_MAPPER.fetch(status.class) + end + end + end + end +end diff --git a/lib/nomius/status/unavailable.rb b/lib/nomius/status/unavailable.rb new file mode 100644 index 0000000..1f27b61 --- /dev/null +++ b/lib/nomius/status/unavailable.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative "base" + +module Nomius + module Status + # Status for name unavailable. + class Unavailable < Base + def available? + false + end + + def unavailalbe? + true + end + + def unresolved? + false + end + end + end +end diff --git a/lib/nomius/status/unresolved.rb b/lib/nomius/status/unresolved.rb new file mode 100644 index 0000000..f88498b --- /dev/null +++ b/lib/nomius/status/unresolved.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative "base" + +module Nomius + module Status + # Status for name availability unresolved. Should be checked manually. + class Unresolved < Base + def available? + false + end + + def unavailalbe? + false + end + + def unresolved? + true + end + end + end +end diff --git a/lib/nomius/version.rb b/lib/nomius/version.rb new file mode 100644 index 0000000..53091a8 --- /dev/null +++ b/lib/nomius/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Nomius + VERSION = "0.1.0" +end diff --git a/nomius.gemspec b/nomius.gemspec new file mode 100644 index 0000000..392f217 --- /dev/null +++ b/nomius.gemspec @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "lib/nomius/version" + +Gem::Specification.new do |spec| + spec.name = "nomius" + spec.version = Nomius::VERSION + spec.authors = ["Oleksii Leonov"] + spec.email = ["oleksii.leonov@syngenta.com", "ospo@syngenta.com"] + + spec.summary = "Nomius — bulk domain & package name availability checker" + spec.description = "Bulk domain & package name availability checker" + spec.homepage = "https://github.com/syngenta/nomius" + spec.license = "MIT" + spec.required_ruby_version = ">= 2.6.0" + + spec.metadata = { + "homepage_uri" => spec.homepage, + "changelog_uri" => "https://github.com/syngenta/nomius/blob/main/CHANGELOG.md", + "source_code_uri" => spec.homepage, + "documentation_uri" => spec.homepage, + "bug_tracker_uri" => "https://github.com/syngenta/nomius/issues", + "rubygems_mfa_required" => "true" + } + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0").reject do |f| + (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ spec/ .git .github .devcontainer]) + end + end + spec.bindir = "exe" + spec.executables = ["nomius"] + spec.extra_rdoc_files = ["LICENSE", "README.md"] + + spec.add_dependency "faraday", "< 3" + spec.add_dependency "faraday-follow_redirects", "< 1" + spec.add_dependency "faraday-retry", "< 3" + spec.add_dependency "retriable", "< 4" + spec.add_dependency "tty-option", "< 1" + spec.add_dependency "tty-progressbar", "< 1" + spec.add_dependency "tty-table", "< 1" + spec.add_dependency "whois", "< 6" + spec.add_dependency "whois-parser", "< 3" +end diff --git a/spec/nomius/cli/command_spec.rb b/spec/nomius/cli/command_spec.rb new file mode 100644 index 0000000..17422e2 --- /dev/null +++ b/spec/nomius/cli/command_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require_relative "../../../lib/nomius/cli/command" + +RSpec.describe Nomius::CLI::Command do + let(:command) { described_class.new.parse(args) } + + describe "fast exit" do + context "when no arguments" do + let(:args) { [] } + + it "prints help and exit" do + expect do + expect { command.run }.to output(/Usage: nomius/).to_stdout + end.to raise_error(SystemExit) + end + end + + context "when no --input or names" do + let(:args) { %w[--silent --output test.csv] } + + it "prints help and exit" do + expect do + expect { command.run }.to output(/Usage: nomius/).to_stdout + end.to raise_error(SystemExit) + end + end + + context "when --help argument" do + let(:args) { %w[--help] } + + it "prints help and exit" do + expect do + expect { command.run }.to output(/Usage: nomius/).to_stdout + end.to raise_error(SystemExit) + end + end + + context "when --version argument" do + let(:args) { %w[--version] } + + it "prints version and exit" do + expect do + expect { command.run }.to output(Nomius::VERSION).to_stdout + end.to raise_error(SystemExit) + end + end + end + + describe "when parsing params" do + subject(:params) do + command.run + command.params + end + + context "when silent" do + let(:args) { %w[--silent testname1] } + + it { expect(params[:silent]).to be(true) } + end + + context "when not silent" do + let(:args) { %w[testname1] } + + it { expect(params[:silent]).to be(false) } + end + + context "when one name" do + let(:args) { %w[testname1] } + + it { expect(params[:names]).to eq(%w[testname1]) } + it { expect(params[:input]).to be_nil } + end + + context "when several names" do + let(:args) { %w[testname1 testname2] } + + it { expect(params[:names]).to eq(%w[testname1 testname2]) } + it { expect(params[:input]).to be_nil } + end + + context "when --input file" do + let(:args) { %w[--input file.txt] } + + it { expect(params[:input]).to eq("file.txt") } + it { expect(params[:names]).to eq([]) } + end + + context "when --output file" do + let(:args) { %w[--output file.txt testname1] } + + it { expect(params[:output]).to eq("file.txt") } + end + end +end diff --git a/spec/nomius/cli_spec.rb b/spec/nomius/cli_spec.rb new file mode 100644 index 0000000..26e417c --- /dev/null +++ b/spec/nomius/cli_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "../../lib/nomius/cli" + +RSpec.describe Nomius::CLI do + describe "#run" do + it "parse arguments and call Runner" do + allow(Nomius::CLI::Runner).to receive(:run) + + described_class.run(%w[--silent --input i.txt --output o.txt name1 name2]) + + expect(Nomius::CLI::Runner).to have_received(:run).with( + names: %w[name1 name2], + silent: true, + input: "i.txt", + output: "o.txt" + ) + end + end +end diff --git a/spec/nomius_spec.rb b/spec/nomius_spec.rb new file mode 100644 index 0000000..755a3a6 --- /dev/null +++ b/spec/nomius_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.describe Nomius do + it "has a version number" do + expect(Nomius::VERSION).not_to be_nil + end +end diff --git a/spec/sample_data/expected_help_output.txt b/spec/sample_data/expected_help_output.txt new file mode 100644 index 0000000..179e96c --- /dev/null +++ b/spec/sample_data/expected_help_output.txt @@ -0,0 +1,32 @@ +Usage: nomius [OPTIONS] [NAMES...] + +Nomius — bulk domain & package name availability checker. + +Options: + -h, --help Print usage + -i, --input string Input file. Could be: + - TXT with each name on a separate line; + - CSV file with 2 columns: "name","comment" ("comment" + is optional). + + -o, --output string Output CSV file + -s, --silent Print less output + --version Print version + +Examples: + Basic usage + Check "firstname" and "othername" names. + $ nomius firstname othername + + Usage with a TXT file + $ nomius --input names.txt + or + $ cat names.txt | nomius + or + $ nomius < names.txt + + Usage with a CSV file + $ nomius --input names.csv + + Usage with a CSV file and output to a CSV file + $ nomius --input names.csv --output results.csv diff --git a/spec/sample_data/system/console_output.txt b/spec/sample_data/system/console_output.txt new file mode 100644 index 0000000..0e7b728 --- /dev/null +++ b/spec/sample_data/system/console_output.txt @@ -0,0 +1,6 @@ +┌──────────┬──────┬──────┬────┬────────┬─────┬─────┬─────┬────────────┐ +│ Name │ .com │ .org │ GH │ Docker │ npm │ pip │ gem │ Comment │ +├──────────┼──────┼──────┼────┼────────┼─────┼─────┼─────┼────────────┤ +│ numpy │ ❌ │ ❌ │ ❌ │ ❌ │ ❌ │ ❌ │ ❌ │ registered │ +│ y5207z5a │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ available │ +└──────────┴──────┴──────┴────┴────────┴─────┴─────┴─────┴────────────┘ diff --git a/spec/sample_data/system/input.csv b/spec/sample_data/system/input.csv new file mode 100644 index 0000000..fe2bfce --- /dev/null +++ b/spec/sample_data/system/input.csv @@ -0,0 +1,2 @@ +numpy,registered +y5207z5a,available diff --git a/spec/sample_data/system/output.csv b/spec/sample_data/system/output.csv new file mode 100644 index 0000000..0a86ee7 --- /dev/null +++ b/spec/sample_data/system/output.csv @@ -0,0 +1,3 @@ +Name,Comment,numpy.com,numpy.org,GitHub.com,hub.docker.com,NPMjs.com,PyPi.org,RubyGems.org +numpy,registered,-,-,-,-,-,-,- +y5207z5a,available,+,+,+,+,+,+,+ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..a31802c --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require "nomius" + +RSpec.configure do |config| + # Fail on deprecations + config.raise_errors_for_deprecations! + + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # Force recommeded expect syntax + expectations.syntax = :expect + + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Force recommeded expect syntax + mocks.syntax = :expect + + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = ".rspec_status" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +end diff --git a/spec/system/nomius_cli_spec.rb b/spec/system/nomius_cli_spec.rb new file mode 100644 index 0000000..c7ea7d5 --- /dev/null +++ b/spec/system/nomius_cli_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "English" +require "tempfile" + +RSpec.describe "nomius command" do + context "without arguments" do + let(:command) { "bundle exec nomius" } + let(:expected_console_output) { File.read("spec/sample_data/expected_help_output.txt") } + + it "exit nomius with 1 and show help" do + results = `#{command}`.gsub(/ +$/, "") + + expect($CHILD_STATUS.exitstatus).to eq(1) + expect(results).to eq(expected_console_output) + end + end + + context "with --help flag" do + let(:command) { "bundle exec nomius --help" } + let(:expected_console_output) { File.read("spec/sample_data/expected_help_output.txt") } + + it "exit nomius with 0 and show help" do + results = `#{command}`.gsub(/ +$/, "") + + expect($CHILD_STATUS.exitstatus).to eq(0) + expect(results).to eq(expected_console_output) + end + end + + context "with input and output to CSV" do + let(:input_path) { "spec/sample_data/system/input.csv" } + let(:output) { Tempfile.new("output.csv") } + let(:command) do + "bundle exec nomius --input #{input_path} --output #{output.path}" + end + let(:expected_csv_output) { File.read("spec/sample_data/system/output.csv") } + let(:expected_console_output) { File.read("spec/sample_data/system/console_output.txt") } + + # NOTE: Nomius intentionally makes requests to external services during this spec. + # It's the one integration spec for complete end-to-end testing. Keeping it as is for now. + # Later, we could use some proxy to stub HTTP & WHOIS requests. + it "runs nomius and print output" do + # Force tty-table to use full-width table. + allow(TTY::Screen).to receive(:width).and_return(1000) + + results = `#{command}` + + expect($CHILD_STATUS.exitstatus).to eq(0) + expect(results).to eq(expected_console_output) + expect(output.read).to eq(expected_csv_output) + end + end +end