jsonapi-resources-anchor 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/lib/anchor/anchor.rb +11 -0
- data/lib/anchor/concerns/annotatable.rb +40 -0
- data/lib/anchor/concerns/custom_linkable.rb +14 -0
- data/lib/anchor/concerns/custom_meta.rb +14 -0
- data/lib/anchor/concerns/schema_serializable.rb +10 -0
- data/lib/anchor/concerns/static_context.rb +13 -0
- data/lib/anchor/concerns/type_inferable.rb +39 -0
- data/lib/anchor/config.rb +21 -0
- data/lib/anchor/json_schema/resource.rb +14 -0
- data/lib/anchor/json_schema/schema_generator.rb +50 -0
- data/lib/anchor/json_schema/serializer.rb +40 -0
- data/lib/anchor/resource.rb +188 -0
- data/lib/anchor/schema.rb +21 -0
- data/lib/anchor/schema_generator.rb +18 -0
- data/lib/anchor/type_script/file_structure.rb +122 -0
- data/lib/anchor/type_script/resource.rb +33 -0
- data/lib/anchor/type_script/schema_generator.rb +82 -0
- data/lib/anchor/type_script/serializer.rb +60 -0
- data/lib/anchor/type_script/types.rb +24 -0
- data/lib/anchor/types/inference/active_record.rb +69 -0
- data/lib/anchor/types/inference/jsonapi.rb +14 -0
- data/lib/anchor/types.rb +85 -0
- data/lib/anchor/version.rb +3 -0
- data/lib/jsonapi-resources-anchor.rb +22 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 10487e8f527e3c65ae349ed70be50c6cfc7ef15791ec9e46197b4ffca184ac63
|
4
|
+
data.tar.gz: c66ceb8ee261a5230d580eda504010472a14dc6046054da32b5394b2fec223c4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0d952dc77d9371b3bc8fc252680056c08442ed316509ad2f159dfd38312b7dd7b97481626769ba30e8c35aa8a08de762ad7061d8d6ba265258c591934ed6e02e
|
7
|
+
data.tar.gz: 2a427fa18bb7aa97fce4f45f78a276e5784e70e2222c33002f9069950b826231dbb6072c26381abf1f55c3a591e92a871f8f95f35a2be4b9e3d4c5f11a496249
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Anchor
|
2
|
+
module Annotatable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class << self
|
7
|
+
attr_reader :anchor_attributes,
|
8
|
+
:anchor_relationships,
|
9
|
+
:anchor_attributes_descriptions,
|
10
|
+
:anchor_relationships_descriptions
|
11
|
+
|
12
|
+
# @param name [String, Symbol]
|
13
|
+
# @param annotation_or_options [Anchor::Types, Hash, NilClass]
|
14
|
+
# @param options [Hash]
|
15
|
+
def attribute(name, annotation_or_options = nil, options = {})
|
16
|
+
@anchor_attributes ||= {}
|
17
|
+
@anchor_attributes_descriptions ||= {}
|
18
|
+
opts = annotation_or_options.is_a?(Hash) ? annotation_or_options : options
|
19
|
+
annotation_given = !(annotation_or_options.is_a?(Hash) || annotation_or_options.nil?)
|
20
|
+
@anchor_attributes[name] = annotation_or_options if annotation_given
|
21
|
+
@anchor_attributes_descriptions[name] = opts[:description] if opts[:description]
|
22
|
+
super(name, opts)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param name [String, Symbol]
|
26
|
+
# @param annotation_or_options [Anchor::Types::Relationship, Hash, NilClass]
|
27
|
+
# @param options [Hash]
|
28
|
+
def relationship(name, annotation_or_options = nil, options = {})
|
29
|
+
@anchor_relationships ||= {}
|
30
|
+
@anchor_relationships_descriptions ||= {}
|
31
|
+
opts = annotation_or_options.is_a?(Hash) ? annotation_or_options : options
|
32
|
+
annotation_given = !(annotation_or_options.is_a?(Hash) || annotation_or_options.nil?)
|
33
|
+
@anchor_relationships[name] = annotation_or_options if annotation_given
|
34
|
+
@anchor_relationships_descriptions[name] = opts[:description] if opts[:description]
|
35
|
+
super(name, opts)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Anchor
|
2
|
+
module TypeInferable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class << self
|
7
|
+
attr_reader :anchor_method_added_count
|
8
|
+
|
9
|
+
# @param name [String] The type identifier to be used in the schema.
|
10
|
+
# @return [String]
|
11
|
+
def anchor_schema_name(name = nil)
|
12
|
+
@anchor_schema_name ||= name || anchor_default_schema_name
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# @anchor_method_added_count[attribute] > 1 implies the attribute
|
18
|
+
# is computed via an instance method defined on the JSONAPI::Resource.
|
19
|
+
# `JSONAPI::Resource.attribute(:name, options)` adds #name to the resource.
|
20
|
+
# A user defined #name on the resource _also_ adds #name.
|
21
|
+
def method_added(method_name)
|
22
|
+
@anchor_method_added_count ||= Hash.new(0)
|
23
|
+
@anchor_method_added_count[method_name] += 1
|
24
|
+
super(method_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
# inspiration from https://github.com/rmosolgo/graphql-ruby/blob/eda9b3d62b9e507787e590f0f179ec9d6956255a/lib/graphql/schema/member/base_dsl_methods.rb?plain=1#L102
|
28
|
+
def anchor_default_schema_name
|
29
|
+
# https://github.com/cerebris/jsonapi-resources/blob/d3c094b46a38650e583f40adc86474827b606fc7/lib/jsonapi/resource_common.rb?plain=1#L506
|
30
|
+
# https://github.com/cerebris/jsonapi-resources/blob/d3c094b46a38650e583f40adc86474827b606fc7/lib/jsonapi/resource_common.rb?plain=1#L564
|
31
|
+
return _type.to_s.classify if Anchor.config.use_type_as_schema_name
|
32
|
+
|
33
|
+
s_name = name.split("::").last
|
34
|
+
s_name.end_with?("Resource") ? s_name.sub(/Resource\Z/, "") : s_name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Anchor
|
2
|
+
class Config
|
3
|
+
attr_accessor :ar_column_to_type,
|
4
|
+
:field_case,
|
5
|
+
:use_active_record_validations,
|
6
|
+
:use_active_record_comment,
|
7
|
+
:infer_nullable_relationships_as_optional,
|
8
|
+
:empty_relationship_type,
|
9
|
+
:use_type_as_schema_name
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@ar_column_to_type = nil
|
13
|
+
@field_case = nil
|
14
|
+
@use_active_record_validations = true
|
15
|
+
@use_active_record_comment = nil
|
16
|
+
@infer_nullable_relationships_as_optional = nil
|
17
|
+
@empty_relationship_type = nil
|
18
|
+
@use_type_as_schema_name = nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Anchor::JSONSchema
|
2
|
+
class Resource < Anchor::Resource
|
3
|
+
def express(context: {}, include_all_fields:, exclude_fields:)
|
4
|
+
included_fields = schema_fetchable_fields(context:, include_all_fields:)
|
5
|
+
included_fields -= exclude_fields if exclude_fields
|
6
|
+
|
7
|
+
properties = [id_property, type_property] +
|
8
|
+
Array.wrap(anchor_attributes_properties(included_fields:)) +
|
9
|
+
Array.wrap(anchor_relationships_property(included_fields:))
|
10
|
+
|
11
|
+
Anchor::Types::Object.new(properties)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Anchor::JSONSchema
|
2
|
+
class SchemaGenerator < Anchor::SchemaGenerator
|
3
|
+
delegate :type_property, to: Anchor::JSONSchema::Serializer
|
4
|
+
|
5
|
+
def initialize(register:, context: {}, include_all_fields: false, exclude_fields: nil) # rubocop:disable Lint/MissingSuper
|
6
|
+
@register = register
|
7
|
+
@context = context
|
8
|
+
@include_all_fields = include_all_fields
|
9
|
+
@exclude_fields = exclude_fields
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
result = {
|
14
|
+
"$schema" => "https://json-schema.org/draft-07/schema",
|
15
|
+
title: "Schema",
|
16
|
+
}
|
17
|
+
result.merge!(type_property(root_object))
|
18
|
+
result["$defs"] = definitions
|
19
|
+
result.to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def resources
|
25
|
+
@resources ||= @register.resources.map { |r| Anchor::JSONSchema::Resource.new(r) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Anchor::Types::Object]
|
29
|
+
def root_object
|
30
|
+
properties = resources.map do |resource|
|
31
|
+
Types::Property.new(resource.anchor_schema_name.underscore, Types::Reference.new(resource.anchor_schema_name))
|
32
|
+
end
|
33
|
+
|
34
|
+
Types::Object.new(properties)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Hash{Symbol, String => Anchor::Types}]
|
38
|
+
def definitions
|
39
|
+
resources.map do |resource|
|
40
|
+
{
|
41
|
+
resource.anchor_schema_name => type_property(resource.express(
|
42
|
+
context: @context,
|
43
|
+
include_all_fields: @include_all_fields,
|
44
|
+
exclude_fields: @exclude_fields.nil? ? [] : @exclude_fields[r.anchor_schema_name.to_sym],
|
45
|
+
)),
|
46
|
+
}
|
47
|
+
end.reduce(&:merge)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Anchor::JSONSchema
|
2
|
+
class Serializer
|
3
|
+
class << self
|
4
|
+
def type_property(type)
|
5
|
+
case type
|
6
|
+
when Anchor::Types::String.singleton_class then { type: "string" }
|
7
|
+
when Anchor::Types::BigDecimal.singleton_class then { type: "string" }
|
8
|
+
when Anchor::Types::Float.singleton_class then { type: "number" }
|
9
|
+
when Anchor::Types::Integer.singleton_class then { type: "number" }
|
10
|
+
when Anchor::Types::Boolean.singleton_class then { type: "boolean" }
|
11
|
+
when Anchor::Types::Null.singleton_class then { type: "null" }
|
12
|
+
when Anchor::Types::Record, Anchor::Types::Record.singleton_class then {
|
13
|
+
type: "object",
|
14
|
+
additionalProperties: "true",
|
15
|
+
}
|
16
|
+
when Anchor::Types::Union then { oneOf: type.types.map { |type| type_property(type) } }
|
17
|
+
when Anchor::Types::Maybe then type_property(Anchor::Types::Union.new([type.type, Anchor::Types::Null]))
|
18
|
+
when Anchor::Types::Array then { type: "array", items: type_property(type.type) }
|
19
|
+
when Anchor::Types::Literal then { enum: [type.value] }
|
20
|
+
when Anchor::Types::Reference then { "$ref" => "#/$defs/#{type.name}" }
|
21
|
+
when Anchor::Types::Object, Anchor::Types::Object.singleton_class then serialize_object(type)
|
22
|
+
when Anchor::Types::Enum.singleton_class then { enum: type.values.map(&:second) }
|
23
|
+
when Anchor::Types::Unknown.singleton_class then {}
|
24
|
+
else raise RuntimeError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def serialize_object(type)
|
31
|
+
{
|
32
|
+
type: "object",
|
33
|
+
properties: type.properties.map { |p| { p.name => type_property(p.type) } }.reduce(&:merge),
|
34
|
+
required: type.properties.reject(&:optional).map(&:name),
|
35
|
+
additionalProperties: false,
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Anchor
|
2
|
+
class Resource
|
3
|
+
delegate_missing_to :@resource_klass
|
4
|
+
attr_reader :resource_klass
|
5
|
+
|
6
|
+
# resource_klass#anchor_attributes, #anchor_relationships, #anchor_attributes_descriptions,
|
7
|
+
# #anchor_relationships_descriptions are optional methods from Anchor::Annotatable.
|
8
|
+
# @param [JSONAPI::Resource] Must include Anchor::TypeInferable
|
9
|
+
def initialize(resource_klass)
|
10
|
+
@resource_klass = resource_klass
|
11
|
+
@anchor_attributes = resource_klass.try(:anchor_attributes) || {}
|
12
|
+
@anchor_relationships = resource_klass.try(:anchor_relationships) || {}
|
13
|
+
@anchor_attributes_descriptions = resource_klass.try(:anchor_attributes_descriptions) || {}
|
14
|
+
@anchor_relationships_descriptions = resource_klass.try(:anchor_relationships_descriptions) || {}
|
15
|
+
@anchor_method_added_count = resource_klass.anchor_method_added_count || Hash.new(0)
|
16
|
+
@anchor_links_schema = resource_klass.try(:anchor_links_schema) || nil
|
17
|
+
@anchor_meta_schema = resource_klass.try(:anchor_meta_schema) || nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def express(...)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
delegate :convert_case, to: Anchor::Types
|
27
|
+
|
28
|
+
def schema_fetchable_fields(context:, include_all_fields:)
|
29
|
+
return fields unless statically_determinable_fetchable_fields? && !include_all_fields
|
30
|
+
@resource_klass.anchor_fetchable_fields(context)
|
31
|
+
end
|
32
|
+
|
33
|
+
def statically_determinable_fetchable_fields?
|
34
|
+
@resource_klass.singleton_class.method_defined?(:anchor_fetchable_fields)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Anchor::Types::Property]
|
38
|
+
def id_property
|
39
|
+
# TODO: resource_key_type can also return a proc
|
40
|
+
res_key_type = case resource_key_type
|
41
|
+
when :integer then Anchor::Types::Integer
|
42
|
+
else Anchor::Types::String
|
43
|
+
end
|
44
|
+
|
45
|
+
Anchor::Types::Property.new(:id, res_key_type)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Anchor::Types::Property]
|
49
|
+
def type_property
|
50
|
+
Anchor::Types::Property.new(:type, Anchor::Types::Literal.new(_type))
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param included_fields [Array<Symbol>]
|
54
|
+
# @return [Array<Anchor::Types::Property>]
|
55
|
+
def anchor_attributes_properties(included_fields:)
|
56
|
+
_attributes.except(:id).filter_map do |attr, options|
|
57
|
+
next if included_fields.exclude?(attr.to_sym)
|
58
|
+
description = @anchor_attributes_descriptions[attr]
|
59
|
+
next Anchor::Types::Property.new(
|
60
|
+
convert_case(attr),
|
61
|
+
@anchor_attributes[attr],
|
62
|
+
false,
|
63
|
+
description,
|
64
|
+
) if @anchor_attributes.key?(attr)
|
65
|
+
|
66
|
+
type = begin
|
67
|
+
model_method = options[:delegate] || attr
|
68
|
+
resource_method = attr
|
69
|
+
|
70
|
+
model_method_defined = _model_class.try(
|
71
|
+
:method_defined?,
|
72
|
+
model_method.to_sym,
|
73
|
+
) && !_model_class.instance_method(model_method.to_sym)
|
74
|
+
.owner.is_a?(ActiveRecord::AttributeMethods::GeneratedAttributeMethods)
|
75
|
+
resource_method_defined = @anchor_method_added_count[resource_method.to_sym] > 1
|
76
|
+
serializer_defined = (_model_class.try(:attribute_types) || {})[model_method.to_s].respond_to?(:coder)
|
77
|
+
method_defined = model_method_defined || resource_method_defined || serializer_defined
|
78
|
+
|
79
|
+
column = !method_defined && _model_class.try(:columns_hash).try(:[], model_method.to_s)
|
80
|
+
if column
|
81
|
+
type = Anchor::Types::Inference::ActiveRecord::SQL.from(column)
|
82
|
+
description ||= column.comment if Anchor.config.use_active_record_comment
|
83
|
+
check_presence = type.is_a?(Anchor::Types::Maybe) && Anchor.config.use_active_record_validations
|
84
|
+
if check_presence && _model_class.validators_on(model_method).any? do |v|
|
85
|
+
if v.is_a?(ActiveRecord::Validations::NumericalityValidator)
|
86
|
+
opts = v.options.with_indifferent_access
|
87
|
+
!(opts[:allow_nil] || opts[:if] || opts[:unless] || opts[:on])
|
88
|
+
elsif v.is_a?(ActiveRecord::Validations::PresenceValidator)
|
89
|
+
opts = v.options.with_indifferent_access
|
90
|
+
!(opts[:if] || opts[:unless] || opts[:on])
|
91
|
+
end
|
92
|
+
end
|
93
|
+
type.type
|
94
|
+
else
|
95
|
+
type
|
96
|
+
end
|
97
|
+
else
|
98
|
+
Anchor::Types::Unknown
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
Anchor::Types::Property.new(convert_case(attr), type, false, description)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param included_fields [Array<Symbol>]
|
107
|
+
# @return [Anchor::Types::Property, NilClass]
|
108
|
+
def anchor_relationships_property(included_fields:)
|
109
|
+
anchor_relationships_properties(included_fields:).then do |properties|
|
110
|
+
break if properties.blank?
|
111
|
+
Anchor::Types::Property.new(:relationships, Anchor::Types::Object.new(properties))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def anchor_links_property
|
116
|
+
if @anchor_links_schema
|
117
|
+
Anchor::Types::Property.new("links", @anchor_links_schema, false)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def anchor_meta_property
|
122
|
+
if @anchor_meta_schema
|
123
|
+
Anchor::Types::Property.new("meta", @anchor_meta_schema, false)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param included_fields [Array<Symbol>]
|
128
|
+
# @return [Array<Anchor::Types::Property>]
|
129
|
+
def anchor_relationships_properties(included_fields:)
|
130
|
+
_relationships.filter_map do |name, rel|
|
131
|
+
next if included_fields.exclude?(name.to_sym)
|
132
|
+
description = @anchor_relationships_descriptions[name]
|
133
|
+
relationship_type = relationship_type_for(rel, rel.resource_klass, name) if @anchor_relationships.exclude?(name)
|
134
|
+
|
135
|
+
relationship_type ||= begin
|
136
|
+
anchor_relationship = @anchor_relationships[name]
|
137
|
+
|
138
|
+
type = if (resources = anchor_relationship.resources)
|
139
|
+
references = resources.map do |resource_klass|
|
140
|
+
Anchor::Types::Reference.new(resource_klass.anchor_schema_name)
|
141
|
+
end
|
142
|
+
null_type = Array.wrap(anchor_relationship.null_elements.presence && Anchor::Types::Null)
|
143
|
+
Anchor::Types::Union.new(references + null_type)
|
144
|
+
else
|
145
|
+
Anchor::Types::Reference.new(anchor_relationship.resource.anchor_schema_name)
|
146
|
+
end
|
147
|
+
|
148
|
+
type = Anchor::Types::Inference::JSONAPI.wrapper_from_relationship(rel).call(type)
|
149
|
+
anchor_relationship.null.present? ? Anchor::Types::Maybe.new(type) : type
|
150
|
+
end
|
151
|
+
|
152
|
+
use_optional = Anchor.config.infer_nullable_relationships_as_optional
|
153
|
+
if use_optional && relationship_type.is_a?(Anchor::Types::Maybe)
|
154
|
+
Anchor::Types::Property.new(convert_case(name), relationship_type.type, true, description)
|
155
|
+
else
|
156
|
+
Anchor::Types::Property.new(convert_case(name), relationship_type, false, description)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# rubocop:disable Layout/LineLength
|
162
|
+
# @param rel [Relationship]
|
163
|
+
# @param resource_klass [Anchor::Resource]
|
164
|
+
# @param name [String, Symbol]
|
165
|
+
# @return [Anchor::Types::Reference, Anchor::Types::Array<Anchor::Types::Reference>, Anchor::Types::Maybe<Anchor::Types::Reference>, Anchor::Types::Union<Anchor::Types::Reference>]
|
166
|
+
def relationship_type_for(rel, resource_klass, name)
|
167
|
+
rel_type = if rel.polymorphic? && rel.respond_to?(:polymorphic_types) # 0.11.0.beta2
|
168
|
+
resource_klasses = rel.polymorphic_types.map { |t| resource_klass_for(t) }
|
169
|
+
Anchor::Types::Union.new(resource_klasses.map { |rk| Anchor::Types::Reference.new(rk.anchor_schema_name) })
|
170
|
+
elsif rel.polymorphic? && rel.class.respond_to?(:polymorphic_types) # TODO: < 0.11.0.beta2
|
171
|
+
resource_klasses = rel.class.polymorphic_types.map { |t| resource_klass_for(t) }
|
172
|
+
Anchor::Types::Union.new(resource_klasses.map { |rk| Anchor::Types::Reference.new(rk.anchor_schema_name) })
|
173
|
+
end
|
174
|
+
|
175
|
+
rel_type ||= Anchor::Types::Reference.new(resource_klass.anchor_schema_name)
|
176
|
+
model_relationship_name = (rel.options[:relation_name] || name).to_s
|
177
|
+
reflection = _model_class.try(:reflections).try(:[], model_relationship_name)
|
178
|
+
wrapper = if reflection
|
179
|
+
Anchor::Types::Inference::ActiveRecord.wrapper_from_reflection(reflection)
|
180
|
+
else
|
181
|
+
Anchor::Types::Inference::JSONAPI.wrapper_from_relationship(rel)
|
182
|
+
end
|
183
|
+
|
184
|
+
wrapper.call(rel_type)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
# rubocop:enable Layout/LineLength
|
188
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Anchor
|
2
|
+
class Schema
|
3
|
+
class << self
|
4
|
+
Register = Struct.new(:resources, :enums, keyword_init: true)
|
5
|
+
|
6
|
+
def register
|
7
|
+
Register.new(resources: @resources || [], enums: @enums || [])
|
8
|
+
end
|
9
|
+
|
10
|
+
def resource(resource)
|
11
|
+
@resources ||= []
|
12
|
+
@resources.push(resource)
|
13
|
+
end
|
14
|
+
|
15
|
+
def enum(enum)
|
16
|
+
@enums ||= []
|
17
|
+
@enums.push(enum)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Anchor
|
2
|
+
class SchemaGenerator
|
3
|
+
def initialize(register:, context:, include_all_fields:, exclude_fields:)
|
4
|
+
@register = register
|
5
|
+
@context = context
|
6
|
+
@include_all_fields = include_all_fields
|
7
|
+
@exclude_fields = exclude_fields
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.call(...)
|
11
|
+
new(...).call
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Anchor::TypeScript
|
2
|
+
class FileStructure
|
3
|
+
# @param file_name [String] name of file, e.g. model.ts
|
4
|
+
# @param type [Anchor::Types]
|
5
|
+
Import = Struct.new(:file_name, :type, keyword_init: true)
|
6
|
+
class FileUtils
|
7
|
+
def self.imports_to_code(imports)
|
8
|
+
imports.group_by(&:file_name).map do |file_name, file_imports|
|
9
|
+
named_imports = file_imports.map do |import|
|
10
|
+
import.type.try(:anchor_schema_name) || import.type.name
|
11
|
+
end.join(",")
|
12
|
+
|
13
|
+
"import type { #{named_imports} } from \"./#{file_name[..-4]}\";"
|
14
|
+
end.join("\n") + "\n"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.def_to_code(identifier, object)
|
18
|
+
expression = Anchor::TypeScript::Serializer.type_string(object)
|
19
|
+
"type #{identifier} = #{expression};" + "\n"
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.export_code(identifier)
|
23
|
+
"export { type #{identifier} };" + "\n"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(definition)
|
28
|
+
@definition = definition
|
29
|
+
@name = definition.name
|
30
|
+
@object = definition.object
|
31
|
+
end
|
32
|
+
|
33
|
+
def name
|
34
|
+
"#{@definition.name}.ts"
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_code(manually_editable: false)
|
38
|
+
imports_string = FileUtils.imports_to_code(imports)
|
39
|
+
name = manually_editable ? "Model" : @name
|
40
|
+
typedef = FileUtils.def_to_code(name, @object)
|
41
|
+
export_string = FileUtils.export_code(@definition.name)
|
42
|
+
|
43
|
+
if manually_editable
|
44
|
+
start_autogen = "// START AUTOGEN\n"
|
45
|
+
end_autogen = "// END AUTOGEN\n"
|
46
|
+
unedited_export_def = "type #{@name} = Model;\n"
|
47
|
+
[start_autogen, imports_string, typedef, end_autogen, unedited_export_def, export_string].join("\n")
|
48
|
+
else
|
49
|
+
[imports_string, typedef, export_string].join("\n")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Array<Import>]
|
54
|
+
def imports
|
55
|
+
shared_imports + relationship_imports
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# @return [Array<Import>]
|
61
|
+
def shared_imports
|
62
|
+
(utils_to_import + enums_to_import).map { |type| Import.new(file_name: "shared.ts", type:) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Array<Import>]
|
66
|
+
def relationship_imports
|
67
|
+
relationships_to_import
|
68
|
+
.reject { |type| type.anchor_schema_name == @name }
|
69
|
+
.map { |type| Import.new(file_name: "#{type.anchor_schema_name}.ts", type:) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def relationships_to_import
|
73
|
+
relationships = @object.properties.find { |p| p.name == :relationships }
|
74
|
+
return [] if relationships.nil? || relationships.type.try(:properties).nil?
|
75
|
+
relationships.type.properties.flat_map { |p| references_from_type(p.type) }.uniq.sort_by(&:anchor_schema_name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def references_from_type(type)
|
79
|
+
case type
|
80
|
+
when Anchor::Types::Array, Anchor::Types::Maybe then references_from_type(type.type)
|
81
|
+
when Anchor::Types::Union then type.types.flat_map { |t| references_from_type(t) }
|
82
|
+
when Anchor::Types::Reference then [type]
|
83
|
+
end.uniq.sort_by(&:anchor_schema_name)
|
84
|
+
end
|
85
|
+
|
86
|
+
def utils_to_import
|
87
|
+
maybe_type = has_maybe?(@object).presence && Anchor::Types::Reference.new("Maybe")
|
88
|
+
[maybe_type].compact
|
89
|
+
end
|
90
|
+
|
91
|
+
def has_maybe?(type)
|
92
|
+
case type
|
93
|
+
when Anchor::Types::Maybe then true
|
94
|
+
when Anchor::Types::Array then has_maybe?(type.type)
|
95
|
+
when Anchor::Types::Union then type.types.any? { |t| has_maybe?(t) }
|
96
|
+
when Anchor::Types::Object, Anchor::Types::Object.singleton_class then type.properties.any? do |p|
|
97
|
+
has_maybe?(p)
|
98
|
+
end
|
99
|
+
when Anchor::Types::Property then has_maybe?(type.type)
|
100
|
+
else false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def enums_to_import
|
105
|
+
enums_to_import_from_type(@object).uniq.sort_by(&:anchor_schema_name)
|
106
|
+
end
|
107
|
+
|
108
|
+
def enums_to_import_from_type(type)
|
109
|
+
case type
|
110
|
+
when Anchor::Types::Enum.singleton_class then [type]
|
111
|
+
when Anchor::Types::Array then enums_to_import_from_type(type.type)
|
112
|
+
when Anchor::Types::Maybe then enums_to_import_from_type(type.type)
|
113
|
+
when Anchor::Types::Union then type.types.flat_map { |t| enums_to_import_from_type(t) }
|
114
|
+
when Anchor::Types::Object, Anchor::Types::Object.singleton_class then type.properties.flat_map do |p|
|
115
|
+
enums_to_import_from_type(p)
|
116
|
+
end
|
117
|
+
when Anchor::Types::Property then enums_to_import_from_type(type.type)
|
118
|
+
else []
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Anchor::TypeScript
|
2
|
+
class Resource < Anchor::Resource
|
3
|
+
Definition = Struct.new(:name, :object, keyword_init: true)
|
4
|
+
|
5
|
+
def express(...)
|
6
|
+
@object = object(...)
|
7
|
+
expression = Anchor::TypeScript::Serializer.type_string(@object)
|
8
|
+
"export type #{anchor_schema_name} = " + expression + ";"
|
9
|
+
end
|
10
|
+
|
11
|
+
def definition(...)
|
12
|
+
@object = object(...)
|
13
|
+
Definition.new(name: anchor_schema_name, object: @object)
|
14
|
+
end
|
15
|
+
|
16
|
+
def object(context: {}, include_all_fields:, exclude_fields:)
|
17
|
+
included_fields = schema_fetchable_fields(context:, include_all_fields:)
|
18
|
+
included_fields -= exclude_fields if exclude_fields
|
19
|
+
|
20
|
+
relationships_property = anchor_relationships_property(included_fields:)
|
21
|
+
if relationships_property.nil? && Anchor.config.empty_relationship_type
|
22
|
+
relationships_property = Anchor::Types::Property.new(:relationships, Anchor.config.empty_relationship_type.call)
|
23
|
+
end
|
24
|
+
|
25
|
+
properties = [id_property, type_property] +
|
26
|
+
Array.wrap(anchor_attributes_properties(included_fields:)) +
|
27
|
+
Array.wrap(relationships_property) +
|
28
|
+
[anchor_meta_property].compact + [anchor_links_property].compact
|
29
|
+
|
30
|
+
Anchor::Types::Object.new(properties)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Anchor::TypeScript
|
2
|
+
class SchemaGenerator < Anchor::SchemaGenerator
|
3
|
+
def initialize(register:, context: {}, include_all_fields: false, exclude_fields: nil) # rubocop:disable Lint/MissingSuper
|
4
|
+
@register = register
|
5
|
+
@context = context
|
6
|
+
@include_all_fields = include_all_fields
|
7
|
+
@exclude_fields = exclude_fields
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
maybe_type = "type Maybe<T> = T | null;"
|
12
|
+
|
13
|
+
enum_expressions = enums.map(&:express)
|
14
|
+
type_expressions = resources.map do |r|
|
15
|
+
r.express(
|
16
|
+
context: @context,
|
17
|
+
include_all_fields: @include_all_fields,
|
18
|
+
exclude_fields: @exclude_fields.nil? ? [] : @exclude_fields[r.anchor_schema_name.to_sym],
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
([maybe_type] + enum_expressions + type_expressions).join("\n\n") + "\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def resources
|
28
|
+
@resources ||= @register.resources.map { |r| Anchor::TypeScript::Resource.new(r) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def enums
|
32
|
+
@enums ||= @register.enums.map { |e| Anchor::TypeScript::Types::Enum.new(e) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class MultifileSchemaGenerator < Anchor::SchemaGenerator
|
37
|
+
def initialize(register:, context: {}, include_all_fields: false, exclude_fields: nil, manually_editable: true) # rubocop:disable Lint/MissingSuper
|
38
|
+
@register = register
|
39
|
+
@context = context
|
40
|
+
@include_all_fields = include_all_fields
|
41
|
+
@exclude_fields = exclude_fields
|
42
|
+
@manually_editable = manually_editable
|
43
|
+
end
|
44
|
+
|
45
|
+
def call
|
46
|
+
[shared_file] + resource_files
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def shared_file
|
52
|
+
maybe_type = "export type Maybe<T> = T | null;"
|
53
|
+
|
54
|
+
enum_expressions = enums.map(&:express)
|
55
|
+
content = ([maybe_type] + enum_expressions).join("\n\n") + "\n"
|
56
|
+
{ name: "shared.ts", content: }
|
57
|
+
end
|
58
|
+
|
59
|
+
def resource_files
|
60
|
+
resources.map do |r|
|
61
|
+
definition = r.definition(
|
62
|
+
context: @context,
|
63
|
+
include_all_fields: @include_all_fields,
|
64
|
+
exclude_fields: @exclude_fields.nil? ? [] : @exclude_fields[r.anchor_schema_name.to_sym],
|
65
|
+
)
|
66
|
+
|
67
|
+
file_structure = ::Anchor::TypeScript::FileStructure.new(definition)
|
68
|
+
content = file_structure.to_code(manually_editable: @manually_editable)
|
69
|
+
name = file_structure.name
|
70
|
+
{ name:, content: }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def resources
|
75
|
+
@resources ||= @register.resources.map { |r| Anchor::TypeScript::Resource.new(r) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def enums
|
79
|
+
@enums ||= @register.enums.map { |e| Anchor::TypeScript::Types::Enum.new(e) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Anchor::TypeScript
|
2
|
+
class Serializer
|
3
|
+
class << self
|
4
|
+
# rubocop:disable Layout/LineLength
|
5
|
+
|
6
|
+
def type_string(type, depth = 1)
|
7
|
+
case type
|
8
|
+
when Anchor::Types::String.singleton_class then "string"
|
9
|
+
when Anchor::Types::BigDecimal.singleton_class then "string"
|
10
|
+
when Anchor::Types::Float.singleton_class then "number"
|
11
|
+
when Anchor::Types::Integer.singleton_class then "number"
|
12
|
+
when Anchor::Types::Boolean.singleton_class then "boolean"
|
13
|
+
when Anchor::Types::Null.singleton_class then "null"
|
14
|
+
when Anchor::Types::Record, Anchor::Types::Record.singleton_class then "Record<string, #{type_string(type.try(:value_type) || Anchor::Types::Unknown)}>"
|
15
|
+
when Anchor::Types::Union then type.types.map { |type| type_string(type, depth) }.join(" | ")
|
16
|
+
when Anchor::Types::Maybe then "Maybe<#{type_string(type.type, depth)}>"
|
17
|
+
when Anchor::Types::Array then "Array<#{type_string(type.type, depth)}>"
|
18
|
+
when Anchor::Types::Literal then serialize_literal(type.value)
|
19
|
+
when Anchor::Types::Reference then type.name
|
20
|
+
when Anchor::Types::Object, Anchor::Types::Object.singleton_class then serialize_object(type, depth)
|
21
|
+
when Anchor::Types::Enum.singleton_class then type.anchor_schema_name
|
22
|
+
when Anchor::Types::Unknown.singleton_class then "unknown"
|
23
|
+
else raise RuntimeError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
# rubocop:enable Layout/LineLength
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def serialize_literal(value)
|
31
|
+
case value
|
32
|
+
when ::String, ::Symbol then "\"#{value}\""
|
33
|
+
else value.to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def serialize_object(type, depth)
|
38
|
+
return "{}" if type.properties.empty?
|
39
|
+
properties = type.properties.flat_map do |p|
|
40
|
+
[
|
41
|
+
p.description && "/** #{p.description} */",
|
42
|
+
"#{safe_name(p)}: #{type_string(p.type, depth + 1)};",
|
43
|
+
].compact
|
44
|
+
end
|
45
|
+
indent = " " * (depth * 2)
|
46
|
+
properties = properties.map { |p| p.prepend(indent) }.join("\n")
|
47
|
+
["{", properties, "}".prepend(indent[2..])].join("\n")
|
48
|
+
end
|
49
|
+
|
50
|
+
def safe_name(property)
|
51
|
+
name = property.name
|
52
|
+
if name.match?(/[^a-zA-Z0-9_]/)
|
53
|
+
"\"#{name}\""
|
54
|
+
else
|
55
|
+
name.to_s + (property.optional ? "?" : "")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Anchor::TypeScript
|
2
|
+
module Types
|
3
|
+
class Enum
|
4
|
+
delegate_missing_to :@enum_klass
|
5
|
+
|
6
|
+
def initialize(enum_klass)
|
7
|
+
@enum_klass = enum_klass
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
def express
|
12
|
+
["export enum #{anchor_schema_name} {", named_constants, "}"].join("\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def named_constants
|
18
|
+
values.map do |name, value|
|
19
|
+
" #{name.to_s.camelize} = #{Anchor::TypeScript::Serializer.type_string(value)},"
|
20
|
+
end.join("\n")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Anchor::Types::Inference
|
2
|
+
module ActiveRecord
|
3
|
+
class << self
|
4
|
+
# rubocop:disable Layout/LineLength
|
5
|
+
|
6
|
+
# @return [Proc{Type => Type, Anchor::Types::Maybe<Type>, Anchor::Types::Array<Type>}]
|
7
|
+
def wrapper_from_reflection(reflection)
|
8
|
+
case reflection
|
9
|
+
when ::ActiveRecord::Reflection::BelongsToReflection then ->(type) { belongs_to_type(reflection, type) }
|
10
|
+
when ::ActiveRecord::Reflection::HasOneReflection then ->(type) { Anchor::Types::Maybe.new(type) }
|
11
|
+
when ::ActiveRecord::Reflection::HasManyReflection then ->(type) { Anchor::Types::Array.new(type) }
|
12
|
+
when ::ActiveRecord::Reflection::HasAndBelongsToManyReflection then ->(type) { Anchor::Types::Array.new(type) }
|
13
|
+
when ::ActiveRecord::Reflection::ThroughReflection then wrapper_from_reflection(reflection.send(:delegate_reflection))
|
14
|
+
else raise "#{reflection.class.name} not supported"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
# rubocop:enable Layout/LineLength
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @param reflection [::ActiveRecord::Reflection::BelongsToReflection]
|
22
|
+
# @param type [Anchor::Types]
|
23
|
+
# @return [Anchor::Types::Maybe<Type>, Type]
|
24
|
+
def belongs_to_type(reflection, type)
|
25
|
+
reflection.options[:optional] ? Anchor::Types::Maybe.new(type) : type
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module SQL
|
30
|
+
class << self
|
31
|
+
def from(column, check_config: true)
|
32
|
+
return Anchor.config.ar_column_to_type.call(column) if check_config && Anchor.config.ar_column_to_type
|
33
|
+
type = from_sql_type(column.type)
|
34
|
+
|
35
|
+
if ["character varying[]", "text[]"].include?(column.sql_type_metadata.sql_type)
|
36
|
+
type = Anchor::Types::Array.new(Anchor::Types::String)
|
37
|
+
end
|
38
|
+
|
39
|
+
column.null ? Anchor::Types::Maybe.new(type) : type
|
40
|
+
end
|
41
|
+
|
42
|
+
def default_ar_column_to_type(column)
|
43
|
+
from(column, check_config: false)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# inspiration from https://github.com/ElMassimo/types_from_serializers/blob/146ba40bc1a0da37473cd3b705a8ca982c2d173f/types_from_serializers/lib/types_from_serializers/generator.rb#L382
|
49
|
+
def from_sql_type(type)
|
50
|
+
case type
|
51
|
+
when :boolean then Anchor::Types::Boolean
|
52
|
+
when :date then Anchor::Types::String
|
53
|
+
when :datetime then Anchor::Types::String
|
54
|
+
when :decimal then Anchor::Types::BigDecimal
|
55
|
+
when :float then Anchor::Types::Float
|
56
|
+
when :integer then Anchor::Types::Integer
|
57
|
+
when :json then Anchor::Types::Record
|
58
|
+
when :jsonb then Anchor::Types::Record
|
59
|
+
when :string then Anchor::Types::String
|
60
|
+
when :text then Anchor::Types::String
|
61
|
+
when :time then Anchor::Types::String
|
62
|
+
when :uuid then Anchor::Types::String
|
63
|
+
else Anchor::Types::Unknown
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Anchor::Types::Inference
|
2
|
+
module JSONAPI
|
3
|
+
class << self
|
4
|
+
# @return [Proc{Type => Type, Anchor::Types::Array<Type>}]
|
5
|
+
def wrapper_from_relationship(relationship)
|
6
|
+
case relationship
|
7
|
+
when ::JSONAPI::Relationship::ToOne then ->(type) { type }
|
8
|
+
when ::JSONAPI::Relationship::ToMany then ->(type) { Anchor::Types::Array.new(type) }
|
9
|
+
else raise "#{relationship.class.name} not supported"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/anchor/types.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module Anchor
|
2
|
+
module Types
|
3
|
+
class String; end
|
4
|
+
class Float; end
|
5
|
+
class Integer; end
|
6
|
+
class BigDecimal; end
|
7
|
+
class Boolean; end
|
8
|
+
class Null; end
|
9
|
+
class Unknown; end
|
10
|
+
Record = Struct.new(:value_type)
|
11
|
+
Maybe = Struct.new(:type)
|
12
|
+
Array = Struct.new(:type)
|
13
|
+
Literal = Struct.new(:value)
|
14
|
+
Union = Struct.new(:types)
|
15
|
+
Reference = Struct.new(:name) do
|
16
|
+
def anchor_schema_name
|
17
|
+
name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
Property = Struct.new(:name, :type, :optional, :description)
|
21
|
+
class Object
|
22
|
+
attr_reader :properties
|
23
|
+
|
24
|
+
def initialize(properties)
|
25
|
+
@properties = properties || []
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def properties
|
30
|
+
@properties ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
def property(name, type, optional: nil, description: nil)
|
34
|
+
@properties ||= []
|
35
|
+
@properties.push(Property.new(name, type, optional, description))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Relationship = Struct.new(:resource, :resources, :null, :null_elements, keyword_init: true)
|
41
|
+
|
42
|
+
class Enum
|
43
|
+
class << self
|
44
|
+
attr_reader :values
|
45
|
+
|
46
|
+
def anchor_schema_name(name = nil)
|
47
|
+
@anchor_schema_name ||= name || default_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def value(name, value)
|
51
|
+
@values ||= []
|
52
|
+
@values.push([name, Types::Literal.new(value)])
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def default_name
|
58
|
+
s_name = name.split("::").last
|
59
|
+
s_name.end_with?("Enum") ? s_name.sub(/Enum\Z/, "") : s_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.camelize_without_inflection(val)
|
65
|
+
vals = val.split("_")
|
66
|
+
if vals.length == 1
|
67
|
+
vals[0]
|
68
|
+
else
|
69
|
+
([vals[0]] + vals[1..].map(&:capitalize)).join("")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param value [String, Symbol]
|
74
|
+
# @return [String]
|
75
|
+
def self.convert_case(value)
|
76
|
+
case Anchor.config.field_case
|
77
|
+
when :camel then value.to_s.underscore.camelize(:lower)
|
78
|
+
when :camel_without_inflection then camelize_without_inflection(value.to_s.underscore)
|
79
|
+
when :kebab then value.to_s.underscore.dasherize
|
80
|
+
when :snake then value.to_s.underscore
|
81
|
+
else value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "anchor/types"
|
2
|
+
require "anchor/anchor"
|
3
|
+
require "anchor/config"
|
4
|
+
require "anchor/resource"
|
5
|
+
require "anchor/schema_generator"
|
6
|
+
require "anchor/concerns/annotatable"
|
7
|
+
require "anchor/concerns/custom_linkable"
|
8
|
+
require "anchor/concerns/custom_meta"
|
9
|
+
require "anchor/concerns/static_context"
|
10
|
+
require "anchor/concerns/type_inferable"
|
11
|
+
require "anchor/concerns/schema_serializable"
|
12
|
+
require "anchor/schema"
|
13
|
+
require "anchor/types/inference/jsonapi"
|
14
|
+
require "anchor/types/inference/active_record"
|
15
|
+
require "anchor/type_script/file_structure"
|
16
|
+
require "anchor/type_script/types"
|
17
|
+
require "anchor/type_script/schema_generator"
|
18
|
+
require "anchor/type_script/serializer"
|
19
|
+
require "anchor/type_script/resource"
|
20
|
+
require "anchor/json_schema/serializer"
|
21
|
+
require "anchor/json_schema/schema_generator"
|
22
|
+
require "anchor/json_schema/resource"
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jsonapi-resources-anchor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: jsonapi-resources
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '7.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '7.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: yard-activesupport-concern
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files: []
|
88
|
+
files:
|
89
|
+
- lib/anchor/anchor.rb
|
90
|
+
- lib/anchor/concerns/annotatable.rb
|
91
|
+
- lib/anchor/concerns/custom_linkable.rb
|
92
|
+
- lib/anchor/concerns/custom_meta.rb
|
93
|
+
- lib/anchor/concerns/schema_serializable.rb
|
94
|
+
- lib/anchor/concerns/static_context.rb
|
95
|
+
- lib/anchor/concerns/type_inferable.rb
|
96
|
+
- lib/anchor/config.rb
|
97
|
+
- lib/anchor/json_schema/resource.rb
|
98
|
+
- lib/anchor/json_schema/schema_generator.rb
|
99
|
+
- lib/anchor/json_schema/serializer.rb
|
100
|
+
- lib/anchor/resource.rb
|
101
|
+
- lib/anchor/schema.rb
|
102
|
+
- lib/anchor/schema_generator.rb
|
103
|
+
- lib/anchor/type_script/file_structure.rb
|
104
|
+
- lib/anchor/type_script/resource.rb
|
105
|
+
- lib/anchor/type_script/schema_generator.rb
|
106
|
+
- lib/anchor/type_script/serializer.rb
|
107
|
+
- lib/anchor/type_script/types.rb
|
108
|
+
- lib/anchor/types.rb
|
109
|
+
- lib/anchor/types/inference/active_record.rb
|
110
|
+
- lib/anchor/types/inference/jsonapi.rb
|
111
|
+
- lib/anchor/version.rb
|
112
|
+
- lib/jsonapi-resources-anchor.rb
|
113
|
+
homepage:
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '3.1'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubygems_version: 3.5.11
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: jsonapi-resources type annotation, inference, and schema/docs generation
|
136
|
+
test_files: []
|