diff --git a/CHANGELOG.md b/CHANGELOG.md index a59ef39..b68ac0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,19 @@ All notable changes will be documented in this file. +## 1.2.0 - 2024-10-08 + +- (AlexT) allow passing endpoint specifications directory in initializer + Bump deps including minitest 5.15.0 -> 5.25.1, mocha 1.13.0 -> 2.4.5 and + minitest-reporters 1.5.0 -> 1.7.1. + ## 1.1.4 - 2024-10-08 - (Dan) Upgrades ruby version to 2.7.8 and version cadence to 1.1.4 ## 1.1.3 - 2024-06-03 -- (AlexT) require 'ostruct' to fix tests in Ruby 3.3.1. Use Matrix tests to check +- (AlexT) require 'ostruct' to fix tests in Ruby 3.3.1. Use Matrix tests to check multiple Ruby versions, 2.7 to 3.3. Bump deps: - rexml 3.2.5 -> 3.2.8 fixing CVE-2024-35176 - concurrent-ruby 1.1.9 -> 1.3.1 diff --git a/Gemfile.lock b/Gemfile.lock index 3338a5a..84bef53 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - sapi-client-ruby (1.1.4) + sapi-client-ruby (1.2.0) faraday_middleware (~> 1.0.0) i18n (~> 1.5) @@ -10,7 +10,7 @@ GEM specs: ansi (1.5.0) ast (2.4.2) - builder (3.2.4) + builder (3.3.0) byebug (11.1.3) concurrent-ruby (1.3.4) docile (1.4.0) @@ -41,13 +41,14 @@ GEM faraday (~> 1.0) i18n (1.14.6) concurrent-ruby (~> 1.0) - minitest (5.15.0) - minitest-reporters (1.5.0) + minitest (5.25.1) + minitest-reporters (1.7.1) ansi builder minitest (>= 5.0) ruby-progressbar - mocha (1.13.0) + mocha (2.4.5) + ruby2_keywords (>= 0.0.5) multipart-post (2.4.1) parallel (1.21.0) parser (3.1.1.0) @@ -86,9 +87,9 @@ PLATFORMS DEPENDENCIES bundler (~> 2.1.4) byebug (~> 11.1.3) - minitest (~> 5.0) - minitest-reporters (~> 1.5.0) - mocha (~> 1.13.0) + minitest (~> 5.25) + minitest-reporters (~> 1.7) + mocha (~> 2.4) rake (~> 13.0.1) rubocop (~> 1.26.0) sapi-client-ruby! diff --git a/README.md b/README.md index 9db3921..8408368 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ modelspec for the API, which is used to generate a custom API class using Ruby metaprogramming _N.B. This respository's primary branch name has been updated, please see the -[#important](#important) section below for more information._ +[Main branch](#main-branch) section below for more information._ ## Usage @@ -19,7 +19,7 @@ application's `Gemfile`: ```ruby source "https://rubygems.pkg.github.com/epimorphics" do - gem "sapi-client-ruby", "~> 1.0.0" + gem "sapi-client-ruby", "~> 1.2" end ``` @@ -51,16 +51,22 @@ about creating a PAT. To aid debugging and exploring a Sapi-NT endpoint, this library has a command-line tool `sapi`. As required inputs, the tool needs both the base URL for the Sapi-NT API instance (e.g. `http://localhost:8080`), and the location of -the Sapi-NT configuration root file. These can either be passed as command-line -arguments, or as environment variables: +the Sapi-NT modelspec files (see [Modelspec files](#modelspec-files) below). +These can either be passed as command-line arguments, or as environment variables: ```sh sapi -b http://localhost:8080 -s test/fixtures/unified-view/application.yaml inspect - +``` +or +```sh export SAPI_BASE_URL=http://localhost:8080 export SAPI_SPEC_FILE=test/fixtures/unified-view/application.yaml sapi inspect ``` +or, using the endpoint specs directory +```sh +sapi -b http://localhost:8080 -s test/fixtures/unified-view/endpointSpecs inspect +``` See `sapi --help` for more details. @@ -241,7 +247,9 @@ $ sapi establishment_item MBTM1R-A8K4VZ-2FJCYJ -j ### Using Sapi-client from code Create a new instance of the `SapiClient::Application`, initialised with the -base URL and the location of the root YAML file for the application: +base URL and either the location of the root YAML file for the application, or +the directory containing the endpoint specification YAML files (see [Modelspec +files](#modelspec-files) below). ```ruby irb(main):001:0> app = SapiClient::Application.new('http://localhost:8080', 'test/fixtures/unified-view/application.yaml') @@ -286,7 +294,7 @@ resources. To associate the values of an endpoint with a facade class, there are a number of options: -- a class may be be passed via the `wrapper` option when invoking an API +- a class may be passed via the `wrapper` option when invoking an API endpoint method. E.g: `myEndpoint.establishment_list(_limit: 1, wrapper: MyClass)` - if no explicit `wrapper option is available`, the endpoint will look for a @@ -349,7 +357,22 @@ The events emitted are: ## Developer notes -### Important +### Modelspec files + +Both `sapi-nt` and `sapi-client-ruby` are configured using a set of "modelspec" +files that detail the endpoint URL templates, arguments and responses of a given +API. + +`sapi-nt` uses an `application.yaml` configuration file that, amongst other things, +points to the location of the directory of these modelspec (YAML) files, although +often these are resources in a JAR file, so use Java's classpath machinery. + +As a step away from too closely coupling `sapi-nt` and `sapi-client-ruby`, we now +additionally allow initialization of the `SapiClient::Application` using the +location of the directory containing the modelspec files, usually called +`endpointSpecs`. + +### Main branch If you have already cloned the repository to your local instance, you will need to run the following commands to update the primary branch name: diff --git a/exe/sapi b/exe/sapi index b6c0182..e6fc03e 100755 --- a/exe/sapi +++ b/exe/sapi @@ -7,12 +7,12 @@ require 'sapi_client' options = { base: ENV['SAPI_BASE_URL'], - spec: ENV['SAPI_SPEC_FILE'] + spec: ENV['SAPI_SPEC_FILE'] || ENV['SAPI_SPEC'] } -def spec_file(options) +def spec_file_or_dir(options) unless File.exist?(options[:spec]) - puts "Could not find spec file #{options[:spec]}" + puts "Could not find spec file/directory #{options[:spec]}" exit(1) end @@ -22,13 +22,13 @@ end def usage <<~USAGE Usage: - sapi [-b SAPI_BASE_URL] [-s SAPI_SPEC_FILE] inspect - sapi [-b SAPI_BASE_URL] [-s SAPI_SPEC_FILE] + sapi [-b SAPI_BASE_URL] [-s SAPI_SPEC] inspect + sapi [-b SAPI_BASE_URL] [-s SAPI_SPEC] Full list of options: sapi -h - Base URL and spec file can also be set as environment variables. + Base URL and spec file/dir can also be set as environment variables. v#{SapiClient::VERSION} USAGE @@ -49,7 +49,7 @@ end def sapi_application(options) SapiClient::Application - .new(options.delete(:base), spec_file(options)) + .new(options.delete(:base), spec_file_or_dir(options)) end def inspect_sapi(options) @@ -90,7 +90,8 @@ OptionParser.new do |parser| options[:base] = url end - parser.on('-s', '--spec-file SAPI_SPEC_FILE', 'The location of the specification file') do |file_name| + parser.on('-s', '--spec-file SAPI_SPEC', + 'The directory with specification files, or location of sapi-nt application.yaml') do |file_name| options[:spec] = file_name end diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index 9cfaaf3..f9b62f1 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -5,14 +5,17 @@ module SapiClient # enclosed endpoint specifications to perform various operations, such as creating # methods we can call class Application - def initialize(base_url, application_spec) - unless File.exist?(application_spec) - raise(SapiError, "Could not find application spec #{application_spec}") + def initialize(base_url, application_or_endpoints) + unless File.exist?(application_or_endpoints) + raise(SapiError, "Could not find spec file/directory #{application_or_endpoints}") end @base_url = base_url - @application_spec_file = application_spec - @specification = YAML.load_file(application_spec) + @application_spec_file = File.file?(application_or_endpoints) ? application_or_endpoints : nil + @endpoints_path = File.directory?(application_or_endpoints) ? application_or_endpoints : nil + @specification = (@application_spec_file && YAML.load_file(application_or_endpoints)) || { + 'sapi-nt' => { 'config' => { 'loadSpecPath' => 'classpath:endpointSpecs' } } + } end attr_reader :base_url, :specification @@ -30,11 +33,15 @@ def application_spec_dir end def load_spec_path - configuration['loadSpecPath'].sub(/^classpath:/, '') + @endpoints_path || configuration['loadSpecPath'].sub(/^classpath:/, '') end def endpoint_group_files - Dir["#{application_spec_dir}/#{load_spec_path}/*.yaml"] + if @endpoints_path.nil? + Dir["#{application_spec_dir}/#{load_spec_path}/*.yaml"] + else + Dir["#{@endpoints_path}/*.yaml"] + end end def endpoints diff --git a/lib/sapi_client/version.rb b/lib/sapi_client/version.rb index f0cf07b..2bb4d1a 100644 --- a/lib/sapi_client/version.rb +++ b/lib/sapi_client/version.rb @@ -2,7 +2,7 @@ module SapiClient MAJOR = 1 - MINOR = 1 - FIX = 4 + MINOR = 2 + FIX = 0 VERSION = "#{MAJOR}.#{MINOR}.#{FIX}" end diff --git a/sapi-client-ruby.gemspec b/sapi-client-ruby.gemspec index 0006b1e..4f9fe27 100644 --- a/sapi-client-ruby.gemspec +++ b/sapi-client-ruby.gemspec @@ -36,9 +36,9 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 2.1.4' spec.add_development_dependency 'byebug', '~> 11.1.3' - spec.add_development_dependency 'minitest', '~> 5.0' - spec.add_development_dependency 'minitest-reporters', '~> 1.5.0' - spec.add_development_dependency 'mocha', '~> 1.13.0' + spec.add_development_dependency 'minitest', '~> 5.25' + spec.add_development_dependency 'minitest-reporters', '~> 1.7' + spec.add_development_dependency 'mocha', '~> 2.4' spec.add_development_dependency 'rake', '~> 13.0.1' spec.add_development_dependency 'rubocop', '~> 1.26.0' spec.add_development_dependency 'simplecov', '~> 0.21.1' diff --git a/test/sapi_client/application_test.rb b/test/sapi_client/application_test.rb index 450096a..31e9f75 100644 --- a/test/sapi_client/application_test.rb +++ b/test/sapi_client/application_test.rb @@ -6,90 +6,97 @@ module SapiClient class ApplicationTest < Minitest::Test describe 'Application' do - let(:spec) { 'test/fixtures/unified-view/application.yaml' } - let(:base_url) { "http://localhost:#{sapi_api_port}" } + [ + 'test/fixtures/unified-view/application.yaml', + 'test/fixtures/unified-view/endpointSpecs' + ].each do |spec_file_or_dir| + let(:spec) { spec_file_or_dir } + let(:base_url) { "http://localhost:#{sapi_api_port}" } - describe '#initialize' do - it 'should load the application specification given' do - app = SapiClient::Application.new(base_url, spec) - _(app.specification).must_be_kind_of Hash - end - - it 'should raise an error if the spec file does not exist' do - _( - -> { SapiClient::Application.new(base_url, 'wimbledon/wombles.yaml') } - ).must_raise(SapiError) - end - - it 'should store the base URL' do - app = SapiClient::Application.new(base_url, spec) - _(app.base_url).must_equal base_url - end - end + describe "with #{spec_file_or_dir}" do + describe '#initialize' do + it 'should load the application specification given' do + app = SapiClient::Application.new(base_url, spec) + _(app.specification).must_be_kind_of Hash + end - describe '#configuration' do - it 'should report the application configuration' do - app = SapiClient::Application.new(base_url, spec) - _(app.configuration).must_be_kind_of Hash - assert app.configuration.key?('loadSpecPath') - end - end + it 'should raise an error if the spec file does not exist' do + _( + -> { SapiClient::Application.new(base_url, 'wimbledon/wombles.yaml') } + ).must_raise(SapiError) + end - describe '#endpoint_group_files' do - it 'should return the names of all of the endpoint specification files' do - app = SapiClient::Application.new(base_url, spec) - file_names = app.endpoint_group_files - _(file_names.length).must_be :>, 5 - _(file_names).must_include('test/fixtures/unified-view/endpointSpecs/establishment.yaml') - end - end + it 'should store the base URL' do + app = SapiClient::Application.new(base_url, spec) + _(app.base_url).must_equal base_url + end + end - describe '#endpoints' do - it 'should return a list of all of the endpoint specification objects' do - app = SapiClient::Application.new(base_url, spec) - eps = app.endpoints - _(eps).must_be_kind_of Array - _(eps.length).must_be :>, 5 + describe '#configuration' do + it 'should report the application configuration' do + app = SapiClient::Application.new(base_url, spec) + _(app.configuration).must_be_kind_of Hash + assert app.configuration.key?('loadSpecPath') + end + end - _(eps.map(&:raw_path)).must_include '/business/id/establishment' - end - end + describe '#endpoint_group_files' do + it 'should return the names of all of the endpoint specification files' do + app = SapiClient::Application.new(base_url, spec) + file_names = app.endpoint_group_files + _(file_names.length).must_be :>, 5 + _(file_names).must_include('test/fixtures/unified-view/endpointSpecs/establishment.yaml') + end + end - describe '#instance' do - it 'should create an instance with methods corresponding to endpoints' do - app = SapiClient::Application.new(base_url, spec) - inst = app.instance - methods = inst.public_methods - _(methods).must_include(:establishment_list) - _(methods).must_include(:establishment_list_spec) - end + describe '#endpoints' do + it 'should return a list of all of the endpoint specification objects' do + app = SapiClient::Application.new(base_url, spec) + eps = app.endpoints + _(eps).must_be_kind_of Array + _(eps.length).must_be :>, 5 - it 'should wrap a list of instances' do - class ::Establishment # rubocop:disable Lint/ConstantDefinitionInBlock - def initialize(_json) - @invoked = true + _(eps.map(&:raw_path)).must_include '/business/id/establishment' end - attr_reader :invoked end - app = SapiClient::Application.new(base_url, spec) - inst = app.instance + describe '#instance' do + it 'should create an instance with methods corresponding to endpoints' do + app = SapiClient::Application.new(base_url, spec) + inst = app.instance + methods = inst.public_methods + _(methods).must_include(:establishment_list) + _(methods).must_include(:establishment_list_spec) + end - VCR.use_cassette('application.test_instance_wrapping') do - establishments = inst.establishment_list(_limit: 1) - _(establishments.first).must_be_kind_of(Establishment) - assert establishments.first.invoked - end - end + it 'should wrap a list of instances' do + class ::Establishment # rubocop:disable Lint/ConstantDefinitionInBlock + def initialize(_json) + @invoked = true + end + attr_reader :invoked + end + + app = SapiClient::Application.new(base_url, spec) + inst = app.instance - it 'should retrieve a hierarchy' do - VCR.use_cassette('application.test_hierarchy') do - app = SapiClient::Application.new( - 'http://fsa-rp-test.epimorphics.net', - 'test/fixtures/regulated-products/application.yaml' - ) - hierarchy = app.instance.feed_category_hierarchy_hierarchy({}, :skos) - _(hierarchy.roots.size).must_equal(5) + VCR.use_cassette('application.test_instance_wrapping') do + establishments = inst.establishment_list(_limit: 1) + _(establishments.first).must_be_kind_of(Establishment) + assert establishments.first.invoked + end + end + + it 'should retrieve a hierarchy' do + VCR.use_cassette('application.test_hierarchy') do + app = SapiClient::Application.new( + 'http://fsa-rp-test.epimorphics.net', + 'test/fixtures/regulated-products/application.yaml' + ) + hierarchy = app.instance.feed_category_hierarchy_hierarchy({}, :skos) + _(hierarchy.roots.size).must_equal(5) + end + end end end end diff --git a/test/sapi_client/endpoint_values_test.rb b/test/sapi_client/endpoint_values_test.rb index 5d394e3..5ae5a31 100644 --- a/test/sapi_client/endpoint_values_test.rb +++ b/test/sapi_client/endpoint_values_test.rb @@ -46,19 +46,26 @@ class EndpointValuesTest < Minitest::Test end describe '#to_a' do - let(:spec) { 'test/fixtures/unified-view/application.yaml' } - let(:base_url) { "http://localhost:#{sapi_api_port}" } + [ + 'test/fixtures/unified-view/application.yaml', + 'test/fixtures/unified-view/endpointSpecs' + ].each do |spec_file_or_dir| + let(:spec) { 'test/fixtures/unified-view/application.yaml' } + let(:base_url) { "http://localhost:#{sapi_api_port}" } - it 'should invoke the endpoint with the parameters' do - app = SapiClient::Application.new(base_url, spec) - inst = app.instance + describe "#with #{spec_file_or_dir}" do + it 'should invoke the endpoint with the parameters' do + app = SapiClient::Application.new(base_url, spec) + inst = app.instance - VCR.use_cassette('endpoint_values.test_to_a') do - evs = SapiClient::EndpointValues.new(inst, :establishment_list) - evs.limit(1) - establishments = evs.to_a - _(establishments).must_be_kind_of(Array) - _(establishments.length).must_equal(1) + VCR.use_cassette('endpoint_values.test_to_a') do + evs = SapiClient::EndpointValues.new(inst, :establishment_list) + evs.limit(1) + establishments = evs.to_a + _(establishments).must_be_kind_of(Array) + _(establishments.length).must_equal(1) + end + end end end end diff --git a/test/sapi_client/regressions_test.rb b/test/sapi_client/regressions_test.rb index baefa32..82bcdf0 100644 --- a/test/sapi_client/regressions_test.rb +++ b/test/sapi_client/regressions_test.rb @@ -7,17 +7,24 @@ module SapiClient class RegressionsTest < Minitest::Test describe 'Regression tests' do describe 'https://github.com/epimorphics/sapi-client-ruby/issues/44' do - let(:spec) { 'test/fixtures/cbd_api/application.yaml' } - let(:base_url) { 'https://fsa-cbd-test.epimorphics.net' } + [ + 'test/fixtures/cbd_api/application.yaml', + 'test/fixtures/cbd_api/endpointSpecs' + ].each do |spec_file_or_dir| + describe "#with #{spec_file_or_dir}" do + let(:spec) { 'test/fixtures/cbd_api/application.yaml' } + let(:base_url) { 'https://fsa-cbd-test.epimorphics.net' } - it 'should return a list containing SapiResource items' do - app = SapiClient::Application.new(base_url, spec) - inst = app.instance + it 'should return a list containing SapiResource items' do + app = SapiClient::Application.new(base_url, spec) + inst = app.instance - VCR.use_cassette('regression_tests.issue-44') do - listings = inst.listing_list(_limit: 1) + VCR.use_cassette('regression_tests.issue-44') do + listings = inst.listing_list(_limit: 1) - _(listings.first).must_be_kind_of SapiClient::SapiResource + _(listings.first).must_be_kind_of SapiClient::SapiResource + end + end end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 7b59187..2b563e2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -36,7 +36,9 @@ config.default_cassette_options = default_cassette_options end -Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)] +unless ENV['RM_INFO'] + Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)] +end # Helper to get the test API port number from the environment, or return a default def sapi_api_port