grape-starter 1.6.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0e8dbdcdf0bc8ec707834a2a55a2c36826c316d82a45f7f8c219e1ec4361f37
4
- data.tar.gz: 21d235ca4a99f447965c8cf9a765bfaabca150f74a493ee332fb60aa137ce7fd
3
+ metadata.gz: 8d9f7ca7efc30bff48e6f765786b43a6cfd6d2c1c68d93063bbab89c4042bb5d
4
+ data.tar.gz: 1ecd2c9a8ce6eeebd94ed79a5e08bb81328c327a361750ca6ae1aff8578281ef
5
5
  SHA512:
6
- metadata.gz: c54cbaf4ce481b619e621c733a6b16bd1582761a297e4380de381773c837d3a0894a472f6e45bf451ad702fa5ca8fe1d0abcf8c067f31844d5eb75532cfec7cf
7
- data.tar.gz: df826306d1964ed1f2a58f87353cfb03b6f86bb084b0cb70e14639b04748d18986e5d971be420efb12919b82a28042cf8e1147e0c938eb5a52c0ef4ed44694ae
6
+ metadata.gz: baa02a26a233adc4f6edd6afe687356b99d29ef59071a170cf6183fb257be59254d7c2bb2e04bcf63bd67e8978e46c6a261cbb71319340ebeedacee3a8427f22
7
+ data.tar.gz: 43a5e2557d01d84bccc0cf91e5d7c52446b86572a2343d19ce2d905172b01b5c83bbf085d04d646e7cf6faf01b5466e0f47912323c32433981b693b5ff1bfad1
@@ -1,7 +1,8 @@
1
1
  name: Pipeline
2
2
  on:
3
3
  pull_request:
4
- types: [assigned, opened, edited, synchronize, reopened]
4
+ branches:
5
+ - 'master'
5
6
  push:
6
7
  branches:
7
8
  - 'master'
@@ -23,9 +24,11 @@ jobs:
23
24
  rspec:
24
25
  runs-on: ubuntu-latest
25
26
  needs: ['rubocop']
27
+ env:
28
+ RACK_ENV: test
26
29
  strategy:
27
30
  matrix:
28
- ruby-version: ['3.0', '3.1', '3.2', head]
31
+ ruby-version: ['3.1', '3.2', head]
29
32
 
30
33
  steps:
31
34
  - uses: actions/checkout@v3
data/.gitignore CHANGED
@@ -10,3 +10,7 @@ pkg/
10
10
  template/Gemfile.lock
11
11
  grape-starter.md
12
12
  tmp
13
+ api/
14
+ .vscode
15
+ spec/fixtures/pmm.json
16
+ spec/fixtures/nested.json
data/.rubocop.yml CHANGED
@@ -12,6 +12,7 @@ AllCops:
12
12
  - '**/template/api/**'
13
13
  - grape-starter.gemspec
14
14
  - template/spec/spec-helper.rb
15
+ - api/**/*
15
16
  UseCache: true
16
17
  NewCops: enable
17
18
  TargetRubyVersion: 3.2
@@ -25,6 +26,9 @@ Layout/IndentationWidth:
25
26
  Layout/LineLength:
26
27
  Max: 120
27
28
 
29
+ Lint/MissingSuper:
30
+ Enabled: false
31
+
28
32
  Metrics/BlockLength:
29
33
  Exclude:
30
34
  - 'spec/**/*'
@@ -32,13 +36,16 @@ Metrics/BlockLength:
32
36
  Metrics/AbcSize:
33
37
  Max: 20
34
38
 
39
+ Metrics/ClassLength:
40
+ Max: 120
41
+
35
42
  Metrics/MethodLength:
36
43
  Max: 20
37
44
 
38
45
  Naming/AccessorMethodName:
39
46
  Exclude:
40
- - 'lib/starter/templates/files.rb'
41
- - 'lib/starter/templates/endpoints.rb'
47
+ - 'lib/starter/builder/files.rb'
48
+ - 'lib/starter/builder/endpoints.rb'
42
49
 
43
50
  Style/AsciiComments:
44
51
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,6 +1,38 @@
1
1
  ### NEXT
2
2
 
3
- - your contributions
3
+ - contributions
4
+
5
+ ### v2.0.0 / 2023-10-21 Imports OApi specs
6
+
7
+ - [(#38)](https://github.com/LeFnord/grape-starter/pull/38) Handles nested body [LeFnord](https://github.com/LeFnord)
8
+ - [(#38)](https://github.com/LeFnord/grape-starter/pull/38) Handle parameters from request body [LeFnord](https://github.com/LeFnord)
9
+ - [(#37)](https://github.com/LeFnord/grape-starter/pull/37) Ignores possible version segments in path [LeFnord](https://github.com/LeFnord)
10
+ - Sets min ruby to 3.1 [LeFnord](https://github.com/LeFnord)
11
+ - [(#36)](https://github.com/LeFnord/grape-starter/pull/36) Handle Parameters [LeFnord](https://github.com/LeFnord)
12
+ - Avoids duplicated mount points. [LeFnord](https://github.com/LeFnord)
13
+ - Small code smells [LeFnord](https://github.com/LeFnord)
14
+ - Sets next version to 2.0.0 [LeFnord](https://github.com/LeFnord)
15
+ - [(#31)](https://github.com/LeFnord/grape-starter/pull/31) Import OAPI spec [LeFnord](https://github.com/LeFnord)
16
+ - Fix for Dockerfile smell DL3020 [#35](https://github.com/LeFnord/grape-starter/pull/35) [Giovanni Rosa](https://github.com/grosa1)
17
+
18
+ ### v1.6.2 / 2023-03-12
19
+
20
+ - [(#34)](https://github.com/LeFnord/grape-starter/pull/34) Makes Names a class [LeFnord](https://github.com/LeFnord)
21
+ - Re-organises libe files. [LeFnord](https://github.com/LeFnord)
22
+ - Minor clean up. [LeFnord](https://github.com/LeFnord)
23
+
24
+ ### v1.6.1 / 2023-02-17 -> yanked
25
+
26
+ ### v1.6.0 / 2023-02-13
27
+
28
+ - [(#33)](https://github.com/LeFnord/grape-starter/pull/33) Replaces Thin by Puma [LeFnord](https://github.com/LeFnord)
29
+ - Minor lint fix. [LeFnord](https://github.com/LeFnord)
30
+ - [(#32)](https://github.com/LeFnord/grape-starter/pull/32) Uses Zeitwerk for loading [LeFnord](https://github.com/LeFnord)
31
+
32
+ ### v1.5.2 / 2023-02-11
33
+
34
+ - Upgrades action. [LeFnord](https://github.com/LeFnord)
35
+ - Uses ruby 3.2 [LeFnord](https://github.com/LeFnord)
4
36
 
5
37
  ### v1.5.1 / 2021-12-28
6
38
 
@@ -47,7 +79,7 @@
47
79
  - Changes ruby.yml to use 2.7 [LeFnord](https://github.com/LeFnord)
48
80
  - Prepare release 1.2.4 [LeFnord](https://github.com/LeFnord)
49
81
  - Respects froozen string stuff. [LeFnord](https://github.com/LeFnord)
50
- - Updates Ruby and deps. (#23) [LeFnord](https://github.com/LeFnord)
82
+ - [(#23)](https://github.com/LeFnord/grape-starter/pull/23) Updates Ruby and deps [LeFnord](https://github.com/LeFnord)
51
83
  - Create ruby.yml [LeFnord](https://github.com/LeFnord)
52
84
 
53
85
  - Fixes gems for sequel. [LeFnord](https://github.com/LeFnord)
@@ -55,8 +87,8 @@
55
87
 
56
88
  ### v1.2.3 / 2019-12-15
57
89
 
58
- - Removes require pry. [LeFnord](LeFnord) closes [!21](https://github.com/LeFnord/grape-starter/issues/21)
59
- - [#22](https://github.com/LeFnord/grape-starter/pull/22) - accomodate activerecord 6.0 (#22) [Ignacio Carrera](https://github.com/nachokb)
90
+ - Removes require pry. [LeFnord](https://github.com/LeFnord) closes [!21](https://github.com/LeFnord/grape-starter/issues/21)
91
+ - [#22](https://github.com/LeFnord/grape-starter/pull/22) - accomodate activerecord 6.0 [#22](https://github.com/LeFnord/grape-starter/pull/22) [Ignacio Carrera](https://github.com/nachokb)
60
92
 
61
93
  ### v1.2.2 / 2019-02-28
62
94
 
data/README.md CHANGED
@@ -1,6 +1,17 @@
1
1
  [![Pipeline](https://github.com/LeFnord/grape-starter/actions/workflows/pipeline.yml/badge.svg?branch=master)](https://github.com/LeFnord/grape-starter/actions/workflows/pipeline.yml)
2
2
  [![Gem Version](https://badge.fury.io/rb/grape-starter.svg)](https://badge.fury.io/rb/grape-starter)
3
3
 
4
+ - [Why the next one?](#why-the-next-one)
5
+ - [Usage](#usage)
6
+ - [Install it](#install-it)
7
+ - [Create a new project](#create-a-new-project)
8
+ - [Add resources](#add-resources)
9
+ - [Import OAPI spec \[WIP\]](#import-oapi-spec-wip)
10
+ - [Remove a resource](#remove-a-resource)
11
+ - [Contributing](#contributing)
12
+ - [Adding a new ORM template](#adding-a-new-orm-template)
13
+ - [License](#license)
14
+
4
15
 
5
16
  # Grape Starter
6
17
 
@@ -9,20 +20,24 @@ Is a tool to help you to build up a skeleton for a [Grape](http://github.com/rub
9
20
 
10
21
  ![ReDoc demo](doc/re-doc.png)
11
22
 
23
+
24
+
25
+
12
26
  ## Why the next one?
13
27
 
14
28
  - build up a playground for your ideas, prototypes, testing behaviour … whatever
15
29
  - ~~no assumtions about~~ you can choose, if you want to use a backend/ORM, ergo no restrictions, only a pure grape/rack skeleton with a nice documentation
16
30
 
31
+
17
32
  ## Usage
18
33
 
19
- #### Install it
34
+ ### Install it
20
35
  ```
21
36
  $ gem install grape-starter
22
37
  ```
23
38
 
24
39
 
25
- #### Create a new project
40
+ ### Create a new project
26
41
  ```
27
42
  $ grape-starter new awesome_api
28
43
  ```
@@ -32,9 +47,7 @@ with following options:
32
47
  -p foobar, --prefix=foobar # sets the prefix of the API (default: none)
33
48
  -o sequel, --orm=sequel # create files for the specified ORM, available: sequel, activerecord (ar) (default: none)
34
49
  ```
35
-
36
50
  This command creates a folder named `awesome_api` containing the skeleton. With following structure:
37
-
38
51
  ```
39
52
  ├── <Standards>
40
53
  ├── api
@@ -88,7 +101,7 @@ the documentation of it under: [http://localhost:9292/doc](http://localhost:9292
88
101
  More could be found in [README](template/README.md).
89
102
 
90
103
 
91
- #### Add resources
104
+ ### Add resources
92
105
  ```
93
106
  $ grape-starter add foo [http methods]
94
107
  ```
@@ -117,7 +130,24 @@ If the `orm` switch `true`, the lib class would be created as child class of a s
117
130
  so for example for Sequel, it would be wirtten: `Foo < Sequel::Model` instead of `Foo`, hereby the using ORM would be taken from the configuration, which was stored by project creation.
118
131
 
119
132
 
120
- #### Remove a resource
133
+ ### Import OAPI spec [WIP]
134
+ ```
135
+ $ grape-starter import path/to/spec
136
+ ```
137
+ to create an API based on the spec.
138
+
139
+ ##### To Dos:
140
+
141
+ - [x] read spec an create per namespace a file with defined endpoint
142
+ - [x] read path parameter
143
+ - [x] requires it in param block
144
+ - [x] specify it
145
+ - [x] handle query parameter -> check it
146
+ - [x] handle body parameter -> check it
147
+ - [ ] add description block
148
+
149
+
150
+ ### Remove a resource
121
151
  ```
122
152
  $ grape-starter rm foo
123
153
  ```
@@ -128,6 +158,7 @@ to remove previous generated files for a resource.
128
158
 
129
159
  Any contributions are welcome on GitHub at https://github.com/LeFnord/grape-starter.
130
160
 
161
+
131
162
  ### Adding a new ORM template
132
163
 
133
164
  To add an new ORM, it needs following steps:
@@ -168,6 +199,7 @@ To add an new ORM, it needs following steps:
168
199
  2. An additional switch in the [`Starter::Orms.build`](https://github.com/LeFnord/grape-starter/blob/ef45133e6d2254efee06ae4f17ede2fc5c06bebb/lib/starter/builder/orms.rb#L7-L18) and [`Starter::Names.lib_klass_name`](https://github.com/LeFnord/grape-starter/blob/ef45133e6d2254efee06ae4f17ede2fc5c06bebb/lib/starter/builder/names.rb#L13-L24) methods to choose the right template.
169
200
  3. An entry in the description of the [`new` command](https://github.com/LeFnord/grape-starter/blob/fa62c8a2ff72f984144b2336859d3e0b397398bd/bin/grape-starter#L28), when it would be called with `-h`
170
201
 
202
+
171
203
  ## License
172
204
 
173
205
  The gem is available as open source under the terms of the [MIT License](LICENSE).
data/bin/grape-starter CHANGED
@@ -73,7 +73,7 @@ command :add do |c|
73
73
  builder_options = global_options.merge(set: set).merge(options)
74
74
  created_files = Starter::Build.add!(resource, builder_options)
75
75
 
76
- `bundle exec rubocop -a #{created_files.join(' ')}`
76
+ `bundle exec rubocop --only Layout -a #{created_files.join(' ')}`
77
77
  $stdout.puts "added resource: #{resource}"
78
78
  rescue => e
79
79
  exit_now! e
@@ -97,6 +97,23 @@ command :rm do |c|
97
97
  end
98
98
  end
99
99
 
100
+ desc 'Adds resources from given OAPI spec'
101
+ long_desc 'Import YAML or JSON OAPI specification given by path argument'
102
+ arg_name 'path'
103
+ command :import do |c|
104
+ c.action do |_global_options, _options, args|
105
+ path = args.first
106
+ exit_now! 'no path given' if path.blank?
107
+ exit_now! "file under #{path} not exists" unless File.exist?(path)
108
+
109
+ created_files = Starter::Import.do_it!(path)
110
+
111
+ `bundle exec rubocop --only Layout -a #{created_files.join(' ')}`
112
+ rescue => e
113
+ exit_now! e
114
+ end
115
+ end
116
+
100
117
 
101
118
  pre do |_global, _command, _options, _args|
102
119
  # Pre logic here
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
24
 
25
- spec.required_ruby_version = '>= 2.7'
25
+ spec.required_ruby_version = '>= 3.1'
26
26
 
27
27
  spec.add_dependency 'gli', '~> 2.20'
28
28
  spec.add_dependency 'activesupport', '>= 6', '< 8'
data/lib/starter/build.rb CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Starter
4
4
  class Build
5
- extend Builder::BaseFile
6
- extend Templates::Files
7
- extend Templates::Endpoints
5
+ extend Shared::BaseFile
6
+ extend Builder::Files
7
+ extend Builder::Endpoints
8
8
 
9
9
  class << self
10
10
  attr_reader :prefix, :resource, :entity,
@@ -28,7 +28,7 @@ module Starter
28
28
  @resource = name
29
29
  @destination = destination
30
30
  @prefix = options[:p] # can be nil
31
- @naming = Starter::Builder::Names.new(@resource)
31
+ @naming = Starter::Names.new(@resource)
32
32
 
33
33
  FileUtils.copy_entry source, destination
34
34
 
@@ -64,7 +64,7 @@ module Starter
64
64
  @force = options[:force]
65
65
  @entity = options[:entity]
66
66
  @orm = options[:orm]
67
- @naming = Starter::Builder::Names.new(@resource)
67
+ @naming = Starter::Names.new(@resource)
68
68
 
69
69
  Orms.add_migration(@naming.klass_name, resource.downcase) if @orm
70
70
  save_resource
@@ -115,7 +115,7 @@ module Starter
115
115
  #
116
116
  # creates a new file in lib folder as namespace, includind the version
117
117
  def add_namespace_with_version
118
- new_lib = File.join(destination, 'lib', @naming.base_file_name)
118
+ new_lib = File.join(destination, 'lib', @naming.resource_file)
119
119
  FileOps.write_file(new_lib, base_namespace_file.strip_heredoc)
120
120
  end
121
121
 
@@ -9,7 +9,7 @@ module Starter
9
9
  'ActiveRecord::Base'
10
10
  end
11
11
 
12
- def initializer
12
+ def initializer # rubocop:disable Metrics/MethodLength
13
13
  <<-FILE.strip_heredoc
14
14
  # frozen_string_literal: true
15
15
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Starter
4
- module Templates
4
+ module Builder
5
5
  # defining the endpoints -> http methods of a resource
6
6
  module Endpoints
7
7
  def crud
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Starter
4
- module Templates
4
+ module Builder
5
5
  module Files
6
6
  # API template for resource
7
7
  def api_file
@@ -37,7 +37,7 @@ module Starter
37
37
  FILE
38
38
  end
39
39
 
40
- def rakefile
40
+ def rakefile # rubocop:disable Metrics/MethodLength
41
41
  <<-FILE.strip_heredoc
42
42
  # Sequel migration tasks
43
43
  namespace :db do
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'yaml'
5
+
6
+ module Starter
7
+ class Import
8
+ extend Shared::BaseFile
9
+
10
+ class << self
11
+ def do_it!(path)
12
+ spec = load_spec(path)
13
+ create_files_from(spec)
14
+ end
15
+
16
+ def load_spec(path)
17
+ return nil if path.blank?
18
+
19
+ spec = case File.extname(path)[1..]
20
+ when 'yaml', 'yml'
21
+ YAML.load_file(path)
22
+ when 'json'
23
+ JSON.load_file(path)
24
+ end
25
+
26
+ Importer::Specification.new(spec:)
27
+ end
28
+
29
+ def create_files_from(spec)
30
+ spec.namespaces.each_with_object([]) do |(name_of, paths), memo|
31
+ @naming = Starter::Names.new(name_of)
32
+
33
+ # 1. build content for file
34
+ namespace = Starter::Importer::Namespace.new(
35
+ naming: @naming,
36
+ paths: paths,
37
+ components: spec.components
38
+ )
39
+
40
+ break if ENV['RACK_ENV'] == 'test'
41
+
42
+ # 2. create endpoint file
43
+ FileOps.write_file(@naming.api_file_name, namespace.content)
44
+ memo << @naming.api_file_name
45
+
46
+ # 3. add mountpoint
47
+ add_mount_point
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: false
2
+
3
+ module Starter
4
+ module Importer
5
+ class Namespace
6
+ attr_accessor :naming, :paths, :components
7
+
8
+ def initialize(naming:, paths:, components:)
9
+ @naming = naming
10
+ @paths = paths
11
+ @components = components
12
+ end
13
+
14
+ def content
15
+ <<-FILE.strip_heredoc
16
+ # frozen_string_literal: true
17
+
18
+ module Api
19
+ module Endpoints
20
+ class #{@naming.klass_name} < Grape::API
21
+ namespace #{namespace} do
22
+ #{endpoints.join("\n")}
23
+ end
24
+ end
25
+ end
26
+ end
27
+ FILE
28
+ end
29
+
30
+ private
31
+
32
+ def namespace
33
+ @namespace ||= naming.version_klass ? "'#{naming.origin}'" : ":#{naming.resource.downcase}"
34
+ end
35
+
36
+ def endpoints
37
+ paths.map do |path, verbs|
38
+ segment = prepare_route(path)
39
+ verbs.keys.each_with_object([]) do |verb, memo|
40
+ next unless allowed_verbs.include?(verb)
41
+
42
+ if (parameters = verbs[verb]['parameters'].presence)
43
+ params_block = params_block(parameters)
44
+ memo << params_block
45
+ end
46
+ memo << "#{verb} '#{segment}' do\n # your code comes here\nend\n"
47
+ end
48
+ end
49
+ end
50
+
51
+ def prepare_route(path)
52
+ params = path.scan(/\{(\w+)\}/)
53
+ if params.empty?
54
+ path
55
+ else
56
+ path.gsub('{', ':').gsub('}', '')
57
+ end
58
+ end
59
+
60
+ def params_block(params)
61
+ params_block = "params do\n"
62
+ params.each_value do |param|
63
+ params_block << " #{param.to_s}\n" # rubocop:disable Lint/RedundantStringCoercion
64
+ end
65
+ params_block << 'end'
66
+ end
67
+
68
+ def allowed_verbs
69
+ %w[get put post delete]
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: false
2
+
3
+ module Starter
4
+ module Importer
5
+ class NestedParams < Parameter
6
+ def initialize(name:, definition:)
7
+ @kind = :body
8
+ @nested = []
9
+ @name = name
10
+ @definition = definition
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,180 @@
1
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ClassLength
2
+ # frozen_string_literal: false
3
+
4
+ module Starter
5
+ module Importer
6
+ class Parameter
7
+ class Error < StandardError; end
8
+
9
+ attr_accessor :kind, :name, :definition, :nested
10
+
11
+ def initialize(definition:, components: {})
12
+ @nested = []
13
+ @kind = validate_parameters(definition:, components:)
14
+ prepare_attributes(definition:, components:)
15
+ end
16
+
17
+ def to_s
18
+ return serialized_object if nested?
19
+
20
+ serialized
21
+ end
22
+
23
+ def nested?
24
+ @nested.present?
25
+ end
26
+
27
+ # initialize helper
28
+ #
29
+ def validate_parameters(definition:, components:)
30
+ return :direct if definition.key?('name')
31
+ return :ref if definition.key?('$ref') && components.key?('parameters')
32
+ return :body if definition.key?('content')
33
+
34
+ raise Error, 'no valid combination given'
35
+ end
36
+
37
+ def prepare_attributes(definition:, components:) # rubocop:disable Metrics/MethodLength
38
+ case kind
39
+ when :direct
40
+ @name = definition['name']
41
+ @definition = definition.except('name')
42
+ when :ref
43
+ found = components.dig(*definition['$ref'].split('/')[2..])
44
+ @name = found['name']
45
+ @definition = found.except('name')
46
+
47
+ if (value = @definition.dig('schema', '$ref').presence)
48
+ @definition['schema'] = components.dig(*value.split('/')[2..])
49
+ end
50
+ when :body
51
+ definition['in'] = 'body'
52
+ schema = definition['content'] ? definition['content'].values.first['schema'] : definition
53
+ if schema.key?('$ref')
54
+ path = schema['$ref'].split('/')[2..]
55
+
56
+ @name, @definition = handle_body(definition:, properties: components.dig(*path))
57
+ @name ||= path.last
58
+ else
59
+ @name, @definition = handle_body(definition:, properties: schema)
60
+ @name = nested.map(&:name).join('_') if @name.nil? && nested?
61
+ end
62
+ end
63
+ end
64
+
65
+ def handle_body(definition:, properties:) # rubocop:disable Metrics/MethodLength
66
+ if simple_object?(properties:)
67
+ name = properties['properties'].keys.first
68
+ type = properties.dig('properties', name, 'type') || 'array'
69
+ subtype = properties.dig('properties', name, 'items', 'type')
70
+ definition['type'] = subtype.nil? ? type : "#{type}[#{subtype}]"
71
+
72
+ properties.dig('properties', name).except('type').each { |k, v| definition[k] = v }
73
+ definition['type'] = 'file' if definition['format'].presence == 'binary'
74
+
75
+ [name, definition]
76
+ elsif object?(definition:) # a nested object -> JSON
77
+ definition['type'] = properties['type'].presence || 'JSON'
78
+ return [nil, definition] if properties.nil? || properties['properties'].nil?
79
+
80
+ properties['properties'].each do |nested_name, nested_definition|
81
+ nested_definition['required'] = required?(properties, nested_name)
82
+ nested = NestedParams.new(name: nested_name, definition: nested_definition)
83
+ nested.prepare_attributes(definition: nested.definition, components: {})
84
+ nested.name = nested_name
85
+ @nested << nested
86
+ end
87
+
88
+ [self.name, definition]
89
+ else # others
90
+ [nil, properties ? definition.merge(properties) : definition]
91
+ end
92
+ end
93
+
94
+ # handle_body helper, check/find/define types
95
+ #
96
+ def object?(definition:)
97
+ definition['type'] == 'object' ||
98
+ definition['content']&.keys&.first&.include?('application/json')
99
+ end
100
+
101
+ def simple_object?(properties:)
102
+ list_of_object?(properties:) &&
103
+ properties['properties'].length == 1
104
+ end
105
+
106
+ def list_of_object?(properties:)
107
+ properties&.key?('properties')
108
+ end
109
+
110
+ def required?(property, name)
111
+ return false unless property['required']
112
+
113
+ property['required'].is_a?(Array) ? property['required'].include?(name) : property['required']
114
+ end
115
+
116
+ # to_s helper
117
+ #
118
+ def serialized_object
119
+ definition.tap do |foo|
120
+ foo['type'] = foo['type'] == 'object' ? 'JSON' : foo['type']
121
+ end
122
+
123
+ parent = NestedParams.new(name: name, definition: definition)
124
+ entry = "#{parent} do\n"
125
+ nested.each { |n| entry << " #{n}\n" }
126
+ entry << ' end'
127
+ if entry.include?("format: 'binary', type: 'File'")
128
+ entry.sub!('type: JSON', 'type: Hash')
129
+ entry.sub!(", documentation: { in: 'body' }", '')
130
+ entry.gsub!(", in: 'body'", '')
131
+ end
132
+
133
+ entry
134
+ end
135
+
136
+ def serialized
137
+ type = definition['type'] || definition['schema']['type']
138
+ type.scan(/\w+/).each { |x| type.match?('JSON') ? type : type.sub!(x, x.capitalize) }
139
+
140
+ if type == 'Array' && definition.key?('items')
141
+ sub = definition.dig('items', 'type').to_s.capitalize
142
+ type = "#{type}[#{sub}]"
143
+ end
144
+
145
+ entry = definition['required'] ? 'requires' : 'optional'
146
+ entry << " :#{name}"
147
+ entry << ", type: #{type}"
148
+ doc = documentation
149
+ entry << ", #{doc}" if doc
150
+
151
+ entry
152
+ end
153
+
154
+ def documentation
155
+ @documentation ||= begin
156
+ tmp = {}
157
+ tmp['desc'] = definition['description'] if definition.key?('description')
158
+ if definition.key?('in') && !(definition['type'] == 'File' && definition['format'] == 'binary')
159
+ tmp['in'] = definition['in']
160
+ end
161
+
162
+ if definition.key?('format')
163
+ tmp['format'] = definition['format']
164
+ tmp['type'] = 'File' if definition['format'] == 'binary'
165
+ end
166
+
167
+ documentation = 'documentation:'
168
+ documentation.tap do |doc|
169
+ doc << ' { '
170
+ content = tmp.map { |k, v| "#{k}: '#{v}'" }
171
+ doc << content.join(', ')
172
+ doc << ' }'
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ClassLength
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Starter
4
+ module Importer
5
+ class Specification
6
+ class Error < StandardError; end
7
+
8
+ attr_accessor :openapi, :info,
9
+ :paths, :components, :webhooks
10
+
11
+ def initialize(spec:)
12
+ # mandatory
13
+ @openapi = spec.fetch('openapi')
14
+ @info = spec.fetch('info')
15
+
16
+ # in contrast to the spec, paths are required
17
+ @paths = spec.fetch('paths').except('/').sort.to_h
18
+
19
+ # optional -> not used atm
20
+ @components = spec.fetch('components', false)
21
+ @webhooks = spec.fetch('webhooks', false)
22
+ end
23
+
24
+ def namespaces
25
+ validate_paths
26
+
27
+ @namespaces ||= paths.keys.each_with_object({}) do |path, memo|
28
+ namespace, rest_path = segmentize(path)
29
+
30
+ memo[namespace] ||= {}
31
+ memo[namespace][rest_path] = prepare_verbs(paths[path])
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def validate_paths
38
+ raise Error, '`paths` empty … nothings to do' if paths.empty?
39
+ raise Error, 'only template given' if paths.keys.one? && paths.keys.first.match?(%r{/\{\w*\}})
40
+ end
41
+
42
+ def segmentize(path)
43
+ segments = path.split('/').delete_if(&:empty?)
44
+ ignore = segments.take_while { |x| x =~ /(v)*(\.\d)+/ || x =~ /(v\d)+/ || x == 'api' }
45
+ rest = segments - ignore
46
+
47
+ [rest.shift, rest.empty? ? '/' : "/#{rest.join('/')}"]
48
+ end
49
+
50
+ def prepare_verbs(spec) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
51
+ path_params = nil
52
+ spec.each_with_object({}) do |(verb, content), memo|
53
+ if verb == 'parameters'
54
+ path_params = content
55
+ next
56
+ end
57
+
58
+ memo[verb] = content
59
+ next unless content.key?('parameters') || content.key?('requestBody') || path_params
60
+
61
+ parameters = ((content['parameters'] || path_params || []) + [content['requestBody']]).compact
62
+
63
+ memo[verb]['parameters'] = parameters.each_with_object({}) do |definition, para|
64
+ parameter = Parameter.new(definition:, components:)
65
+ para[parameter.name] = parameter
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Starter
4
+ class Names
5
+ attr_accessor :resource, :origin, :version_klass
6
+
7
+ def initialize(resource)
8
+ @version_klass = false
9
+ @origin = resource
10
+ @resource = if resource.match?(/([[:digit:]][[:punct:]])+/)
11
+ @version_klass = true
12
+ digit = resource.scan(/\d/).first.to_i - 1
13
+ letter = ('a'..'z').to_a[digit]
14
+ "#{letter}_#{resource.tr('.', '_')}"
15
+ else
16
+ resource
17
+ end
18
+ end
19
+
20
+ def klass_name
21
+ return @resource.classify if version_klass
22
+
23
+ for_klass = @resource.tr('-', '/')
24
+ singular? ? for_klass.classify : for_klass.classify.pluralize
25
+ end
26
+
27
+ # rubocop:disable Style/StringConcatenation
28
+ def resource_file
29
+ @resource.tr('/', '-').downcase + '.rb'
30
+ end
31
+ # rubocop:enable Style/StringConcatenation
32
+
33
+ def resource_spec
34
+ resource_file.gsub(/.rb$/, '_spec.rb')
35
+ end
36
+
37
+ # entry in api/base.rb
38
+ def mount_point
39
+ " mount Endpoints::#{klass_name}\n"
40
+ end
41
+
42
+ # endpoints file
43
+ def api_file_name
44
+ File.join(Dir.getwd, 'api', 'endpoints', resource_file)
45
+ end
46
+
47
+ # entities file
48
+ def entity_file_name
49
+ File.join(Dir.getwd, 'api', 'entities', resource_file)
50
+ end
51
+
52
+ # lib file
53
+ def lib_file_name
54
+ File.join(Dir.getwd, 'lib', 'models', resource_file)
55
+ end
56
+
57
+ # model entry in lib file
58
+ def lib_klass_name
59
+ return klass_name unless @orm
60
+
61
+ case Starter::Config.read[:orm]
62
+ when 'sequel'
63
+ extend(Starter::Builder::Sequel)
64
+ "#{klass_name} < #{model_klass}"
65
+ when 'activerecord', 'ar'
66
+ extend(Starter::Builder::ActiveRecord)
67
+ "#{klass_name} < #{model_klass}"
68
+ else
69
+ klass_name
70
+ end
71
+ end
72
+
73
+ # resource spec
74
+ def api_spec_name
75
+ File.join(Dir.getwd, 'spec', 'requests', resource_spec)
76
+ end
77
+
78
+ # lib spec
79
+ def lib_spec_name
80
+ File.join(Dir.getwd, 'spec', 'lib', 'models', resource_spec)
81
+ end
82
+
83
+ def singular?
84
+ @resource.singularize.inspect == @resource.inspect
85
+ end
86
+ end
87
+ end
@@ -1,23 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Starter
4
- module Builder
4
+ module Shared
5
5
  module BaseFile
6
6
  # add it in api base
7
7
  def add_mount_point
8
- FileOps.call!(@naming.api_base_file_name) { |content| add_to_base(content) }
8
+ FileOps.call!(base_file_path) { |content| add_to_base(content) }
9
9
  end
10
10
 
11
11
  # adding mount point to base class
12
12
  def add_to_base(file)
13
- occurence = file.scan(/(\s+mount\s.*?\n)/).last.first
13
+ occurence = file.scan(/(\s+mount\s.*?\n)/).map(&:first).join
14
+ return if occurence.include?(@naming.mount_point.strip)
15
+
14
16
  replacement = occurence + @naming.mount_point
15
17
  file.sub!(occurence, replacement)
16
18
  end
17
19
 
18
20
  # removes in api base
19
21
  def remove_mount_point
20
- FileOps.call!(@naming.api_base_file_name) { |content| remove_from_base(content) }
22
+ FileOps.call!(base_file_path) { |content| remove_from_base(content) }
21
23
  end
22
24
 
23
25
  # removes mount point from base class
@@ -39,8 +41,11 @@ module Starter
39
41
 
40
42
  # get api base file as string
41
43
  def base_file
42
- file = File.join(Dir.getwd, 'api', 'base.rb')
43
- FileOps.read_file(file)
44
+ FileOps.read_file(base_file_path)
45
+ end
46
+
47
+ def base_file_path
48
+ File.join(Dir.getwd, 'api', 'base.rb')
44
49
  end
45
50
  end
46
51
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Starter
4
- VERSION = '1.6.2'
4
+ VERSION = '2.0.0'
5
5
  end
data/lib/starter.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'awesome_print'
4
- require 'active_support'
5
- require 'active_support/core_ext/string'
4
+ require 'active_support/all'
6
5
  require 'zeitwerk'
7
6
 
8
7
  loader = Zeitwerk::Loader.for_gem
@@ -25,3 +25,6 @@ Style/AsciiComments:
25
25
 
26
26
  Style/Documentation:
27
27
  Enabled: false
28
+
29
+ Style/RedundantArrayConstructor:
30
+ Enabled: false
data/template/Dockerfile CHANGED
@@ -4,7 +4,7 @@ FROM ruby:3
4
4
  ENV NODE_ENV='development'
5
5
  ENV RACK_ENV='development'
6
6
 
7
- ADD . /dummy
7
+ COPY . /dummy
8
8
  WORKDIR /dummy
9
9
 
10
10
  COPY Gemfile Gemfile.lock ./
data/template/Gemfile CHANGED
@@ -5,7 +5,7 @@ source 'http://rubygems.org'
5
5
  # Server stuff
6
6
  gem 'puma'
7
7
 
8
- gem 'rack'
8
+ gem 'rack', '< 3.0'
9
9
  gem 'rack-cors'
10
10
 
11
11
  # API stuff
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'rubygems'
4
4
  require 'bundler/setup'
5
- require 'active_support'
5
+ require 'active_support/all'
6
6
 
7
7
  Bundler.require :default, ENV.fetch('RACK_ENV', nil)
8
8
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape-starter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LeFnord
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-12 00:00:00.000000000 Z
11
+ date: 2023-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gli
@@ -112,16 +112,21 @@ files:
112
112
  - lib/starter.rb
113
113
  - lib/starter/build.rb
114
114
  - lib/starter/builder/activerecord.rb
115
- - lib/starter/builder/base_file.rb
116
- - lib/starter/builder/names.rb
115
+ - lib/starter/builder/endpoints.rb
116
+ - lib/starter/builder/files.rb
117
117
  - lib/starter/builder/sequel.rb
118
118
  - lib/starter/config.rb
119
119
  - lib/starter/file_ops.rb
120
+ - lib/starter/import.rb
121
+ - lib/starter/importer/namespace.rb
122
+ - lib/starter/importer/nested_params.rb
123
+ - lib/starter/importer/parameter.rb
124
+ - lib/starter/importer/specification.rb
125
+ - lib/starter/names.rb
120
126
  - lib/starter/orms.rb
121
127
  - lib/starter/rake/grape_tasks.rb
122
128
  - lib/starter/rspec/request_specs.rb
123
- - lib/starter/templates/endpoints.rb
124
- - lib/starter/templates/files.rb
129
+ - lib/starter/shared/base_file.rb
125
130
  - lib/starter/version.rb
126
131
  - template/.gitignore
127
132
  - template/.rubocop.yml
@@ -164,14 +169,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
169
  requirements:
165
170
  - - ">="
166
171
  - !ruby/object:Gem::Version
167
- version: '2.7'
172
+ version: '3.1'
168
173
  required_rubygems_version: !ruby/object:Gem::Requirement
169
174
  requirements:
170
175
  - - ">="
171
176
  - !ruby/object:Gem::Version
172
177
  version: '0'
173
178
  requirements: []
174
- rubygems_version: 3.4.7
179
+ rubygems_version: 3.4.17
175
180
  signing_key:
176
181
  specification_version: 4
177
182
  summary: Creates a Grape Rack skeleton
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Starter
4
- module Builder
5
- class Names
6
- def initialize(resource)
7
- @resource = resource
8
- end
9
-
10
- def klass_name
11
- for_klass = prepare_klass
12
- singular? ? for_klass.classify : for_klass.classify.pluralize
13
- end
14
-
15
- # rubocop:disable Style/StringConcatenation
16
- def base_file_name
17
- @resource.tr('/', '-').downcase + '.rb'
18
- end
19
- # rubocop:enable Style/StringConcatenation
20
-
21
- def api_base_file_name
22
- File.join(Dir.getwd, 'api', 'base.rb')
23
- end
24
-
25
- def base_spec_name
26
- base_file_name.gsub(/.rb$/, '_spec.rb')
27
- end
28
-
29
- # entry in api/base.rb
30
- def mount_point
31
- " mount Endpoints::#{klass_name}\n"
32
- end
33
-
34
- # endpoints file
35
- def api_file_name
36
- File.join(Dir.getwd, 'api', 'endpoints', base_file_name)
37
- end
38
-
39
- # entities file
40
- def entity_file_name
41
- File.join(Dir.getwd, 'api', 'entities', base_file_name)
42
- end
43
-
44
- # lib file
45
- def lib_file_name
46
- File.join(Dir.getwd, 'lib', 'models', base_file_name)
47
- end
48
-
49
- # model entry in lib file
50
- def lib_klass_name
51
- return klass_name unless @orm
52
-
53
- case Starter::Config.read[:orm]
54
- when 'sequel'
55
- extend(Starter::Builder::Sequel)
56
- "#{klass_name} < #{model_klass}"
57
- when 'activerecord', 'ar'
58
- extend(Starter::Builder::ActiveRecord)
59
- "#{klass_name} < #{model_klass}"
60
- else
61
- klass_name
62
- end
63
- end
64
-
65
- # resource spec
66
- def api_spec_name
67
- File.join(Dir.getwd, 'spec', 'requests', base_spec_name)
68
- end
69
-
70
- # lib spec
71
- def lib_spec_name
72
- File.join(Dir.getwd, 'spec', 'lib', 'models', base_spec_name)
73
- end
74
-
75
- def singular?
76
- @resource.singularize.inspect == @resource.inspect
77
- end
78
-
79
- private
80
-
81
- def prepare_klass
82
- @resource.tr('-', '/')
83
- end
84
- end
85
- end
86
- end