grape-starter 1.6.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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