grape-path-helpers-2.0.1/0000755000004100000410000000000014554651460015244 5ustar www-datawww-datagrape-path-helpers-2.0.1/Gemfile.lock0000644000004100000410000000436714554651460017500 0ustar www-datawww-dataPATH remote: . specs: grape-path-helpers (2.0.1) activesupport grape (~> 2.0) rake (> 12) ruby2_keywords (~> 0.0.2) GEM remote: https://rubygems.org/ specs: activesupport (7.0.3.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) ast (2.4.0) builder (3.2.4) coderay (1.1.2) concurrent-ruby (1.2.2) diff-lcs (1.3) dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) dry-inflector (1.0.0) dry-logic (1.5.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) dry-types (1.7.1) concurrent-ruby (~> 1.0) dry-core (~> 1.0) dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) grape (2.0.0) activesupport (>= 5) builder dry-types (>= 1.1) mustermann-grape (~> 1.0.0) rack (>= 1.3.0) rack-accept i18n (1.14.1) concurrent-ruby (~> 1.0) method_source (0.9.0) minitest (5.20.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) mustermann-grape (1.0.2) mustermann (>= 1.0.0) parallel (1.12.1) parser (2.5.1.0) ast (~> 2.4.0) powerpack (0.1.1) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) rack (3.0.8) rack-accept (0.4.5) rack (>= 0.4) rainbow (3.0.0) rake (13.1.0) rspec (3.7.0) rspec-core (~> 3.7.0) rspec-expectations (~> 3.7.0) rspec-mocks (~> 3.7.0) rspec-core (3.7.1) rspec-support (~> 3.7.0) rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.7.0) rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.7.0) rspec-support (3.7.1) rubocop (0.56.0) parallel (~> 1.10) parser (>= 2.5) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.9.0) ruby2_keywords (0.0.5) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (1.3.3) zeitwerk (2.6.12) PLATFORMS ruby DEPENDENCIES grape-path-helpers! pry (~> 0.11) rspec (~> 3.7) rubocop (~> 0.56) BUNDLED WITH 2.2.3 grape-path-helpers-2.0.1/.gitlab-ci.yml0000644000004100000410000000042014554651460017674 0ustar www-datawww-dataimage: "ruby:3.0-alpine" cache: paths: - vendor/ruby key: ruby-3.0 before_script: - apk add --update git - gem install bundler - bundle install --path vendor/ruby rubocop: script: - bundle exec rubocop rspec: script: - bundle exec rspec spec grape-path-helpers-2.0.1/.gitignore0000644000004100000410000000003214554651460017227 0ustar www-datawww-data*.swp *.gem .ruby-version grape-path-helpers-2.0.1/CONTRIBUTING.md0000644000004100000410000000424114554651460017476 0ustar www-datawww-data## Developer Certificate of Origin and License By contributing to GitLab B.V., you accept and agree to the following terms and conditions for your present and future contributions submitted to GitLab B.V. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., you reserve all right, title, and interest in and to your Contributions. All contributions are subject to the Developer Certificate of Origin and license set out at [docs.gitlab.com/ce/legal/developer_certificate_of_origin](https://docs.gitlab.com/ce/legal/developer_certificate_of_origin). _This notice should stay as the first item in the CONTRIBUTING.md file._ ## Code of conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers 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. Project maintainers who do not follow the Code of Conduct may be removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com. This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 1.1.0, available at [https://contributor-covenant.org/version/1/1/0/](https://contributor-covenant.org/version/1/1/0/). grape-path-helpers-2.0.1/grape-path-helpers.gemspec0000644000004100000410000000156514554651460022310 0ustar www-datawww-datarequire File.join(Dir.pwd, 'lib', 'grape-path-helpers', 'version') Gem::Specification.new do |gem| gem.name = 'grape-path-helpers' gem.version = GrapePathHelpers::VERSION gem.licenses = ['MIT'] gem.summary = 'Route path helpers for Grape' gem.description = 'Route path helpers for Grape' gem.authors = ['Drew Blessing', 'Harper Henn'] gem.email = '' gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) gem.homepage = 'https://gitlab.com/gitlab-org/grape-path-helpers' gem.add_runtime_dependency 'activesupport' gem.add_runtime_dependency 'grape', '~> 2.0' gem.add_runtime_dependency 'rake', '> 12' gem.add_runtime_dependency 'ruby2_keywords', '~> 0.0.2' gem.add_development_dependency 'pry', '~> 0.11' gem.add_development_dependency 'rspec', '~> 3.7' gem.add_development_dependency 'rubocop', '~> 0.56' end grape-path-helpers-2.0.1/lib/0000755000004100000410000000000014554651460016012 5ustar www-datawww-datagrape-path-helpers-2.0.1/lib/tasks/0000755000004100000410000000000014554651460017137 5ustar www-datawww-datagrape-path-helpers-2.0.1/lib/tasks/grape_path_helpers.rake0000644000004100000410000000023114554651460023633 0ustar www-datawww-datanamespace :grape do desc 'Print route helper methods.' task path_helpers: :environment do GrapePathHelpers::RouteDisplayer.new.display end end grape-path-helpers-2.0.1/lib/grape-path-helpers.rb0000644000004100000410000000077114554651460022034 0ustar www-datawww-datarequire 'grape' require 'active_support' require 'active_support/core_ext/class' require 'grape-path-helpers/decorated_route' require 'grape-path-helpers/named_route_matcher' require 'grape-path-helpers/all_routes' require 'grape-path-helpers/route_displayer' # Load the Grape route helper for Rails module GrapePathHelpers require 'grape-path-helpers/railtie' if defined?(Rails) end Grape::API::Instance.extend GrapePathHelpers::AllRoutes Grape::Endpoint.prepend GrapePathHelpers::NamedRouteMatcher grape-path-helpers-2.0.1/lib/grape-path-helpers/0000755000004100000410000000000014554651460021502 5ustar www-datawww-datagrape-path-helpers-2.0.1/lib/grape-path-helpers/tasks.rb0000644000004100000410000000011614554651460023152 0ustar www-datawww-dataDir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each { |f| load f } grape-path-helpers-2.0.1/lib/grape-path-helpers/all_routes.rb0000644000004100000410000000161214554651460024200 0ustar www-datawww-datamodule GrapePathHelpers # methods to extend Grape::API's behavior so it can get a # list of routes from all APIs and decorate them with # the DecoratedRoute class module AllRoutes def decorated_routes_by_helper_name return @decorated_routes_by_helper_name if @decorated_routes_by_helper_name # rubocop:disable Metrics/LineLength routes = {} all_routes .map { |r| DecoratedRoute.new(r) } .sort_by { |r| -r.dynamic_path_segments.count } .each do |route| route.helper_names.each do |helper_name| key = helper_name.to_sym routes[key] ||= [] routes[key] << route end end @decorated_routes_by_helper_name = routes end def all_routes routes = descendants.flat_map { |s| s.send(:prepare_routes) } routes.uniq { |r| r.options.merge(path: r.path) } end end end grape-path-helpers-2.0.1/lib/grape-path-helpers/route_displayer.rb0000644000004100000410000000201014554651460025232 0ustar www-datawww-datamodule GrapePathHelpers # class for displaying the path, helper method name, # and required arguments for every Grape::Route. class RouteDisplayer def route_attributes Grape::API::Instance .decorated_routes_by_helper_name .values .flatten .uniq .map do |route| { route_path: route.route_path, route_method: route.route_method, helper_names: route.helper_names, helper_arguments: route.helper_arguments } end end def display puts("== GRAPE ROUTE HELPERS ==\n\n") route_attributes.each do |attributes| printf("%s: %s\n", 'Verb', attributes[:route_method]) printf("%s: %s\n", 'Path', attributes[:route_path]) printf("%s: %s\n", 'Helper Method', attributes[:helper_names].join(', ')) printf("%s: %s\n", 'Arguments', attributes[:helper_arguments].join(', ')) puts("\n") end end end end grape-path-helpers-2.0.1/lib/grape-path-helpers/decorated_route.rb0000644000004100000410000001071514554651460025203 0ustar www-datawww-datamodule GrapePathHelpers # wrapper around Grape::Route that adds a helper method class DecoratedRoute attr_reader :route, :helper_names, :helper_arguments, :extension, :route_options PATH_SEGMENTS_REGEXP = %r{\(/?\.:?\w+\)|/(?!\))|(?<=\))|\??\*} PATH_SEGMENTS_WITH_WILDCARDS_REGEXP = %r{\(/?\.:?\w+\)|/(?!\))|(?<=\))|\?} def self.sanitize_method_name(string) string.gsub(/\W|^[0-9]/, '_') end def initialize(route) @route = route @route_options = route.options @helper_names = [] @helper_arguments = required_helper_segments @extension = default_extension define_path_helpers end def default_extension pattern = /\((\.\:?\w+)\)$/ match = route_path.match(pattern) return '' unless match ext = match.captures.first if ext == '.:format' '' else ext end end def define_path_helpers route_versions.each do |version| route_attributes = { version: version, format: extension } method_name = path_helper_name(route_attributes) @helper_names << method_name define_path_helper(method_name, route_attributes) end end def define_path_helper(method_name, route_attributes) method_body = <<-RUBY def #{method_name}(attributes = {}, include_wildcard_segments = false) attrs = #{route_attributes}.merge(attributes) query_params = attrs.delete(:params) content_type = attrs.delete(:format) path = '/' + path_segments_with_values(attrs, include_wildcard_segments).join('/') path + content_type + query_string(query_params) end RUBY instance_eval method_body end def query_string(params) if params.nil? '' else '?' + params.to_param end end def route_versions return [nil] if route_version.nil? || route_version.empty? if route_version.is_a?(String) version_pattern = /[^\[",\]\s]+/ route_version.scan(version_pattern) else route_version end end def path_helper_name(opts = {}) if route_options[:as] name = route_options[:as].to_s else segments = path_segments_with_values(opts) name = if segments.empty? 'root' else segments.join('_') end end sanitized_name = self.class.sanitize_method_name(name) sanitized_name + '_path' end def segment_to_value(segment, opts = {}) if dynamic_segment?(segment) options = route.options.merge(stringify_keys(opts)) key = segment.slice(1..-1).to_sym options[key] else segment end end def path_segments_with_values(opts, include_wildcard_segments = false) segments = path_segments(include_wildcard_segments).map do |s| segment_to_value(s, opts) end segments.reject(&:blank?) end def path_segments(include_wildcard_segments = false) pattern = if include_wildcard_segments PATH_SEGMENTS_WITH_WILDCARDS_REGEXP else PATH_SEGMENTS_REGEXP end route_path.split(pattern).reject(&:blank?) end def dynamic_path_segments segments = path_segments.select do |segment| dynamic_segment?(segment) end segments.map { |s| s.slice(1..-1) } end def dynamic_segment?(segment) segment.start_with?(':', '*') end def optional_segment?(segment) segment.start_with?('(') end def required_helper_segments segments_in_options = dynamic_path_segments.select do |segment| route.options[segment.to_sym] end dynamic_path_segments - segments_in_options end def special_keys %w[format params] end def uses_segments_in_path_helper?(segments) segments = segments.reject { |x| special_keys.include?(x) } if helper_arguments.empty? && segments.any? false else helper_arguments.all? { |x| segments.include?(x) } end end def route_path route.path end def route_version route.version end def route_namespace route.namespace end def route_method route.request_method end private def stringify_keys(original) original.each_with_object({}) do |(key, value), hash| hash[key.to_sym] = value end end end end grape-path-helpers-2.0.1/lib/grape-path-helpers/version.rb0000644000004100000410000000010514554651460023510 0ustar www-datawww-data# Gem version module GrapePathHelpers VERSION = '2.0.1'.freeze end grape-path-helpers-2.0.1/lib/grape-path-helpers/named_route_matcher.rb0000644000004100000410000000216714554651460026042 0ustar www-datawww-datamodule GrapePathHelpers # methods to extend Grape::Endpoint so that calls # to unknown methods will look for a route with a matching # helper function name module NamedRouteMatcher def method_missing(method_name, *args) return super unless method_name.end_with?('_path') possible_routes = Grape::API::Instance .decorated_routes_by_helper_name[method_name] return super unless possible_routes segments = args.first || {} return super unless segments.is_a?(Hash) requested_segments = segments.keys.map(&:to_s) route = possible_routes.detect do |r| r.uses_segments_in_path_helper?(requested_segments) end if route route.send(method_name, *args) else super end end ruby2_keywords(:method_missing) def respond_to_missing?(method_name, _include_private = false) grape_route_path?(method_name) || super end def grape_route_path?(method_name) method_name.end_with?('_path') && !Grape::API::Instance .decorated_routes_by_helper_name[method_name].nil? end end end grape-path-helpers-2.0.1/lib/grape-path-helpers/railtie.rb0000644000004100000410000000040214554651460023454 0ustar www-datawww-datamodule GrapePathHelpers # Pulls in Rake helper for displaying route helper methods class Railtie < Rails::Railtie rake_tasks do files = File.join(File.dirname(__FILE__), '../tasks/*.rake') Dir[files].each { |f| load f } end end end grape-path-helpers-2.0.1/lib/grape/0000755000004100000410000000000014554651460017110 5ustar www-datawww-datagrape-path-helpers-2.0.1/lib/grape/path_helpers.rb0000644000004100000410000000003514554651460022111 0ustar www-datawww-datarequire 'grape-path-helpers' grape-path-helpers-2.0.1/LICENSE.txt0000644000004100000410000000213414554651460017067 0ustar www-datawww-dataMIT License (MIT) Copyright (c) 2015,2016 Harper Henn Copyright (c) 2018-2020 GitLab B.V. 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. grape-path-helpers-2.0.1/spec/0000755000004100000410000000000014554651460016176 5ustar www-datawww-datagrape-path-helpers-2.0.1/spec/spec_helper.rb0000644000004100000410000000027214554651460021015 0ustar www-datawww-data$LOAD_PATH.unshift File.expand_path('lib') require 'pry' require 'grape/path_helpers' support_files = Dir.glob('spec/support/*') support_files.each { |f| require File.expand_path(f) } grape-path-helpers-2.0.1/spec/grape_path_helpers/0000755000004100000410000000000014554651460022032 5ustar www-datawww-datagrape-path-helpers-2.0.1/spec/grape_path_helpers/route_displayer_spec.rb0000644000004100000410000000111314554651460026577 0ustar www-datawww-datarequire 'spec_helper' describe GrapePathHelpers::RouteDisplayer do subject(:route_displayer) { described_class.new } describe '#route_attributes' do subject { route_displayer.route_attributes } it 'returns the list of attributes' do is_expected.to include(a_hash_including( route_path: '/:version/ping(.:format)', route_method: 'GET', helper_names: ['beta_1_ping_path'], helper_arguments: [] )) end end end grape-path-helpers-2.0.1/spec/grape_path_helpers/all_routes_spec.rb0000644000004100000410000000305414554651460025544 0ustar www-datawww-datarequire 'spec_helper' # rubocop:disable Metrics/BlockLength describe GrapePathHelpers::AllRoutes do Grape::API::Instance.extend described_class describe '#all_routes' do context 'when API is mounted within another API' do let(:mounting_api) { Spec::Support::MountedAPI } it 'does not include the same route twice' do mounting_api # A route is unique if no other route shares the same set of options all_route_options = Grape::API::Instance.all_routes.map do |r| r.instance_variable_get(:@options).merge(path: r.path) end duplicates = all_route_options.select do |o| all_route_options.count(o) > 1 end expect(duplicates).to be_empty end end # rubocop:disable Metrics/LineLength context 'when there are multiple POST routes with the same namespace in the same API' do it 'returns all POST routes' do expected_routes = Spec::Support::MultiplePostsAPI.routes.map(&:path) all_routes = Grape::API::Instance.all_routes expect(all_routes.map(&:path)).to include(*expected_routes) end end context 'when an API is created via an intermediate class' do it 'includes those routes on both Grape::API::Instance and the base class' do expect(Spec::Support::BaseAPI.all_routes.map(&:origin)).to include('/derived_ping') expect(Grape::API::Instance.all_routes.map(&:origin)).to include('/derived_ping') end end # rubocop:enable Metrics/LineLength end end # rubocop:enable Metrics/BlockLength grape-path-helpers-2.0.1/spec/grape_path_helpers/named_route_matcher_spec.rb0000644000004100000410000001077314554651460027406 0ustar www-datawww-datarequire 'spec_helper' # rubocop:disable Metrics/BlockLength describe GrapePathHelpers::NamedRouteMatcher do include described_class let(:helper_class) do fake_class = Class.new do prepend GrapePathHelpers::NamedRouteMatcher def method_missing(method_id, *args, **kwargs) [method_id, args, kwargs] || super end def respond_to_missing?(_method_name, _include_private = false) super end end fake_class.new end it 'is available inside Grape endpoints' do endpoint = Spec::Support::API.endpoints.first expect(endpoint.api_v1_cats_path(id: 5)) .to eq('/api/v1/cats/5.json') end describe '#method_missing' do it 'returns super method_missing if the route does not exist' do expect(helper_class.test_method(id: 1)) .to eq([:test_method, [], { id: 1 }]) end it 'returns super method_missing if first arg is not a hash' do expect(helper_class.api_v1_cats_path(:arg1, kwarg1: :kwarg1)) .to eq([:api_v1_cats_path, [:arg1], { kwarg1: :kwarg1 }]) end context 'when method name matches a Grape::Route path helper name' do it 'returns the path for that route object' do expect(helper_class.api_v1_ping_path).to eq('/api/v1/ping.json') end context 'when route contains dynamic segments' do it 'returns the path for that route object' do expect(helper_class.api_v1_cats_path(id: 5)) .to eq('/api/v1/cats/5.json') end end context 'when route requires dynamic segments but none are passed in' do it 'returns super method_missing' do expect(helper_class.api_v1_cats_owners_path) .to eq([:api_v1_cats_owners_path, [], {}]) end end context 'when route has no dynamic segments but some are passed in' do it 'returns super method_missing' do expect(helper_class.api_v1_ping_path(invalid: 'test')) .to eq([:api_v1_ping_path, [], { invalid: 'test' }]) end end end end describe '#respond_to_missing?' do context 'when method name doesnt end with _path suffix' do let(:method_name) { :api_v1_cats } it 'returns false' do expect(respond_to_missing?(method_name)).to eq(false) end it 'doesnt execute decorated_routes_by_helper_name method' do expect(Grape::API::Instance) .not_to receive(:decorated_routes_by_helper_name) respond_to_missing?(method_name) end end context 'when method name matches a Grape::Route path with segments' do let(:method_name) { :api_v1_cats_path } it 'returns true' do expect(respond_to_missing?(method_name)).to eq(true) end end context 'when method name matches a Grape::Route path' do let(:method_name) { :api_v1_ping_path } it 'returns true' do expect(respond_to_missing?(method_name)).to eq(true) end end context 'when method name does not match a Grape::Route path' do let(:method_name) { :some_other_path } it 'returns false' do expect(respond_to_missing?(method_name)).to eq(false) end end end context 'when Grape::Route objects share the same helper name' do context 'when helpers require different segments to generate their path' do it 'uses arguments to infer which route to use' do show_path = helper_class.api_v1_cats_path( 'id' => 1 ) expect(show_path).to eq('/api/v1/cats/1.json') index_path = helper_class.api_v1_cats_path expect(index_path).to eq('/api/v1/cats.json') end it 'does not get shadowed by another route with less segments' do show_path = helper_class.api_v1_cats_owners_path( 'id' => 1 ) expect(show_path).to eq('/api/v1/cats/1/owners.json') show_path = helper_class.api_v1_cats_owners_path( 'id' => 1, 'owner_id' => 1 ) expect(show_path).to eq('/api/v1/cats/1/owners/1.json') end end context 'when query params are passed in' do it 'uses arguments to infer which route to use' do show_path = helper_class.api_v1_cats_path( 'id' => 1, params: { 'foo' => 'bar' } ) expect(show_path).to eq('/api/v1/cats/1.json?foo=bar') index_path = helper_class.api_v1_cats_path( params: { 'foo' => 'bar' } ) expect(index_path).to eq('/api/v1/cats.json?foo=bar') end end end end # rubocop:enable Metrics/BlockLength grape-path-helpers-2.0.1/spec/grape_path_helpers/decorated_route_spec.rb0000644000004100000410000002677414554651460026561 0ustar www-datawww-datarequire 'spec_helper' # rubocop:disable Metrics/BlockLength describe GrapePathHelpers::DecoratedRoute do let(:api) { Spec::Support::API } let(:routes) do api.routes.map do |route| described_class.new(route) end end let(:index_route) do routes.detect { |route| route.route_namespace == '/cats' } end let(:show_route) do routes.detect { |route| route.route_namespace == '/cats/:id' } end let(:catch_all_route) do routes.detect { |route| route.route_path =~ /\*path/ } end let(:custom_route) do routes.detect { |route| route.route_path =~ /custom_name/ } end let(:ping_route) do routes.detect { |route| route.route_path =~ /ping/ } end let(:optional_route) do routes.detect { |route| route.route_path =~ /optional/ } end let(:wildcard_route) do routes.detect { |route| route.route_path =~ /\*owner_ids/ } end describe '#sanitize_method_name' do it 'removes characters that are illegal in Ruby method names' do illegal_names = ['beta-1', 'name_with_+', 'name_with_('] sanitized = illegal_names.map do |name| described_class.sanitize_method_name(name) end expect(sanitized).to match_array(%w[beta_1 name_with__ name_with__]) end it 'only replaces integers if they appear at the beginning' do illegal_name = '1' legal_name = 'v1' expect(described_class.sanitize_method_name(illegal_name)).to eq('_') expect(described_class.sanitize_method_name(legal_name)).to eq('v1') end end describe '#define_path_helper' do context 'with only static segments' do let(:route) { ping_route } subject { route.api_v1_ping_path } it { is_expected.to eq '/api/v1/ping.json' } end context 'with optional segments' do let(:route) { optional_route } subject { route.api_v1_cats______optional_path } it { is_expected.to eq '/api/v1/cats/(-/)/optional.json' } end context 'with dynamic segments' do let(:route) { show_route } subject { route.api_v1_cats_path(id: 1) } it { is_expected.to eq '/api/v1/cats/1.json' } end context 'with wildcard segments' do let(:route) { wildcard_route } context 'including them' do subject do route.api_v1_cats_owners_owner_ids_cats_path( { owner_ids: 'foobar' }, true ) end it { is_expected.to eq '/api/v1/cats/owners/foobar/cats.json' } end context 'excluding them' do subject { route.api_v1_cats_owners_owner_ids_cats_path } it { is_expected.to eq '/api/v1/cats/owners/owner_ids/cats.json' } end end end describe '#helper_names' do context 'when a route is given a custom helper name' do it 'uses the custom name instead of the dynamically generated one' do expect(custom_route.helper_names.first) .to eq('my_custom_route_name_path') end it 'returns the correct path' do expect( custom_route.my_custom_route_name_path ).to eq('/api/v1/custom_name.json') end end context 'when an API has multiple POST routes in a resource' do let(:api) { Spec::Support::MultiplePostsAPI } it 'it creates a helper for each POST route' do expect(routes.size).to eq(2) end end context 'when an API has multiple versions' do let(:api) { Spec::Support::APIWithMultipleVersions } it "returns the route's helper name for each version" do helper_names = ping_route.helper_names expect(helper_names.size).to eq(api.versions.size) end it 'returns an array of the routes versions' do expect(ping_route.route_version).to eq(%w[beta alpha v1]) end end context 'when an API has one version' do it "returns the route's helper name for that version" do helper_name = show_route.helper_names.first expect(helper_name).to eq('api_v1_cats_path') end end end describe '#helper_arguments' do context 'when no user input is needed to generate the correct path' do it 'returns an empty array' do expect(index_route.helper_arguments).to eq([]) end end context 'when user input is needed to generate the correct path' do it 'returns an array of required segments' do expect(show_route.helper_arguments).to eq(['id']) end end end describe '#path_segments' do context 'when path has optional segments' do it 'leaves them intact' do result = optional_route.path_segments expect(result).to eq(%w[api :version cats :id (-/) optional]) end end end describe '#path_segments_with_values' do context 'when path has dynamic segments' do it 'replaces segments with corresponding values found in options' do opts = { id: 1 } result = show_route.path_segments_with_values(opts) expect(result).to include(1) end context 'when options contains string keys' do it 'replaces segments with corresponding values found in the options' do opts = { 'id' => 1 } result = show_route.path_segments_with_values(opts) expect(result).to include(1) end end end context 'when path has wildcard segments' do context 'with regex excluding them' do it "doesn't replaces wildcard segments" do opts = { version: 4, id: 34, owner_ids: 'foobar' } result = wildcard_route.path_segments_with_values(opts) expect(result).to include(4) expect(result).to include(34) expect(result).to include('owner_ids') expect(result).not_to include('foobar') end it "doesn't blank wildcard segments" do opts = { version: 4, id: 34, owner_ids: '' } result = wildcard_route.path_segments_with_values(opts) expect(result).to include(4) expect(result).to include(34) expect(result).to include('owner_ids') end end context 'with regex including them' do context 'replaces segments' do it 'with static text' do opts = { version: 4, id: 34, owner_ids: 'foobar' } result = wildcard_route.path_segments_with_values(opts, true) expect(result).to include(4) expect(result).to include(34) expect(result).to include('foobar') expect(result).not_to include('owner_ids') end it 'with additional segments' do opts = { version: 4, id: 34, owner_ids: 'foo/bar/baz' } result = wildcard_route.path_segments_with_values(opts, true) expect(result).to include(4) expect(result).to include(34) expect(result).to include('foo/bar/baz') expect(result).not_to include('owner_ids') end end it 'blanks wildcard segments' do opts = { version: 4, id: 34, owner_ids: '' } result = wildcard_route.path_segments_with_values(opts, true) expect(result).to include(4) expect(result).to include(34) expect(result).not_to include('owner_ids') end context 'when options contains string keys' do it 'replaces segments' do opts = { 'version' => 4, 'id' => 34, 'owner_ids' => 'foobar' } result = wildcard_route.path_segments_with_values(opts, true) expect(result).to include(4) expect(result).to include(34) expect(result).to include('foobar') expect(result).not_to include('owner_ids') end end end end end describe '#path_helper_name' do it "returns the name of a route's helper method" do expect(index_route.path_helper_name).to eq('api_v1_cats_path') end context 'when the path is the root path' do let(:api_with_root) do Class.new(Grape::API) do get '/' do end end end let(:root_route) do grape_route = api_with_root.routes.first described_class.new(grape_route) end it 'returns "root_path"' do result = root_route.path_helper_name expect(result).to eq('root_path') end end context 'when the path is a catch-all path' do it 'returns a name without the glob star' do result = catch_all_route.path_helper_name expect(result).to eq('api_v1_path_path') end end context 'when the path has a wildcard segment' do it 'returns a name without the glob star' do result = wildcard_route.path_helper_name expect(result).to eq('api_v1_cats_owners_owner_ids_cats_path') end end end describe '#segment_to_value' do context 'when segment is dynamic' do it 'returns the value the segment corresponds to' do result = index_route.segment_to_value(':version') expect(result).to eq('v1') end context 'when segment is found in options' do it 'returns the value found in options' do options = { id: 1 } result = show_route.segment_to_value(':id', options) expect(result).to eq(1) end end end context 'when segment is static' do it 'returns the segment' do result = index_route.segment_to_value('api') expect(result).to eq('api') end end end describe 'path helper method' do context 'when given a "params" key' do context 'when value under "params" key is a hash' do it 'creates a query string' do query = { foo: :bar, baz: :zot } path = index_route.api_v1_cats_path(params: query) expect(path).to eq('/api/v1/cats.json?' + query.to_param) end end context 'when value under "params" is not a hash' do it 'coerces the value into a string' do path = index_route.api_v1_cats_path(params: 1) expect(path).to eq('/api/v1/cats.json?1') end end end # handle different Grape::Route#route_path formats in Grape 0.12.0 context 'when route_path contains a specific format' do it 'returns the correct path with the correct format' do path = index_route.api_v1_cats_path expect(path).to eq('/api/v1/cats.json') end end context 'when helper does not require arguments' do it 'returns the correct path' do path = index_route.api_v1_cats_path expect(path).to eq('/api/v1/cats.json') end end context 'when arguments are needed required to construct the right path' do context 'when not missing arguments' do it 'returns the correct path' do path = show_route.api_v1_cats_path(id: 1) expect(path).to eq('/api/v1/cats/1.json') end end end context "when a route's API has multiple versions" do let(:api) { Spec::Support::APIWithMultipleVersions } it 'returns a path for each version' do expect(ping_route.alpha_ping_path).to eq('/alpha/ping') expect(ping_route.beta_ping_path).to eq('/beta/ping') expect(ping_route.v1_ping_path).to eq('/v1/ping') end end context 'when a format is given' do it 'returns the path with a correct extension' do path = show_route.api_v1_cats_path(id: 1, format: '.xml') expect(path).to eq('/api/v1/cats/1.xml') end end end describe '#optional_segment?' do it { expect(index_route.optional_segment?('mandatory')).to eq(false) } it { expect(index_route.optional_segment?('(optional/)')).to eq(true) } end end # rubocop:enable Metrics/BlockLength grape-path-helpers-2.0.1/spec/support/0000755000004100000410000000000014554651460017712 5ustar www-datawww-datagrape-path-helpers-2.0.1/spec/support/api.rb0000644000004100000410000000332214554651460021010 0ustar www-datawww-datamodule Spec module Support # Test API class API < Grape::API::Instance version 'v1' prefix 'api' format 'json' get 'custom_name', as: :my_custom_route_name do 'hello' end get 'ping' do 'pong' end resource :cats do get '/' do %w[cats cats cats] end route_param :id do get do 'cat' end end get ':id/(-/)optional' do 'optional content' end get ':id/owners' do %w[owner1 owner2] end get ':id/owners/:owner_id' do 'owner' end get ':id/owners/*owner_ids/cats' do %w[cats cats cats] end end route :any, '*path' do 'catch-all route' end end # API with more than one version class APIWithMultipleVersions < Grape::API::Instance version %w[beta alpha v1] get 'ping' do 'pong' end end # API with another API mounted inside it class MountedAPI < Grape::API::Instance mount Spec::Support::API mount Spec::Support::APIWithMultipleVersions end # API with a version that would be illegal as a method name class APIWithIllegalVersion < Grape::API::Instance version 'beta-1' get 'ping' do 'pong' end end # API with multiple POST routes class MultiplePostsAPI < Grape::API::Instance resource :hamlet do post 'to_be' do end post 'or_not_to_be' do end end end class BaseAPI < Grape::API::Instance end class DerivedAPI < BaseAPI get 'derived_ping' do 'pong' end end end end grape-path-helpers-2.0.1/Rakefile0000644000004100000410000000054714554651460016717 0ustar www-datawww-datarequire 'rubygems' require 'bundler' require 'bundler/gem_tasks' Bundler.setup :default, :test, :development Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' end task :spec require 'rainbow/ext/string' unless String.respond_to?(:color) task default: [:spec] grape-path-helpers-2.0.1/Gemfile0000644000004100000410000000004714554651460016540 0ustar www-datawww-datasource 'https://rubygems.org' gemspec grape-path-helpers-2.0.1/.travis.yml0000644000004100000410000000031314554651460017352 0ustar www-datawww-datalanguage: ruby cache: bundler rvm: - ruby-head - 2.3.0 - 2.2.0 - 2.1.0 - 2.0.0 - 1.9.3 - jruby-19mode - jruby-head matrix: allow_failures: - rvm: ruby-head - rvm: jruby-head grape-path-helpers-2.0.1/README.md0000644000004100000410000001013114554651460016517 0ustar www-datawww-data# grape-path-helpers [![Build status](https://gitlab.com/gitlab-org/grape-path-helpers/badges/master/pipeline.svg)](https://gitlab.com/gitlab-org/grape-path-helpers/commits/master) Provides named route helpers for Grape APIs, similar to [Rails' route helpers](http://edgeguides.rubyonrails.org/routing.html#path-and-url-helpers). **This is a fork and rename of [grape-route-helpers](https://github.com/reprah/grape-route-helpers). It [includes some fixes](CHANGELOG.md) needed for GitLab.** ### Installation #### Rails p 1.) Add the gem to your Gemfile. ```bash $ bundle install grape-path-helpers ``` #### Sinatra/Rack 1.) Add the gem to your Gemfile if you're using Bundler. If you're not using Bundler to install/manage dependencies: ```bash $ gem install grape-path-helpers ``` ```ruby # environment setup file require 'grape' require 'grape/path_helpers' ``` 2.) Write a rake task called `:environment` that loads the application's environment first. This gem's tasks are dependent on it. You could put this in the root of your project directory: ```ruby # Rakefile require 'rake' require 'bundler' Bundler.setup require 'grape-path-helpers' require 'grape-path-helpers/tasks' desc 'load the Sinatra environment.' task :environment do require File.expand_path('your_app_file', File.dirname(__FILE__)) end ``` ### Usage #### List All Helper Names To see which methods correspond to which paths, and which options you can pass them: ```bash # In your API root directory, at the command line $ rake grape:path_helpers ``` #### Use Helpers in IRB/Pry You can use helper methods in your REPL session by including a module: ```ruby [1] pry(main)> include GrapePathHelpers::NamedRouteMatcher ``` #### Use Helpers in Your API Use the methods inside your Grape API actions. Given this example API: ```ruby class ExampleAPI < Grape::API::Instance version 'v1' prefix 'api' format 'json' get 'ping' do 'pong' end resource :cats do get '/' do %w(cats cats cats) end route_param :id do get do 'cat' end end end route :any, '*anything' do redirect api_v1_cats_path end end ``` You'd have the following methods available inside your Grape API actions: ```ruby # specifying the version when using Grape's "path" versioning strategy api_v1_ping_path # => '/api/v1/ping.json' # specifying the format api_v1_cats_path(format: '.xml') # => '/api/v1/cats.xml' # adding a query string api_v1_cats_path(params: { sort_by: :age }) # => '/api/v1/cats?sort_by=age' # passing in values required to build a path api_v1_cats_path(id: 1) # => '/api/v1/cats/1.json' # catch-all paths have helpers api_v1_anything_path # => '/api/v1/*anything' ``` #### Custom Helper Names If you want to assign a custom helper name to a route, pass the `:as` option when creating your route in your API: ```ruby class Base < Grape::API::Instance get 'ping', as: 'is_the_server_running' 'pong' end end ``` This results in creating a helper called `is_the_server_running_path`. #### Testing You can use route helpers in your API tests by including the `GrapePathHelpers::NamedRouteMatcher` module inside your specs. Here's an example: ```ruby require 'spec_helper' describe Api::Base do include GrapePathHelpers::NamedRouteMatcher describe 'GET /ping' do it 'returns a 200 OK' do get api_v2_ping_path expect(response.status).to be(200) end end end ``` ### Contributing 1.) Fork it 2.) Create your feature branch `(git checkout -b my-new-feature)` 3.) Write specs for your feature 4.) Commit your changes `(git commit -am 'Add some feature')` 5.) Push to the branch `(git push origin my-new-feature)` 6.) Create a new pull request ### Releasing 1. Update the [CHANGELOG](CHANGELOG.md). 2. Update the version in [lib/grape-path-helpers/version.rb](lib/grape-path-helpers/version.rb). 3. [Tag](https://gitlab.com/gitlab-org/grape-path-helpers/-/tags) the commit with the version number prefixed by 'v'. 4. Run `gem build grape-path-helpers` locally. 5. Run `gem push $new_file.gem` locally, where `$new_file` is the file generated by the step above. ### License See LICENSE grape-path-helpers-2.0.1/.rubocop.yml0000644000004100000410000000033114554651460017513 0ustar www-datawww-dataMetrics/ClassLength: Max: 104 Enabled: false Metrics/MethodLength: Max: 21 Enabled: false Style/FileName: Enabled: false # Removed in newer versions of Rubocop Lint/SplatKeywordArguments: Enabled: false grape-path-helpers-2.0.1/CHANGELOG.md0000644000004100000410000000735114554651460017063 0ustar www-datawww-data# Changelog ## 2.0.1 * [Fix helpers not available by default in Grape endpoints](https://gitlab.com/gitlab-org/ruby/gems/grape-path-helpers/-/merge_requests/46) ## 2.0.0 * [Adds support for Rack 3](https://gitlab.com/gitlab-org/ruby/gems/grape-path-helpers/-/merge_requests/45) ## 1.7.1 * Fix undefined method error (https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/42) ## 1.7.0 * [Further improve performance of route matching](https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/38) ## 1.6.3 * [Fix route matcher when method ends in path and arg isn't a Hash](https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/35) ## 1.6.2 * [Improve performance of route matching](https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/33) ## 1.6.1 * [Use ruby2_keywords to fix 2.7 warning](https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/31) ## 1.6.0 * [Extract kwargs to fix 2.7 warnings](https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/29) ## 1.5.0 * [Relax rake dependency](https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/27) ## 1.4.0 * [Support using a base class other than Grape::API::Instance](https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/23) ## 1.3.0 * [Upgrade to Grape 1.3.1](https://gitlab.com/gitlab-org/grape-path-helpers/-/merge_requests/21) ## 1.2.0 * [Add wildcard segments support](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/16) ## 1.1.0 * [Relax dependency on ActiveSupport](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/12) ## 1.0.6 * [Fix segments parsing for optional segments](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/10) ## 1.0.5 * [Relax dependencies](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/9) ## 1.0.4 * [Fix respond_to_missing? for included modules](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/8) ## 1.0.3 * [Fix return value in method_missing? implementation](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/7) * [Fix broken respond_to_missing? implementation](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/6) ## 1.0.2 * [Rename rake task from `grape:route_helpers` to `grape:path_helpers`](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/5) ## 1.0.1 * [Do not shadow helpers with the same name but more params](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/3) * [Reduce the number of calls to HashWithIndifferentAccess](https://gitlab.com/gitlab-org/grape-path-helpers/merge_requests/4) * Update to Grape 1.0 # Changelog for grape-route-helpers ## December 17 2016 * Bump to 2.1.0 * Fix bug that caused POST routes to be ignored if more than one was defined * Many thanks to @njd5475 for the bug report and pull request that helped me write this ## April 28 2016 * Release 2.0.0 * Fix incompatibility between grape-route-helpers and grape 0.16.0 ## April 11 2016 * Release 1.2.2 * Fixed incompatibility between grape-route-helpers and Ruby 2.3 by merging PR #8 from phallguy ## October 11 2015 * Release 1.2.1 * Fixed issue #4 ## September 27 2015 * Release 1.2.0 * You can now assign custom helper names to Grape routes * Fixed a bug where routes would be listed more than once in the rake task if they are mounted to another API * Added the HTTP verb to rake task output ## July 5 2015 * You can now pass query parameters to helper functions in order to generate your own query string (Issue #1) ## June 28 2015 Release 1.0.1 * Rename rake task from `grape:routes` to `grape:route_helpers` * If a Grape::Route has a specific format (json, etc.) in its route_path attribute, the helper will return a path with this extension at the end