diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..36bd035 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# see http://editorconfig.org/ + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a45c387 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +UK_CARD_EXTERNAL_ID='ABCDEFGHIJKLM123' + +UK_REST_API_USERNAME='username' +UK_REST_API_PASSWORD='password' +UK_REST_API_MERCHANT_UNIQUE_TAG='tag' +UK_REST_API_PROGRAM_UNIQUE_TAG='tag' diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..4717517 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +coverage +Gemfile.lock +spec/vcr_cassettes diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..8bb8919 --- /dev/null +++ b/.rspec @@ -0,0 +1,5 @@ +--color +--fail-fast +--format documentation +--profile +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..058272f --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,74 @@ +require: + - rubocop-performance + - rubocop-rspec + +AllCops: + TargetRubyVersion: 2.6.4 + +Layout/AlignParameters: + EnforcedStyle: with_fixed_indentation + +Layout/DotPosition: + EnforcedStyle: trailing + +Layout/EndAlignment: + AutoCorrect: true + EnforcedStyleAlignWith: variable + +Layout/IndentFirstArrayElement: + EnforcedStyle: consistent + +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented + +Layout/MultilineOperationIndentation: + EnforcedStyle: indented + +Lint/AmbiguousBlockAssociation: + Exclude: + - spec/**/* + +RSpec/BeforeAfterAll: + Enabled: false + +RSpec/ExampleLength: + Max: 10 + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/NestedGroups: + Max: 4 + +RSpec/FilePath: + Enabled: false + +Security/Eval: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/GuardClause: + Enabled: false + +Style/LambdaCall: + Enabled: false + +Style/NegatedIf: + Enabled: false + +Style/Send: + Enabled: true + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: consistent_comma diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6b0313b --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +# Declare your dependencies in eml.gemspec +gemspec diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..52ff9b1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2019- Morning Coffee Pty Ltd (https://morningcoffee.com.au) + +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 100755 index 0000000..3d7b7f0 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# EML API integration library for Ruby + +The EML API integration library provides convenient access to EML's REST Web +Services and Transaction Notification Service for applications written in +the Ruby language. + +Version 1 supports the UK APIs with Australian support coming as existing code +is extracted. + +## Installation + +You don't need this source code unless you want to modify the gem. If you just +want to use the package, just include the following in your Gemfile: + +```sh +gem "eml", github: "MorningCoffeeDev/eml_ruby" +``` + +If you are debugging or developing this gem and wish to use it within the +context of an existing application, modify your Gemfile to read: + +```sh +gem "eml", path: "../path_to_gem" +``` + +### Requirements + +- Ruby 2.6+ (untested in prior versions) + +## Usage + +Each geographical region is a separate module and will need to be configured +with your supplied credentials. + +```ruby +require "eml" + +EML::UK.configure do |config| + config.username = "username" + config.password = "password" + config.merchant = "merchant_id" + config.program = "program_id" +end +``` + +Make sure you never commit your credentials to git. If you are using Ruby on +Rails, it is usually best to keep your secrets in your +[credentials file]: https://edgeguides.rubyonrails.org/security.html#custom-credentials + +## Development + +Since it is necessary to authenticate with EML to test API resources, +credentials and a test card ID are required. The dotenv gem is loaded when tests +are run and will look for a .env file in the root directory. A .env.example file +has been supplied so you copy it to .env and replace the example values: + +```sh +cp .env.example .env +``` + +The .env file has been added to the .gitignore list and should never been +commited to the repository. + +Run all tests: + +```sh +bundle exec rspec +``` + +Run a single test suite: + +```sh +bundle exec rspec spec/path/to/file.rb +``` + +Run a single test, include the line number: + +```sh +bundle exec rspec spec/path/to/file.rb:123 +``` + +Please ensure that all changes have been run by the Rubocop before creating a +pull request: + +```sh +bundle exec rubocop +``` diff --git a/eml.gemspec b/eml.gemspec new file mode 100644 index 0000000..6f324b1 --- /dev/null +++ b/eml.gemspec @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(::File.join(::File.dirname(__FILE__), "lib")) + +require "eml/version" + +Gem::Specification.new do |s| + s.name = "eml" + s.version = EML::VERSION + s.required_ruby_version = ">= 2.6.0" + s.summary = "Ruby bindings for the EML API" + s.description = "Connect to the EML payments APIs and " \ + "Transaction Notification Serices" + s.author = "Morning Coffee" + s.email = "developers@morningcoffee.com.au" + s.homepage = "https://github.com/MorningCoffeeDev/eml_ruby" + s.license = "MIT" + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- test/*`.split("\n") + s.require_paths = ["lib"] + + s.add_dependency "http", "~> 4.0.0" + + s.add_development_dependency "rspec", "~> 3.8" + s.add_development_dependency "rubocop", "~> 0.71" + s.add_development_dependency "rubocop-performance" + s.add_development_dependency "rubocop-rspec" + s.add_development_dependency "simplecov" + s.add_development_dependency "vcr" +end diff --git a/lib/eml.rb b/lib/eml.rb new file mode 100644 index 0000000..af77360 --- /dev/null +++ b/lib/eml.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require "http" + +require "eml/version" +require "eml/api_operations" +require "eml/uk" + +module EML +end diff --git a/lib/eml/api_operation/create.rb b/lib/eml/api_operation/create.rb new file mode 100644 index 0000000..018ed5d --- /dev/null +++ b/lib/eml/api_operation/create.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module EML + module APIOperation + module Create + def create(params = {}) + request(:post, resource_url, params) + end + end + end +end diff --git a/lib/eml/api_operation/list.rb b/lib/eml/api_operation/list.rb new file mode 100644 index 0000000..ee6b745 --- /dev/null +++ b/lib/eml/api_operation/list.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module EML + module APIOperation + module List + def list(filters = {}) + request(:get, resource_url, filters) + end + end + end +end diff --git a/lib/eml/api_operation/request.rb b/lib/eml/api_operation/request.rb new file mode 100644 index 0000000..d6da5d9 --- /dev/null +++ b/lib/eml/api_operation/request.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module EML + module APIOperation + module Request + module ClassMethods + def request(method, url, params = {}) + end + end + + def self.included(base) + base.extend(ClassMethods) + end + end + end +end diff --git a/lib/eml/api_operations.rb b/lib/eml/api_operations.rb new file mode 100644 index 0000000..2c89792 --- /dev/null +++ b/lib/eml/api_operations.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require "eml/api_operation/create" +require "eml/api_operation/request" diff --git a/lib/eml/error.rb b/lib/eml/error.rb new file mode 100644 index 0000000..d52189e --- /dev/null +++ b/lib/eml/error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "eml/error/rest" + +module EML + # Error is the base class for more specific EML errors + class Error + attr_reader :message + + def initialize(message = nil) + @message = message + end + end +end diff --git a/lib/eml/error/rest/authentication.rb b/lib/eml/error/rest/authentication.rb new file mode 100644 index 0000000..9daa27e --- /dev/null +++ b/lib/eml/error/rest/authentication.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module EML + class AuthenticationError < Error + end +end diff --git a/lib/eml/uk.rb b/lib/eml/uk.rb new file mode 100644 index 0000000..a6c20c3 --- /dev/null +++ b/lib/eml/uk.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require "eml/uk/api_resource" +require "eml/uk/config" +require "eml/uk/resources" + +module EML + module UK + end +end diff --git a/lib/eml/uk/api_resource.rb b/lib/eml/uk/api_resource.rb new file mode 100644 index 0000000..e50e59e --- /dev/null +++ b/lib/eml/uk/api_resource.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module EML + class APIResource + def resource_url + if id.present? + "#{self::ENDPOINT_BASE}/#{id}" + else + self::ENDPOINT_BASE + end + end + end +end diff --git a/lib/eml/uk/config.rb b/lib/eml/uk/config.rb new file mode 100644 index 0000000..f39623f --- /dev/null +++ b/lib/eml/uk/config.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module EML + module UK + class << self + def configure + yield config + end + + def config + @config ||= Config.new + end + end + + class Config + attr_accessor :username + attr_accessor :password + attr_accessor :merchant + attr_accessor :program + end + end +end diff --git a/lib/eml/uk/resources.rb b/lib/eml/uk/resources.rb new file mode 100644 index 0000000..b6bcf01 --- /dev/null +++ b/lib/eml/uk/resources.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "eml/uk/resources/card" diff --git a/lib/eml/version.rb b/lib/eml/version.rb new file mode 100644 index 0000000..2a9dfd1 --- /dev/null +++ b/lib/eml/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module EML + VERSION = "0.0.1" +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..f3b0162 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,118 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +require "simplecov" +SimpleCov.start do + add_filter "/spec/" + + add_group( + "API Operations", %w[lib/eml/api_operations.rb lib/eml/api_operation/] + ) + add_group "UK", %w[lib/eml/uk.rb lib/eml/uk/] +end + +require "eml" + +RSpec.configure do |config| + # 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| + # 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| + # 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. +=begin + # 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 = "spec/examples.txt" + + # 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! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # 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 + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # 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 +end + +require "vcr" +VCR.configure do |c| + c.cassette_library_dir = "spec/vcr_cassettes" +end diff --git a/spec/vcr_cassettes/.keep b/spec/vcr_cassettes/.keep new file mode 100644 index 0000000..e69de29