blueprinter_schema 0.6.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b10fc12e29b6f54a7f5515b3d97809bd65e1fded8acd99a2d9395339cf7b1b2c
4
- data.tar.gz: 4f0403c88c922e8c026d4ac0b7dd686c1e88f7f0f3fcde3174f184bfb9e375b9
3
+ metadata.gz: 9246edd433fe3a71df7924c38bfd92928af95ddfdc90aebab7285ed6270e3fdc
4
+ data.tar.gz: 228234c8612cdf7e8b728e67974831263bc077864ea6ec0f0608d6c23c8cbe8b
5
5
  SHA512:
6
- metadata.gz: ac1dd30628176e9f5feb4f461bd4a4931050967945be9f81e1e6ceaa4387f52c1139d652a518acf4b2bce5e5309d9c95ccd8d5708215c672975b730e9be20e57
7
- data.tar.gz: f77ef5d6d8c7d3c8a204f5b053b7a56183c3298f6b8b6a22606257e16febb0087ccb304ad60ed7acd26f3123693b40756503b178096870fd80d705d604f737e6
6
+ metadata.gz: e1d5833548d59cb364765ea3db2bee8c50c13fadb12bd18e742014e48c8636de6a1ef4519ab79ac8c194b3301fa5dc9b0f9cdd61da70383d052e50687619a496
7
+ data.tar.gz: 3e731df8e7f2ac8736fda0fdcd40797b6ec024297cb186c255e71087fdb9a6a9db365517c5dd9fb25a24ea5f128c2c49b5e1aebef1fd83f8ecd500f22fc82d23
data/.rubocop.yml CHANGED
@@ -9,3 +9,7 @@ AllCops:
9
9
  # Do not enforce documentation
10
10
  Style/Documentation:
11
11
  Enabled: false
12
+
13
+ # Matching big hashes
14
+ RSpec/ExampleLength:
15
+ Enabled: false
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # BlueprinterSchema
2
2
 
3
- ### Create JSON Schema from [Blueprinter](https://github.com/procore-oss/blueprinter) Serializers and ActiveRecord Models.
4
-
5
- Serializers define which fields are used. Models know the database field types. Put these together and you get a JSON Schema.
3
+ #### Create JSON Schemas from [Blueprinter](https://github.com/procore-oss/blueprinter) Serializers.
6
4
 
7
5
  ## Installation
8
6
 
@@ -14,35 +12,54 @@ gem "blueprinter_schema"
14
12
 
15
13
  ## Usage
16
14
 
17
- With the folloing Models and Serializers:
18
15
  ```rb
19
- class Address < ApplicationRecord
20
- belongs_to :user
16
+ class UserSerializer < Blueprinter::Base
17
+ field :first_name, type: [:string, :null]
18
+ field :last_name, type: [:string, :null]
19
+ field :full_name, type: [:string, :null], description: "The concatendated first and last name."
20
+ field :email, type: :string, format: :email
21
21
  end
22
22
 
23
- class User < ApplicationRecord
24
- has_many :addresses
25
- end
23
+ BlueprinterSchema.generate(serializer: UserSerializer)
24
+ ```
26
25
 
27
- class AddressSerializer < Blueprinter::Base
28
- identifier :id
26
+ ```rb
27
+ {
28
+ "type" => "object",
29
+ "properties" => {
30
+ "first_name" => {
31
+ "type" => ["string", "null"]
32
+ },
33
+ "last_name" => {
34
+ "type" => ["string", "null"]
35
+ },
36
+ "full_name" => {
37
+ "type" => ["string", "null"],
38
+ "description" => "The concatendated first and last name."
39
+ },
40
+ "email" => {
41
+ "type" => "string",
42
+ "format" => "email"
43
+ }
44
+ },
45
+ "required" => ["first_name", "last_name", "full_name", "email"],
46
+ "additionalProperties" => false
47
+ }
48
+ ```
29
49
 
30
- field :address
31
- end
50
+ You can pass an ActiveRecord Model to automatically infer types from DB fields:
32
51
 
52
+ ```rb
33
53
  class UserSerializer < Blueprinter::Base
34
- identifier :id
35
-
36
- field :name, description: 'The name of the user'
37
- fields :email, :created_at
54
+ field :first_name
55
+ field :last_name
56
+ field :email
57
+ end
38
58
 
39
- association :addresses, blueprint: AddressSerializer
59
+ class User < ApplicationRecord
40
60
  end
41
- ```
42
61
 
43
- Generate JSON Schema:
44
- ```rb
45
- BlueprinterSchema.generate(UserSerializer, User)
62
+ BlueprinterSchema.generate(serializer: UserSerializer, model: User)
46
63
  ```
47
64
 
48
65
  ```rb
@@ -50,38 +67,74 @@ BlueprinterSchema.generate(UserSerializer, User)
50
67
  "type" => "object",
51
68
  "title" => "User",
52
69
  "properties" => {
53
- "id" => {
54
- "type" => "integer"
70
+ "first_name" => {
71
+ "type" => ["string", "null"]
55
72
  },
56
- "created_at" => {
57
- "type" => "string", "format" => "date-time"
73
+ "last_name" => {
74
+ "type" => ["string", "null"]
58
75
  },
59
76
  "email" => {
60
77
  "type" => "string"
61
- },
62
- "name" => {
63
- "type" => ["string", "null"],
64
- 'description' => 'The name of the user'
78
+ }
79
+ },
80
+ "required" => ["first_name", "last_name", "email"],
81
+ "additionalProperties" => false
82
+ }
83
+ ```
84
+
85
+ You can use associations:
86
+
87
+ ```rb
88
+ class UserSerializer < Blueprinter::Base
89
+ field :email, type: :string
90
+
91
+ association :addresses, blueprint: AddressSerializer, collection: true
92
+ association :profile, blueprint: ProfileSerializer
93
+ end
94
+
95
+ class AddressSerializer < Blueprinter::Base
96
+ field :address, type: :string
97
+ end
98
+
99
+ class ProfileSerializer < Blueprinter::Base
100
+ field :public, type: :boolean
101
+ end
102
+
103
+ BlueprinterSchema.generate(serializer: UserSerializer)
104
+ ```
105
+
106
+ ```rb
107
+ {
108
+ "type" => "object",
109
+ "properties" => {
110
+ "email" => {
111
+ "type" => "string"
65
112
  },
66
113
  "addresses" => {
67
114
  "type" => "array",
68
115
  "items" => {
69
116
  "type" => "object",
70
- "title" => "Address",
71
117
  "properties" => {
72
- "id" => {
73
- "type" => "integer"
74
- },
75
118
  "address" => {
76
119
  "type" => "string"
77
120
  }
78
121
  },
79
- "required" => ["id", "address"],
122
+ "required" => ["address"],
80
123
  "additionalProperties" => false
81
124
  }
125
+ },
126
+ "profile" => {
127
+ "type" => "object",
128
+ "properties" => {
129
+ "public" => {
130
+ "type" => "boolean"
131
+ }
132
+ },
133
+ "required" => ["public"],
134
+ "additionalProperties" => false
82
135
  }
83
136
  },
84
- "required" => ["id", "created_at", "email", "name"],
137
+ "required" => ["email", "addresses", "profile"],
85
138
  "additionalProperties" => false
86
139
  }
87
140
  ```
@@ -90,13 +143,11 @@ BlueprinterSchema.generate(UserSerializer, User)
90
143
 
91
144
  ```rb
92
145
  BlueprinterSchema.generate(
93
- serializer,
94
- model,
95
- {
96
- include_conditional_fields: true, # Whether or not to include conditional fields from the serializer
97
- fallback_type: {}, # Type when no DB column is found or type is unknown. E.g. { 'type' => 'object' }
98
- view: :default # The Blueprint view to use
99
- }
146
+ serializer:,
147
+ model: nil,
148
+ include_conditional_fields: true, # Whether or not to include conditional fields from the serializer
149
+ fallback_type: {}, # Type when no DB column or type definition is found. E.g. { 'type' => 'object' }
150
+ view: :default # The blueprint view to use
100
151
  )
101
152
  ```
102
153
 
@@ -1,31 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlueprinterSchema
4
+ class InvalidJsonSchemaType < StandardError; end
5
+
6
+ # rubocop:disable Metrics/ClassLength
4
7
  class Generator
5
- def initialize(serializer, model, options)
8
+ def initialize(serializer:, model:, skip_conditional_fields:, fallback_definition:, view:)
6
9
  @serializer = serializer
7
10
  @model = model
8
- @options = options
11
+ @skip_conditional_fields = skip_conditional_fields
12
+ @fallback_definition = fallback_definition
13
+ @view = view
9
14
  end
10
15
 
11
16
  def generate
12
- {
17
+ schema = {
13
18
  'type' => 'object',
14
- 'title' => @model.name,
15
19
  'properties' => build_properties,
16
20
  'required' => build_required_fields,
17
21
  'additionalProperties' => false
18
22
  }
23
+
24
+ schema['title'] = @model.name if @model
25
+
26
+ schema
19
27
  end
20
28
 
21
29
  private
22
30
 
23
31
  def fields
24
- @fields ||= @serializer.reflections[@options[:view]].fields
32
+ @fields ||= @serializer.reflections[@view].fields
25
33
  end
26
34
 
27
35
  def associations
28
- @associations ||= @serializer.reflections[@options[:view]].associations
36
+ @associations ||= @serializer.reflections[@view].associations
29
37
  end
30
38
 
31
39
  def build_properties
@@ -45,20 +53,41 @@ module BlueprinterSchema
45
53
  end
46
54
 
47
55
  def skip_field?(field)
48
- !@options[:include_conditional_fields] &&
49
- (field.options[:if] || field.options[:unless] || field.options[:exclude_if_nil])
56
+ @skip_conditional_fields && (field.options[:if] || field.options[:unless])
50
57
  end
51
58
 
52
59
  def build_required_fields
53
- fields.reject { |_, field| skip_field?(field) }.keys.map(&:to_s)
60
+ fields
61
+ .reject { |_, field| field.options[:exclude_if_nil] }
62
+ .reject { |_, field| skip_field?(field) }
63
+ .keys.map(&:to_s)
54
64
  end
55
65
 
66
+ # rubocop:disable Metrics/AbcSize
56
67
  def field_to_json_schema(field)
57
- column = @model.columns_hash[field.name.to_s]
68
+ type_definition = @fallback_definition.dup
58
69
 
59
- type = ar_column_to_json_schema(column) || @options[:fallback_type]
60
- type['description'] = field.options[:description] if field.options[:description]
61
- type
70
+ if field.options[:type]
71
+ type_definition['type'] = ensure_valid_json_schema_types!(field)
72
+ elsif @model
73
+ column = @model.columns_hash[field.name.to_s]
74
+ type_definition = ar_column_to_json_schema(column)
75
+ end
76
+
77
+ type_definition['format'] = field.options[:format] if field.options[:format]
78
+ type_definition['description'] = field.options[:description] if field.options[:description]
79
+ type_definition
80
+ end
81
+ # rubocop:enable Metrics/AbcSize
82
+
83
+ def ensure_valid_json_schema_types!(field)
84
+ types = [field.options[:type]].flatten.map(&:to_s)
85
+
86
+ return field.options[:type] if types.all? do |type|
87
+ %w[string integer number boolean object array null].include?(type)
88
+ end
89
+
90
+ raise BlueprinterSchema::InvalidJsonSchemaType, "Invalid type: #{field.options[:type]}"
62
91
  end
63
92
 
64
93
  # rubocop:disable Metrics/MethodLength
@@ -95,13 +124,23 @@ module BlueprinterSchema
95
124
 
96
125
  return { 'type' => 'object' } unless blueprint_class
97
126
 
98
- ar_association = @model.reflect_on_association(association.name)
99
- is_collection = ar_association.collection?
100
- association_model = ar_association.klass
127
+ ar_association = @model&.reflect_on_association(association.name)
128
+ is_collection = ar_association ? ar_association.collection? : association.options[:collection]
101
129
 
102
- associated_schema = BlueprinterSchema.generate(blueprint_class, association_model)
130
+ associated_schema = recursive_generate(blueprint_class, ar_association&.klass)
103
131
 
104
132
  is_collection ? { 'type' => 'array', 'items' => associated_schema } : associated_schema
105
133
  end
134
+
135
+ def recursive_generate(serializer, model)
136
+ BlueprinterSchema.generate(
137
+ serializer:,
138
+ model:,
139
+ skip_conditional_fields: @skip_conditional_fields,
140
+ fallback_definition: @fallback_definition,
141
+ view: @view
142
+ )
143
+ end
106
144
  end
145
+ # rubocop:enable Metrics/ClassLength
107
146
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlueprinterSchema
4
- VERSION = '0.6.0'
4
+ VERSION = '1.0.1'
5
5
  end
@@ -5,10 +5,12 @@ require_relative 'blueprinter_schema/generator'
5
5
 
6
6
  module BlueprinterSchema
7
7
  def self.generate(
8
- serializer,
9
- model,
10
- options = { include_conditional_fields: true, fallback_type: {}, view: :default }
8
+ serializer:,
9
+ model: nil,
10
+ skip_conditional_fields: false,
11
+ fallback_definition: {},
12
+ view: :default
11
13
  )
12
- Generator.new(serializer, model, options).generate
14
+ Generator.new(serializer:, model:, skip_conditional_fields:, fallback_definition:, view:).generate
13
15
  end
14
16
  end
metadata CHANGED
@@ -1,42 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blueprinter_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - thisismydesign
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: activerecord
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0'
26
- - !ruby/object:Gem::Dependency
27
- name: blueprinter
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '0'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '0'
11
+ dependencies: []
40
12
  email:
41
13
  - git.thisismydesign@gmail.com
42
14
  executables: []