typespec_from_serializers 0.1.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 +7 -0
- data/CHANGELOG.md +102 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/lib/typespec_from_serializers/dsl.rb +46 -0
- data/lib/typespec_from_serializers/generator.rb +622 -0
- data/lib/typespec_from_serializers/railtie.rb +48 -0
- data/lib/typespec_from_serializers/version.rb +6 -0
- data/lib/typespec_from_serializers.rb +5 -0
- metadata +285 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8e14e0d2601ec8ee7b5efa388163e3997830d825b6f98f9df7e40ecd0248a8f6
|
4
|
+
data.tar.gz: d15e88b3aa25f587697c2614b867ccaa31b3438c08d11ca12bd17ddf752313b8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a2a246f2e90ce1fb583ce3bd1abc4c621c36f69fca705c853360e371beb22f668aa51ba043e4b7c3fdec9e2055577dd5df9b92c645984a4af81543aec546826b
|
7
|
+
data.tar.gz: 47249bf5f3c9f9af0c801790f65d0d8aa829071770f7590c15897c73fe286af61922bf92d9985bea9acb201c7d43b8c9fb5d84f2a5e71bb9720dd2a2c1d90fce
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# [2.3.0](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@2.2.0...types_from_serializers@2.3.0) (2024-08-23)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* generate types for inline serializers, exclude them from the index ([1c3657c](https://github.com/ElMassimo/types_from_serializers/commit/1c3657c61a1bc891f3219f6eaf8557cd3cd6344a)), closes [#19](https://github.com/ElMassimo/types_from_serializers/issues/19)
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
# [2.2.0](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@2.1.0...types_from_serializers@2.2.0) (2024-08-23)
|
11
|
+
|
12
|
+
|
13
|
+
### Bug Fixes
|
14
|
+
|
15
|
+
* be more accurate regarding decimal serialization ([cd63653](https://github.com/ElMassimo/types_from_serializers/commit/cd636530a5710112a14746cc7d0e3f15016cd5e1))
|
16
|
+
|
17
|
+
|
18
|
+
### Features
|
19
|
+
|
20
|
+
* infer type from enums ([#20](https://github.com/ElMassimo/types_from_serializers/issues/20)) ([49dc61d](https://github.com/ElMassimo/types_from_serializers/commit/49dc61da2718256e9b5f5743b5a65c4746d64c2f))
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
# [2.1.0](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@2.0.2...types_from_serializers@2.1.0) (2023-07-19)
|
25
|
+
|
26
|
+
|
27
|
+
### Features
|
28
|
+
|
29
|
+
* add `namespace` option to generate `.d.ts` files ([#9](https://github.com/ElMassimo/types_from_serializers/issues/9)) ([6f67b1a](https://github.com/ElMassimo/types_from_serializers/commit/6f67b1ad9283868e8e3325042645bceccc85b047))
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
## [2.0.2](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@2.0.1...types_from_serializers@2.0.2) (2023-04-05)
|
34
|
+
|
35
|
+
|
36
|
+
### Features
|
37
|
+
|
38
|
+
* map citext from PostgreSQL to string ([#7](https://github.com/ElMassimo/types_from_serializers/issues/7)) ([d8c6848](https://github.com/ElMassimo/types_from_serializers/commit/d8c6848b99b0f4ba3770871f491755c229a2c4b0))
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
## [2.0.1](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@2.0.0...types_from_serializers@2.0.1) (2023-04-03)
|
43
|
+
|
44
|
+
|
45
|
+
### Bug Fixes
|
46
|
+
|
47
|
+
* `add_attribute` now expects keyword arguments ([154b49e](https://github.com/ElMassimo/types_from_serializers/commit/154b49e463e3e6533b21520b7f0d699e6f0f47ba))
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
## [2.0.0](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@0.1.2...types_from_serializers@2.0.0) (2023-04-02)
|
52
|
+
|
53
|
+
This version adds support for `oj_serializers-2.0.2`, supporting all changes in:
|
54
|
+
|
55
|
+
- https://github.com/ElMassimo/oj_serializers/pull/9
|
56
|
+
|
57
|
+
### Features ✨
|
58
|
+
|
59
|
+
- Now keys will match the [`transform_keys`](https://github.com/ElMassimo/oj_serializers#transforming-attribute-keys-) configuration instead of always being camelized
|
60
|
+
- Support for [`flat_one`](https://github.com/ElMassimo/oj_serializers#composing-serializers-)
|
61
|
+
- Use relative paths for imports to make the output configuration more flexible
|
62
|
+
- Define the order of properties in the interface with `sort_properties_by`
|
63
|
+
|
64
|
+
## [0.1.3](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@0.1.2...types_from_serializers@0.1.3) (2022-07-12)
|
65
|
+
|
66
|
+
|
67
|
+
### Features
|
68
|
+
|
69
|
+
* apply the sql mapping fallback as the default ([64898c4](https://github.com/ElMassimo/types_from_serializers/commit/64898c4e3a3f83ea67294f2200f253cd2a64aea9))
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
## [0.1.2](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@0.1.1...types_from_serializers@0.1.2) (2022-07-12)
|
74
|
+
|
75
|
+
|
76
|
+
### Bug Fixes
|
77
|
+
|
78
|
+
* avoid having the full file path in the cache key ([556f8f6](https://github.com/ElMassimo/types_from_serializers/commit/556f8f667608fa950a3ad0647540055b1b5f1dc8))
|
79
|
+
|
80
|
+
|
81
|
+
|
82
|
+
## [0.1.1](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@0.1.0...types_from_serializers@0.1.1) (2022-07-12)
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
# 0.1.0 (2022-07-12)
|
87
|
+
|
88
|
+
|
89
|
+
### Features
|
90
|
+
|
91
|
+
- Start simple, no additional syntax required
|
92
|
+
- Infers types from a related `ActiveRecord` model, using the SQL schema
|
93
|
+
- Understands JS native types and how to map SQL columns: `string`, `boolean`, etc
|
94
|
+
- Automatically types [associations](https://github.com/ElMassimo/oj_serializers#associations-dsl-), importing the generated types for the referenced serializers
|
95
|
+
- Detects [conditional attributes](https://github.com/ElMassimo/oj_serializers#rendering-an-attribute-conditionally) and marks them as optional: `name?: string`
|
96
|
+
- Fallback to a custom interface using `type_from`
|
97
|
+
- Supports custom types and automatically adds the necessary imports
|
98
|
+
- handle non-ActiveRecord models and extract types from unions ([ea9b2a7](https://github.com/ElMassimo/types_from_serializers/commit/ea9b2a71cb85503ff691e5ef115ab73f89b005af))
|
99
|
+
- support specifying base serializers and additional dirs to scan ([164cfe1](https://github.com/ElMassimo/types_from_serializers/commit/164cfe17bb0527c59cf95441381aef7bf797a568))
|
100
|
+
|
101
|
+
|
102
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2021 Máximo Mussini
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
<h1 align="center">
|
2
|
+
TypeSpec From Serializers
|
3
|
+
<p align="center">
|
4
|
+
<a href="https://travis-ci.org/dannote/typespec_from_serializers"><img alt="Build Status" src="https://travis-ci.org/dannote/typespec_from_serializers.svg"/></a>
|
5
|
+
<a href="http://inch-ci.org/github/dannote/typespec_from_serializers"><img alt="Inline docs" src="http://inch-ci.org/github/dannote/typespec_from_serializers.svg"/></a>
|
6
|
+
<a href="https://codeclimate.com/github/dannote/typespec_from_serializers"><img alt="Maintainability" src="https://codeclimate.com/github/dannote/typespec_from_serializers/badges/gpa.svg"/></a>
|
7
|
+
<a href="https://codeclimate.com/github/dannote/typespec_from_serializers"><img alt="Test Coverage" src="https://codeclimate.com/github/dannote/typespec_from_serializers/badges/coverage.svg"/></a>
|
8
|
+
<a href="https://rubygems.org/gems/typespec_from_serializers"><img alt="Gem Version" src="https://img.shields.io/gem/v/typespec_from_serializers.svg?colorB=e9573f"/></a>
|
9
|
+
<a href="https://github.com/dannote/typespec_from_serializers/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/></a>
|
10
|
+
</p>
|
11
|
+
</h1>
|
12
|
+
|
13
|
+
[aliases]: https://vite-ruby.netlify.app/guide/development.html#import-aliases-%F0%9F%91%89
|
14
|
+
[config options]: https://github.com/dannote/typespec_from_serializers/blob/main/lib/typespec_from_serializers/generator.rb#L82-L85
|
15
|
+
[readme]: https://github.com/dannote/typespec_from_serializers
|
16
|
+
|
17
|
+
**TypeSpec From Serializers** is a Ruby gem that automatically generates [TypeSpec](https://typespec.io) definitions from Ruby serializers and Rails routes. It is a derivative work of [`types_from_serializers`][types_from_serializers] by ElMassimo, originally designed to generate TypeScript definitions. This fork adapts the core functionality to produce TypeSpec descriptions, enabling Rails developers to define APIs compatible with TypeSpec’s ecosystem, including OpenAPI generation and client/server code scaffolding.
|
18
|
+
|
19
|
+
For more information, check the main [README].
|
20
|
+
|
21
|
+
### Installation 💿
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem 'typespec_from_serializers'
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install typespec_from_serializers
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
# Internal: A DSL to specify types for serializer attributes.
|
6
|
+
module TypeSpecFromSerializers
|
7
|
+
module DSL
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Override: Capture the name of the model related to the serializer.
|
12
|
+
#
|
13
|
+
# name - An alias for the internal object in the serializer.
|
14
|
+
# model - The name of an ActiveRecord model to infer types from the schema.
|
15
|
+
# typespec_from - The name of a TypeScript model to infer types from.
|
16
|
+
def object_as(name, model: nil, typespec_from: nil)
|
17
|
+
# NOTE: Avoid taking memory for type information that won't be used.
|
18
|
+
if Rails.env.development?
|
19
|
+
model ||= name.is_a?(Symbol) ? name : try(:_serializer_model_name) || name
|
20
|
+
define_singleton_method(:_serializer_model_name) { model }
|
21
|
+
define_singleton_method(:_serializer_typespec_from) { typespec_from } if typespec_from
|
22
|
+
end
|
23
|
+
|
24
|
+
super(name)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Shortcut for typing a serializer attribute.
|
28
|
+
#
|
29
|
+
# It specifies the type for a serializer method that will be defined
|
30
|
+
# immediately after calling this method.
|
31
|
+
def type(type, **options)
|
32
|
+
attribute type: type, **options
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Override: Remove unnecessary options in production, types are only
|
38
|
+
# used when generating code in development.
|
39
|
+
unless Rails.env.development?
|
40
|
+
def add_attribute(name, type: nil, optional: nil, **options)
|
41
|
+
super(name, **options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,622 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
require "fileutils"
|
5
|
+
require "pathname"
|
6
|
+
|
7
|
+
# Public: Automatically generates TypeSpec descriptions for Ruby serializers and Rails routes.
|
8
|
+
module TypeSpecFromSerializers
|
9
|
+
DEFAULT_TRANSFORM_KEYS = ->(key) { key.camelize(:lower).chomp("?") }
|
10
|
+
|
11
|
+
# Internal: Extensions that simplify the implementation of the generator.
|
12
|
+
module SerializerRefinements
|
13
|
+
refine String do
|
14
|
+
# Internal: Converts a name such as :user to the User constant.
|
15
|
+
def to_model
|
16
|
+
classify.safe_constantize
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
refine Symbol do
|
21
|
+
def safe_constantize
|
22
|
+
to_s.classify.safe_constantize
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_model
|
26
|
+
to_s.to_model
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
refine Class do
|
31
|
+
# Internal: Name of the TypeSpec model.
|
32
|
+
def tsp_name
|
33
|
+
TypeSpecFromSerializers.config.name_from_serializer.call(name).tr_s(":", "")
|
34
|
+
end
|
35
|
+
|
36
|
+
# Internal: The base name of the TypeSpec file to be written.
|
37
|
+
def tsp_filename
|
38
|
+
TypeSpecFromSerializers.config.name_from_serializer.call(name).gsub("::", "/")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Internal: If the serializer was defined inside a file.
|
42
|
+
def inline_serializer?
|
43
|
+
name.include?("Serializer::")
|
44
|
+
end
|
45
|
+
|
46
|
+
# Internal: The TypeSpec properties of the serialzeir model.
|
47
|
+
def tsp_properties
|
48
|
+
@tsp_properties ||= begin
|
49
|
+
model_class = _serializer_model_name&.to_model
|
50
|
+
model_columns = model_class.try(:columns_hash) || {}
|
51
|
+
model_enums = model_class.try(:defined_enums) || {}
|
52
|
+
typespec_from = try(:_serializer_typespec_from)
|
53
|
+
|
54
|
+
prepare_attributes(
|
55
|
+
sort_by: TypeSpecFromSerializers.config.sort_properties_by,
|
56
|
+
transform_keys: TypeSpecFromSerializers.config.transform_keys || try(:_transform_keys) || DEFAULT_TRANSFORM_KEYS,
|
57
|
+
)
|
58
|
+
.flat_map { |key, options|
|
59
|
+
if options[:association] == :flat
|
60
|
+
options.fetch(:serializer).tsp_properties
|
61
|
+
else
|
62
|
+
Property.new(
|
63
|
+
name: key,
|
64
|
+
type: options[:serializer] || options[:type],
|
65
|
+
optional: options[:optional] || options.key?(:if),
|
66
|
+
multi: options[:association] == :many,
|
67
|
+
column_name: options.fetch(:value_from),
|
68
|
+
).tap do |property|
|
69
|
+
property.infer_typespec_from(model_columns, model_enums, typespec_from)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Internal: A first pass of gathering types for the serializer attributes.
|
77
|
+
def tsp_model
|
78
|
+
@tsp_model ||= Interface.new(
|
79
|
+
name: tsp_name,
|
80
|
+
filename: tsp_filename,
|
81
|
+
properties: tsp_properties,
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Internal: The configuration for TypeSpec generation.
|
88
|
+
Config = Struct.new(
|
89
|
+
:base_serializers,
|
90
|
+
:serializers_dirs,
|
91
|
+
:output_dir,
|
92
|
+
:custom_typespec_dir,
|
93
|
+
:name_from_serializer,
|
94
|
+
:global_types,
|
95
|
+
:sort_properties_by,
|
96
|
+
:sql_to_typespec_type_mapping,
|
97
|
+
:action_to_operation_mapping,
|
98
|
+
:skip_serializer_if,
|
99
|
+
:transform_keys,
|
100
|
+
:namespace,
|
101
|
+
keyword_init: true,
|
102
|
+
) do
|
103
|
+
def relative_custom_typespec_dir
|
104
|
+
@relative_custom_typespec_dir ||= (custom_typespec_dir || output_dir.parent).relative_path_from(output_dir.join("models"))
|
105
|
+
end
|
106
|
+
|
107
|
+
def unknown_type
|
108
|
+
:unknown
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Internal: Information to generate a TypeSpec model for a serializer.
|
113
|
+
Interface = Struct.new(
|
114
|
+
:name,
|
115
|
+
:filename,
|
116
|
+
:properties,
|
117
|
+
keyword_init: true,
|
118
|
+
) do
|
119
|
+
using SerializerRefinements
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
to_h.inspect
|
123
|
+
end
|
124
|
+
|
125
|
+
# Internal: Returns a list of imports for types used in this model.
|
126
|
+
def used_imports
|
127
|
+
association_serializers, attribute_types = properties.map(&:type).compact.uniq
|
128
|
+
.partition { |type| type.respond_to?(:tsp_model) }
|
129
|
+
|
130
|
+
serializer_type_imports = association_serializers.map(&:tsp_model)
|
131
|
+
.map { |type| [type.name, relative_path(type.pathname, pathname)] }
|
132
|
+
|
133
|
+
custom_type_imports = attribute_types
|
134
|
+
.flat_map { |type| extract_typespec_types(type.to_s) }
|
135
|
+
.uniq
|
136
|
+
.reject { |type| global_type?(type) }
|
137
|
+
.map { |type|
|
138
|
+
type_path = TypeSpecFromSerializers.config.relative_custom_typespec_dir.join(type)
|
139
|
+
[type, relative_path(type_path, pathname)]
|
140
|
+
}
|
141
|
+
|
142
|
+
(custom_type_imports + serializer_type_imports)
|
143
|
+
.map { |model, filename| %(import "#{filename}.tsp";\n) }
|
144
|
+
end
|
145
|
+
|
146
|
+
def as_typespec
|
147
|
+
indent = TypeSpecFromSerializers.config.namespace ? 2 : 1
|
148
|
+
<<~TSP.gsub(/\n$/, "")
|
149
|
+
model #{name} {
|
150
|
+
#{" " * indent}#{properties.index_by(&:name).values.map(&:as_typespec).join("\n#{" " * indent}")}
|
151
|
+
#{" " * (indent - 1)}}
|
152
|
+
TSP
|
153
|
+
end
|
154
|
+
|
155
|
+
protected
|
156
|
+
|
157
|
+
def pathname
|
158
|
+
@pathname ||= Pathname.new(filename)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Internal: Calculates a relative path that can be used in an import.
|
162
|
+
def relative_path(target_path, importer_path)
|
163
|
+
path = target_path.relative_path_from(importer_path.parent).to_s
|
164
|
+
path.start_with?(".") ? path : "./#{path}"
|
165
|
+
end
|
166
|
+
|
167
|
+
# Internal: Extracts any types inside generics or array types.
|
168
|
+
def extract_typespec_types(type)
|
169
|
+
type.split(".").first
|
170
|
+
end
|
171
|
+
|
172
|
+
# NOTE: Treat uppercase names as custom types.
|
173
|
+
# Lowercase names would be native types, such as :string and :boolean.
|
174
|
+
def global_type?(type)
|
175
|
+
type[0] == type[0].downcase || TypeSpecFromSerializers.config.global_types.include?(type)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Internal: The type metadata for a serializer attribute.
|
180
|
+
Property = Struct.new(
|
181
|
+
:name,
|
182
|
+
:type,
|
183
|
+
:optional,
|
184
|
+
:multi,
|
185
|
+
:column_name,
|
186
|
+
keyword_init: true,
|
187
|
+
) do
|
188
|
+
using SerializerRefinements
|
189
|
+
|
190
|
+
def inspect
|
191
|
+
to_h.inspect
|
192
|
+
end
|
193
|
+
|
194
|
+
# Internal: Infers the property's type by checking a corresponding SQL
|
195
|
+
# column, or falling back to a TypeSpec model if provided.
|
196
|
+
def infer_typespec_from(columns_hash, defined_enums, tsp_model)
|
197
|
+
if type
|
198
|
+
type
|
199
|
+
elsif (enum = defined_enums[column_name.to_s])
|
200
|
+
self.type = enum.keys.map(&:inspect).join(" | ")
|
201
|
+
elsif (column = columns_hash[column_name.to_s])
|
202
|
+
self.multi = true if column.try(:array)
|
203
|
+
self.optional = true if column.null && !column.default
|
204
|
+
self.type = TypeSpecFromSerializers.config.sql_to_typespec_type_mapping[column.type]
|
205
|
+
elsif tsp_model
|
206
|
+
self.type = "#{tsp_model}.#{name}::type"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def as_typespec
|
211
|
+
type_str = if type.respond_to?(:tsp_name)
|
212
|
+
type.tsp_name
|
213
|
+
else
|
214
|
+
type || TypeSpecFromSerializers.config.unknown_type
|
215
|
+
end
|
216
|
+
|
217
|
+
"#{name}#{"?" if optional}: #{type_str}#{"[]" if multi};"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Internal: Represents a TypeSpec resource interface
|
222
|
+
Resource = Struct.new(:name, :path, :operations, keyword_init: true) do
|
223
|
+
def as_typespec
|
224
|
+
<<~TSP
|
225
|
+
#{" " * 1}@route("#{path}")
|
226
|
+
#{" " * 1}interface #{name} {
|
227
|
+
#{" " * 1}#{operations.map(&:as_typespec).join("\n ")}
|
228
|
+
#{" " * 1}}
|
229
|
+
TSP
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Internal: Represents a TypeSpec operation within a resource
|
234
|
+
Operation = Struct.new(:method, :action, :path_params, :response_type, keyword_init: true) do
|
235
|
+
def as_typespec
|
236
|
+
method_map = {
|
237
|
+
"GET" => "get",
|
238
|
+
"POST" => "post",
|
239
|
+
"PUT" => "put",
|
240
|
+
"PATCH" => "patch",
|
241
|
+
"DELETE" => "delete",
|
242
|
+
}
|
243
|
+
tsp_method = method_map[method] || method.downcase
|
244
|
+
operation_name = TypeSpecFromSerializers.config.action_to_operation_mapping[action] || action
|
245
|
+
params = params_typespec
|
246
|
+
params_str = params.empty? ? "()" : "(#{params})"
|
247
|
+
|
248
|
+
"#{" " * 1}@#{tsp_method} #{operation_name}#{params_str}: #{response_type.gsub("::", "")};"
|
249
|
+
end
|
250
|
+
|
251
|
+
def params_typespec
|
252
|
+
params = []
|
253
|
+
params += path_params.map { |param| "@path #{param}: string" } if path_params.any?
|
254
|
+
params << "@body body: #{response_type.gsub("::", "")}" if %w[POST PUT PATCH].include?(method)
|
255
|
+
params.join(", ")
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Internal: Structure to keep track of changed files.
|
260
|
+
class Changes
|
261
|
+
def initialize(dirs)
|
262
|
+
@added = Set.new
|
263
|
+
@removed = Set.new
|
264
|
+
@modified = Set.new
|
265
|
+
track_changes(dirs)
|
266
|
+
end
|
267
|
+
|
268
|
+
def updated?
|
269
|
+
@modified.any? || @added.any? || @removed.any?
|
270
|
+
end
|
271
|
+
|
272
|
+
def any_removed?
|
273
|
+
@removed.any?
|
274
|
+
end
|
275
|
+
|
276
|
+
def modified_files
|
277
|
+
@modified
|
278
|
+
end
|
279
|
+
|
280
|
+
def only_modified?
|
281
|
+
@added.empty? && @removed.empty?
|
282
|
+
end
|
283
|
+
|
284
|
+
def clear
|
285
|
+
@added.clear
|
286
|
+
@removed.clear
|
287
|
+
@modified.clear
|
288
|
+
end
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
def track_changes(dirs)
|
293
|
+
Listen.to(*dirs, only: %r{.rb$}) do |modified, added, removed|
|
294
|
+
modified.each { |file| @modified.add(file) }
|
295
|
+
added.each { |file| @added.add(file) }
|
296
|
+
removed.each { |file| @removed.add(file) }
|
297
|
+
end.start
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
class << self
|
302
|
+
using SerializerRefinements
|
303
|
+
|
304
|
+
attr_reader :force_generation
|
305
|
+
|
306
|
+
# Public: Configuration of the code generator.
|
307
|
+
def config
|
308
|
+
(@config ||= default_config(root)).tap do |config|
|
309
|
+
yield(config) if block_given?
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Public: Generates code for all serializers in the app.
|
314
|
+
def generate(force: ENV["SERIALIZER_TYPESPEC_FORCE"])
|
315
|
+
@force_generation = force
|
316
|
+
config.output_dir.rmtree if force && config.output_dir.exist?
|
317
|
+
|
318
|
+
if config.namespace
|
319
|
+
load_serializers(all_serializer_files) if force
|
320
|
+
else
|
321
|
+
generate_index_file
|
322
|
+
end
|
323
|
+
|
324
|
+
generate_routes
|
325
|
+
|
326
|
+
loaded_serializers.each do |serializer|
|
327
|
+
generate_model_for(serializer)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def generate_changed
|
332
|
+
if changes.updated?
|
333
|
+
config.output_dir.rmtree if changes.any_removed?
|
334
|
+
load_serializers(changes.modified_files)
|
335
|
+
generate
|
336
|
+
changes.clear
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# Internal: Defines a TypeSpec model for the serializer.
|
341
|
+
def generate_model_for(serializer)
|
342
|
+
model = serializer.tsp_model
|
343
|
+
|
344
|
+
write_if_changed(filename: "models/#{model.filename}", cache_key: model.inspect, extension: "tsp") {
|
345
|
+
serializer_model_content(model)
|
346
|
+
}
|
347
|
+
end
|
348
|
+
|
349
|
+
# Internal: Allows to import all serializer types from a single file.
|
350
|
+
def generate_index_file
|
351
|
+
cache_key = all_serializer_files.map { |file| file.delete_prefix(root.to_s) }.join
|
352
|
+
write_if_changed(filename: "index", cache_key: cache_key) {
|
353
|
+
load_serializers(all_serializer_files)
|
354
|
+
serializers_index_content(loaded_serializers)
|
355
|
+
}
|
356
|
+
end
|
357
|
+
|
358
|
+
# Internal: Generates TypeSpec routes from Rails routes
|
359
|
+
def generate_routes
|
360
|
+
return unless defined?(Rails) && Rails.application
|
361
|
+
|
362
|
+
routes = collect_rails_routes
|
363
|
+
cache_key = routes.map { |r| r.operations.map { |op| "#{op.method}#{r.path}#{op.action}" }.join }.join
|
364
|
+
write_if_changed(filename: "routes", cache_key: cache_key) {
|
365
|
+
routes_content(routes)
|
366
|
+
}
|
367
|
+
end
|
368
|
+
|
369
|
+
# Internal: Checks if it should avoid generating an model.
|
370
|
+
def skip_serializer?(serializer)
|
371
|
+
serializer.name.in?(config.base_serializers) ||
|
372
|
+
config.skip_serializer_if.call(serializer)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Internal: Returns an object compatible with FileUpdateChecker.
|
376
|
+
def track_changes
|
377
|
+
changes
|
378
|
+
end
|
379
|
+
|
380
|
+
private
|
381
|
+
|
382
|
+
def root
|
383
|
+
defined?(Rails) ? Rails.root : Pathname.new(Dir.pwd)
|
384
|
+
end
|
385
|
+
|
386
|
+
def changes
|
387
|
+
@changes ||= Changes.new(config.serializers_dirs)
|
388
|
+
end
|
389
|
+
|
390
|
+
def all_serializer_files
|
391
|
+
config.serializers_dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] }.sort
|
392
|
+
end
|
393
|
+
|
394
|
+
def load_serializers(files)
|
395
|
+
files.each { |file| require file }
|
396
|
+
end
|
397
|
+
|
398
|
+
def loaded_serializers
|
399
|
+
config.base_serializers.map(&:constantize)
|
400
|
+
.flat_map(&:descendants)
|
401
|
+
.uniq
|
402
|
+
.sort_by(&:name)
|
403
|
+
.reject { |s| skip_serializer?(s) }
|
404
|
+
rescue NameError
|
405
|
+
raise ArgumentError, "Please ensure all your serializers extend BaseSerializer, or configure `config.base_serializers`."
|
406
|
+
end
|
407
|
+
|
408
|
+
# Internal: Collects routes from Rails and groups them into resources
|
409
|
+
def collect_rails_routes
|
410
|
+
return [] unless defined?(Rails) && Rails.application
|
411
|
+
|
412
|
+
routes_by_controller = Rails.application.routes.routes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |route, hash|
|
413
|
+
next unless route.defaults[:controller] && route.verb.present?
|
414
|
+
|
415
|
+
controller = route.defaults[:controller]
|
416
|
+
action = route.defaults[:action]
|
417
|
+
method = route.verb.split("|").first
|
418
|
+
path = route.path.spec.to_s.sub("(.:format)", "")
|
419
|
+
response_type = infer_response_type(controller, action) || "unknown"
|
420
|
+
|
421
|
+
unless hash[controller].any? { |r| r[:method] == method && r[:action] == action }
|
422
|
+
hash[controller] << {
|
423
|
+
method: method,
|
424
|
+
action: action,
|
425
|
+
path: path,
|
426
|
+
response_type: response_type,
|
427
|
+
}
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
routes_by_controller.map do |controller, routes|
|
432
|
+
path_segments = routes.map { |r| r[:path].split("/")[1..-1] || [] }.uniq.sort_by(&:length)
|
433
|
+
base_path = path_segments.any? ? path_segments.first.join("/")&.split("/{")&.first || controller : controller
|
434
|
+
|
435
|
+
operations = routes.map do |route|
|
436
|
+
path_params = route[:path].scan(/{([^}]+)}/).flatten
|
437
|
+
response_type = if route[:response_type] == route[:action]
|
438
|
+
"unknown"
|
439
|
+
else
|
440
|
+
(route[:action] == "index") ? "#{route[:response_type]}[]" : route[:response_type]
|
441
|
+
end
|
442
|
+
Operation.new(
|
443
|
+
method: route[:method],
|
444
|
+
action: route[:action],
|
445
|
+
path_params: (route[:action] == "show") ? ["id"] : path_params,
|
446
|
+
response_type: response_type,
|
447
|
+
)
|
448
|
+
end
|
449
|
+
Resource.new(
|
450
|
+
name: controller.tr("/", "_").camelize,
|
451
|
+
path: "/#{base_path}",
|
452
|
+
operations: operations,
|
453
|
+
)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# Internal: Infers the response type based on controller and action
|
458
|
+
def infer_response_type(controller, action)
|
459
|
+
controller_class = "#{controller.camelize}Controller".safe_constantize
|
460
|
+
return nil unless controller_class
|
461
|
+
|
462
|
+
model_name = controller.singularize.camelize
|
463
|
+
serializer_class = "#{model_name}Serializer".safe_constantize
|
464
|
+
serializer_class&.tsp_name
|
465
|
+
end
|
466
|
+
|
467
|
+
# Internal: Generates the routes.tsp content with resources
|
468
|
+
def routes_content(routes)
|
469
|
+
imports = routes.flat_map { |r| r.operations.map(&:response_type) }.compact.uniq.map do |type|
|
470
|
+
base_type = (type || "unknown").split("[]").first.gsub("::", "")
|
471
|
+
next if base_type == "unknown"
|
472
|
+
relative_path = "./#{base_type}.tsp"
|
473
|
+
%(import "#{relative_path}";\n)
|
474
|
+
end.compact.uniq.join
|
475
|
+
|
476
|
+
resources = routes.map(&:as_typespec).join("\n").strip
|
477
|
+
<<~TSP
|
478
|
+
//
|
479
|
+
// DO NOT MODIFY: This file was automatically generated by TypeSpecFromSerializers.
|
480
|
+
import "@typespec/http";
|
481
|
+
|
482
|
+
#{imports}
|
483
|
+
using TypeSpec.Http;
|
484
|
+
|
485
|
+
namespace Routes {
|
486
|
+
#{resources}
|
487
|
+
}
|
488
|
+
TSP
|
489
|
+
end
|
490
|
+
|
491
|
+
def default_config(root)
|
492
|
+
Config.new(
|
493
|
+
# The base serializers that all other serializers extend.
|
494
|
+
base_serializers: ["BaseSerializer"],
|
495
|
+
|
496
|
+
# The dirs where the serializer files are located.
|
497
|
+
serializers_dirs: [root.join("app/serializers").to_s],
|
498
|
+
|
499
|
+
# The dir where model files are placed.
|
500
|
+
output_dir: root.join(defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/frontend").join("typespec/generated"),
|
501
|
+
|
502
|
+
# Remove the serializer suffix from the class name.
|
503
|
+
name_from_serializer: ->(name) {
|
504
|
+
name.split("::").map { |n| n.delete_suffix("Serializer") }.join("::")
|
505
|
+
},
|
506
|
+
|
507
|
+
# Types that don't need to be imported in TypeSpec.
|
508
|
+
global_types: [
|
509
|
+
"Array",
|
510
|
+
"Record",
|
511
|
+
"Date",
|
512
|
+
].to_set,
|
513
|
+
|
514
|
+
# Allows to choose a different sort order, alphabetical by default.
|
515
|
+
sort_properties_by: :name,
|
516
|
+
|
517
|
+
# Allows to avoid generating a serializer.
|
518
|
+
skip_serializer_if: ->(serializer) { false },
|
519
|
+
|
520
|
+
# Maps SQL column types to TypeSpec native and custom types.
|
521
|
+
sql_to_typespec_type_mapping: {
|
522
|
+
boolean: :boolean,
|
523
|
+
date: :plainDate,
|
524
|
+
datetime: :utcDateTime,
|
525
|
+
timestamp: :utcDateTime,
|
526
|
+
timestamptz: :offsetDateTime,
|
527
|
+
time: :plainTime,
|
528
|
+
decimal: :decimal128,
|
529
|
+
numeric: :decimal128,
|
530
|
+
integer: :int32,
|
531
|
+
bigint: :int64,
|
532
|
+
smallint: :int16,
|
533
|
+
tinyint: :int8,
|
534
|
+
float: :float32,
|
535
|
+
double: :float64,
|
536
|
+
real: :float32,
|
537
|
+
string: :string,
|
538
|
+
text: :string,
|
539
|
+
citext: :string,
|
540
|
+
binary: :bytes,
|
541
|
+
blob: :bytes,
|
542
|
+
json: "Record<string, unknown>",
|
543
|
+
jsonb: "Record<string, unknown>",
|
544
|
+
uuid: :string,
|
545
|
+
},
|
546
|
+
|
547
|
+
# Map Rails actions to TypeSpec operations
|
548
|
+
action_to_operation_mapping: {
|
549
|
+
"index" => "list",
|
550
|
+
"show" => "read",
|
551
|
+
"create" => "create",
|
552
|
+
"update" => "update",
|
553
|
+
"destroy" => "delete",
|
554
|
+
},
|
555
|
+
|
556
|
+
# Allows to transform keys, useful when converting objects client-side.
|
557
|
+
transform_keys: nil,
|
558
|
+
|
559
|
+
# Allows scoping typespec definitions to a namespace
|
560
|
+
namespace: nil,
|
561
|
+
)
|
562
|
+
end
|
563
|
+
|
564
|
+
# Internal: Writes if the file does not exist or the cache key has changed.
|
565
|
+
# The cache strategy consists of a comment on the first line of the file.
|
566
|
+
#
|
567
|
+
# Yields to receive the rendered file content when it needs to.
|
568
|
+
def write_if_changed(filename:, cache_key:, extension: "tsp")
|
569
|
+
filename = config.output_dir.join("#{filename}.#{extension}")
|
570
|
+
FileUtils.mkdir_p(filename.dirname)
|
571
|
+
cache_key_comment = "// TypeSpecFromSerializers CacheKey #{Digest::MD5.hexdigest(cache_key)}\n"
|
572
|
+
File.open(filename, "a+") { |file|
|
573
|
+
if stale?(file, cache_key_comment)
|
574
|
+
file.truncate(0)
|
575
|
+
file.write(cache_key_comment)
|
576
|
+
file.write(yield)
|
577
|
+
end
|
578
|
+
}
|
579
|
+
end
|
580
|
+
|
581
|
+
def serializers_index_content(serializers)
|
582
|
+
<<~TSP
|
583
|
+
//
|
584
|
+
// DO NOT MODIFY: This file was automatically generated by TypeSpecFromSerializers.
|
585
|
+
|
586
|
+
import "./routes.tsp";
|
587
|
+
#{serializers.reject(&:inline_serializer?).map { |s|
|
588
|
+
%(import "./models/#{s.tsp_filename}.tsp";)
|
589
|
+
}.join("\n")}
|
590
|
+
TSP
|
591
|
+
end
|
592
|
+
|
593
|
+
def serializer_model_content(model)
|
594
|
+
config.namespace ? declaration_model_definition(model) : standard_model_definition(model)
|
595
|
+
end
|
596
|
+
|
597
|
+
def standard_model_definition(model)
|
598
|
+
<<~TSP
|
599
|
+
//
|
600
|
+
// DO NOT MODIFY: This file was automatically generated by TypeSpecFromSerializers.
|
601
|
+
#{model.used_imports.join}
|
602
|
+
#{model.as_typespec}
|
603
|
+
TSP
|
604
|
+
end
|
605
|
+
|
606
|
+
def declaration_model_definition(model)
|
607
|
+
<<~TSP
|
608
|
+
//
|
609
|
+
// DO NOT MODIFY: This file was automatically generated by TypeSpecFromSerializers.
|
610
|
+
#{model.used_imports.empty? ? "export {}\n" : model.used_imports.join}
|
611
|
+
namespace #{config.namespace} {
|
612
|
+
#{model.as_typespec}
|
613
|
+
}
|
614
|
+
TSP
|
615
|
+
end
|
616
|
+
|
617
|
+
# Internal: Returns true if the cache key has changed since the last codegen.
|
618
|
+
def stale?(file, cache_key_comment)
|
619
|
+
@force_generation || file.gets != cache_key_comment
|
620
|
+
end
|
621
|
+
end
|
622
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/railtie"
|
4
|
+
|
5
|
+
class TypeSpecFromSerializers::Railtie < Rails::Railtie
|
6
|
+
railtie_name :typespec_from_serializers
|
7
|
+
|
8
|
+
# Automatically generates code whenever a serializer is loaded.
|
9
|
+
if defined?(Rails.env) && Rails.env.development?
|
10
|
+
require_relative "generator"
|
11
|
+
|
12
|
+
initializer "typespec_from_serializers.reloader" do |app|
|
13
|
+
if Gem.loaded_specs["listen"]
|
14
|
+
require "listen"
|
15
|
+
|
16
|
+
app.config.after_initialize do
|
17
|
+
app.reloaders << TypeSpecFromSerializers.track_changes
|
18
|
+
end
|
19
|
+
|
20
|
+
app.config.to_prepare do
|
21
|
+
TypeSpecFromSerializers.generate_changed
|
22
|
+
end
|
23
|
+
else
|
24
|
+
app.config.to_prepare do
|
25
|
+
TypeSpecFromSerializers.generate
|
26
|
+
end
|
27
|
+
|
28
|
+
Rails.logger.warn "Add 'listen' to your Gemfile to automatically generate code on serializer changes."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Suitable when triggering code generation manually.
|
34
|
+
rake_tasks do |app|
|
35
|
+
namespace :typespec_from_serializers do
|
36
|
+
desc "Generates TypeSpec descriptions for each serializer in the app."
|
37
|
+
task generate: :environment do
|
38
|
+
require_relative "generator"
|
39
|
+
start_time = Time.zone.now
|
40
|
+
print "Generating TypeSpec descriptions..."
|
41
|
+
serializers = TypeSpecFromSerializers.generate(force: true)
|
42
|
+
puts "completed in #{(Time.zone.now - start_time).round(2)} seconds.\n"
|
43
|
+
puts "Found #{serializers.size} serializers:"
|
44
|
+
puts serializers.map { |s| "\t#{s.name}" }.join("\n")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typespec_from_serializers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Danila Poyarkov
|
8
|
+
- Máximo Mussini
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2025-03-01 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: railties
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '5.1'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '5.1'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: oj_serializers
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 2.0.2
|
35
|
+
- - "~>"
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 2.0.2
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: listen
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.2'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: bundler
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2'
|
69
|
+
type: :development
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2'
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: rake
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '13'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '13'
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: rspec-given
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.8'
|
97
|
+
type: :development
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.8'
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
name: rspec-snapshot
|
106
|
+
requirement: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
type: :development
|
112
|
+
prerelease: false
|
113
|
+
version_requirements: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
- !ruby/object:Gem::Dependency
|
119
|
+
name: simplecov
|
120
|
+
requirement: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "<"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.18'
|
125
|
+
type: :development
|
126
|
+
prerelease: false
|
127
|
+
version_requirements: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "<"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.18'
|
132
|
+
- !ruby/object:Gem::Dependency
|
133
|
+
name: standard
|
134
|
+
requirement: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.0'
|
139
|
+
type: :development
|
140
|
+
prerelease: false
|
141
|
+
version_requirements: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '1.0'
|
146
|
+
- !ruby/object:Gem::Dependency
|
147
|
+
name: activerecord
|
148
|
+
requirement: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
type: :development
|
154
|
+
prerelease: false
|
155
|
+
version_requirements: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
- !ruby/object:Gem::Dependency
|
161
|
+
name: js_from_routes
|
162
|
+
requirement: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
type: :development
|
168
|
+
prerelease: false
|
169
|
+
version_requirements: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: sqlite3
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
type: :development
|
182
|
+
prerelease: false
|
183
|
+
version_requirements: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
- !ruby/object:Gem::Dependency
|
189
|
+
name: rubocop
|
190
|
+
requirement: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
type: :development
|
196
|
+
prerelease: false
|
197
|
+
version_requirements: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
- !ruby/object:Gem::Dependency
|
203
|
+
name: rubocop-rails
|
204
|
+
requirement: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
type: :development
|
210
|
+
prerelease: false
|
211
|
+
version_requirements: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
- !ruby/object:Gem::Dependency
|
217
|
+
name: rubocop-rspec
|
218
|
+
requirement: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
223
|
+
type: :development
|
224
|
+
prerelease: false
|
225
|
+
version_requirements: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
- !ruby/object:Gem::Dependency
|
231
|
+
name: rubocop-performance
|
232
|
+
requirement: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - ">="
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '0'
|
237
|
+
type: :development
|
238
|
+
prerelease: false
|
239
|
+
version_requirements: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - ">="
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0'
|
244
|
+
description: typespec_from_serializers helps you by automatically generating TypeSpec
|
245
|
+
descriptions for your JSON serializers, allowing you typecheck your frontend code
|
246
|
+
to ship fast and with confidence.
|
247
|
+
email:
|
248
|
+
- maximomussini@gmail.com
|
249
|
+
executables: []
|
250
|
+
extensions: []
|
251
|
+
extra_rdoc_files:
|
252
|
+
- README.md
|
253
|
+
files:
|
254
|
+
- CHANGELOG.md
|
255
|
+
- LICENSE.txt
|
256
|
+
- README.md
|
257
|
+
- lib/typespec_from_serializers.rb
|
258
|
+
- lib/typespec_from_serializers/dsl.rb
|
259
|
+
- lib/typespec_from_serializers/generator.rb
|
260
|
+
- lib/typespec_from_serializers/railtie.rb
|
261
|
+
- lib/typespec_from_serializers/version.rb
|
262
|
+
homepage: https://github.com/dannote/typespec_from_serializers
|
263
|
+
licenses:
|
264
|
+
- MIT
|
265
|
+
metadata: {}
|
266
|
+
post_install_message:
|
267
|
+
rdoc_options: []
|
268
|
+
require_paths:
|
269
|
+
- lib
|
270
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
271
|
+
requirements:
|
272
|
+
- - ">="
|
273
|
+
- !ruby/object:Gem::Version
|
274
|
+
version: '0'
|
275
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
276
|
+
requirements:
|
277
|
+
- - ">="
|
278
|
+
- !ruby/object:Gem::Version
|
279
|
+
version: '0'
|
280
|
+
requirements: []
|
281
|
+
rubygems_version: 3.5.16
|
282
|
+
signing_key:
|
283
|
+
specification_version: 4
|
284
|
+
summary: Generate TypeSpec descriptions from your JSON serializers.
|
285
|
+
test_files: []
|