sober_swag 0.20.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97cb4bbfd84f28f6f79368c3f78182835217868a1d3142c38107ab3b03552952
4
- data.tar.gz: 1cd8446250a8ddadc16eb114a773b9d317c2182e2bcb8c692c59681331ef972b
3
+ metadata.gz: f9dce6daadaff9f7ddb1530ecf5fa69693454a29f04da849eec9bea8969166bc
4
+ data.tar.gz: a6cd16e93640b2d9c27fab081c3551d8498643044a6ebf9cab8100b51d8d916d
5
5
  SHA512:
6
- metadata.gz: f6b055ea451db16f12a02ebe2bc6da459aefcf13605a54f4f0b0cff8aa694b73e2c4cb2c2f759a89c859a820a74a37391728998e0939a770cda2783c2190586e
7
- data.tar.gz: 3a49c153297750447c2776b086da135b81c80a1b628780d9a7ab6f68f204ddeaa3b21e08ffd02f8a49e92f1a7ac5942b0e42d295307299e8623ebb6a765731dd
6
+ metadata.gz: fb2dcb7bee3b89e643b3e5d2d1e3d5b7034161ae563de59909120a3f4c817a8f14ab88d5d1968efb2b914b068cf0370d41f9094bbec7ae8ef96a87dfe3ad64fd
7
+ data.tar.gz: bfc9bfe3b4e93dca2a774e416496d430dfa63185acc82db780cc4f26d7eec1ff2a6db0f10103fd962243ab46b9799b8e6536dd94cc07758b5f52ea07af71df67
@@ -0,0 +1,39 @@
1
+ name: Ruby Benchmark
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ benchmark:
11
+
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ ruby: [ '2.6', '2.7', '3.0' ]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v2
19
+ - name: Set up Ruby
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.ruby }}
23
+ - uses: actions/cache@v2
24
+ with:
25
+ path: vendor/bundle
26
+ key: ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
27
+ restore-keys: |
28
+ ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-
29
+ - name: Install dependencies
30
+ run: |
31
+ bundle config path vendor/bundle
32
+ bundle install
33
+ - name: Run Benchmark
34
+ run: bundle exec ruby bench/benchmark.rb
35
+ - uses: actions/upload-artifact@v2
36
+ with:
37
+ name: benchmark-result
38
+ path: benchmark_results.yaml
39
+ if-no-files-found: error
@@ -14,13 +14,11 @@ on:
14
14
  branches: [ master ]
15
15
 
16
16
  jobs:
17
- test:
18
-
17
+ lint:
19
18
  runs-on: ubuntu-latest
20
19
  strategy:
21
20
  matrix:
22
21
  ruby: [ '2.6', '2.7', '3.0' ]
23
-
24
22
  steps:
25
23
  - uses: actions/checkout@v2
26
24
  - name: Set up Ruby
data/.gitignore CHANGED
@@ -17,3 +17,4 @@ Gemfile.lock
17
17
  /vendor/
18
18
 
19
19
  .yardoc
20
+ benchmark_results.yaml
data/CHANGELOG.md CHANGED
@@ -1,9 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.21.0] 2021-09-02
4
+
5
+ - Added a new method of serializing views based on hash lookups, improving performance
6
+ - Added a benchmarking suite
7
+ - Added `except` parameter to the `merge` method, which allows a specified field to be excluded from the merge.
8
+ - Add `type_key` to output objects, for easily serializing out type fields with a constant string.
9
+ - Added `type_attribute` to `SoberSwag::InputObject` to add easy constant-value disambiguation.
10
+
3
11
  ## [v0.20.0] 2021-05-17
4
12
 
5
13
  - Added YARD documentation to almost every method
6
- - Added `except` parameter to the `merge` method, which allows a specified field to be excluded from the merge.
14
+
7
15
 
8
16
  ## [v0.19.0] 2021-03-10
9
17
 
data/Gemfile CHANGED
@@ -12,3 +12,7 @@ gem 'redcarpet'
12
12
  gem 'yard-activesupport-concern'
13
13
 
14
14
  gem 'solargraph'
15
+
16
+ gem 'benchmark-ips'
17
+
18
+ gem 'rspec-its'
@@ -0,0 +1,34 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'sober_swag'
4
+
5
+ require 'yaml'
6
+ require 'benchmark/ips'
7
+
8
+ ##
9
+ # Quick and dirty way to benchmark things.
10
+ class Bench
11
+ class << self
12
+ def report(name, &block)
13
+ puts name
14
+
15
+ data[name] ||= Benchmark.ips(&block).data
16
+ end
17
+
18
+ def data
19
+ @data ||= {}
20
+ end
21
+
22
+ def write!(filename)
23
+ File.open(filename, 'w') do |f|
24
+ f << YAML.dump(data)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ Dir['bench/benchmarks/**/*.rb'].sort.each do |file|
31
+ require_relative file.gsub(%r{^bench/}, '')
32
+ end
33
+
34
+ Bench.write!('benchmark_results.yaml')
@@ -0,0 +1,21 @@
1
+ ##
2
+ # Bench test for serializing multiple fields.
3
+ class BasicFieldSerializer
4
+ Idea = Struct.new(:name, :grade, :cool)
5
+
6
+ Output = SoberSwag::OutputObject.define do
7
+ field :name, primitive(:String)
8
+ field :grade, primitive(:Integer)
9
+ field :cool, primitive(:Bool)
10
+ end
11
+
12
+ OutputSerializer = Output.serializer
13
+
14
+ MyIdea = Idea.new('Bob', 12, false)
15
+
16
+ Bench.report 'Basic Field Serializers' do |bm|
17
+ bm.report('Output Object') { Output.serialize(MyIdea) }
18
+ bm.report('Serializer of Output Object') { OutputSerializer.serialize(MyIdea) }
19
+ bm.compare!
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ ##
2
+ # Benchmark for speed of selecting what view to use.
3
+ class ViewSelection
4
+ Accomplishment = Struct.new(:name, :description)
5
+ Person = Struct.new(:first_name, :last_name, :accomplishments)
6
+
7
+ MyPerson = Person.new(
8
+ 'Joeseph',
9
+ 'Biden',
10
+ [
11
+ Accomplishment.new('Became President', 'Won a Presidential Election'),
12
+ Accomplishment.new('Oldest President', 'Oldest man to be elected president at time of election'),
13
+ Accomplishment.new('Became Senator', 'Got Elected to the Senate'),
14
+ Accomplishment.new('Youngest Senator', 'Youngest person elected Senator at time of election')
15
+ ]
16
+ )
17
+
18
+ AccomplishmentSerializer = SoberSwag::OutputObject.define do
19
+ field :name, primitive(:String)
20
+
21
+ view :detail do
22
+ field :description, primitive(:String)
23
+ end
24
+ end
25
+
26
+ PersonSerializer = SoberSwag::OutputObject.define do
27
+ field :first_name, primitive(:String)
28
+ field :last_name, primitive(:String)
29
+
30
+ # make a bunch of dummy views
31
+ 1.upto(10).each { |n| view(:"view_#{n}") {} }
32
+
33
+ view :detail do
34
+ field :accomplishments, AccomplishmentSerializer.view(:detail)
35
+ end
36
+
37
+ 1.upto(10).each { |n| view(:"view_after_#{n}") {} }
38
+ end
39
+
40
+ Bench.report 'View Selection' do |bm|
41
+ bm.report('With no view') { PersonSerializer.serialize(MyPerson) }
42
+
43
+ bm.report('With a view') { PersonSerializer.serialize(MyPerson, { view: :detail }) }
44
+
45
+ bm.compare!
46
+ end
47
+ end
data/docs/serializers.md CHANGED
@@ -105,7 +105,7 @@ This changes the type properly too.
105
105
 
106
106
  98% of the time, when we're writing web APIs, we want to transform our domain objects into JSON objects.
107
107
  We often want different ways to do this, too.
108
- Consider, for exmaple, and API for a college.
108
+ Consider, for example, an API for a college.
109
109
  We might want to provide one detailed way to serialize a student, which includes their full name, grade, student ID, GPA, and so on.
110
110
  On another page, we might want to display a classroom with a list of students.
111
111
  However, on the classroom page, we don't want to serialize a full student: that's sending too much data.
data/example/Gemfile CHANGED
@@ -8,7 +8,7 @@ gem 'actionpack', '>= 6.0.3.2'
8
8
  # Use sqlite3 as the database for Active Record
9
9
  gem 'sqlite3', '~> 1.4'
10
10
  # Use Puma as the app server
11
- gem 'puma', '~> 5.2'
11
+ gem 'puma', '~> 5.3'
12
12
  # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
13
13
  # gem 'jbuilder', '~> 2.7'
14
14
  # Use Active Model has_secure_password
@@ -34,7 +34,7 @@ group :development, :test do
34
34
  end
35
35
 
36
36
  group :development do
37
- gem 'listen', '>= 3.0.5', '< 3.6'
37
+ gem 'listen', '>= 3.0.5', '< 3.7'
38
38
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
39
39
  gem 'spring'
40
40
  gem 'spring-watcher-listen', '~> 2.0.0'
data/example/Gemfile.lock CHANGED
@@ -64,7 +64,7 @@ GEM
64
64
  minitest (~> 5.1)
65
65
  tzinfo (~> 1.1)
66
66
  zeitwerk (~> 2.2, >= 2.2.2)
67
- bootsnap (1.7.3)
67
+ bootsnap (1.7.5)
68
68
  msgpack (~> 1.0)
69
69
  builder (3.2.4)
70
70
  byebug (11.1.3)
@@ -103,13 +103,13 @@ GEM
103
103
  dry-types (>= 0.8.1)
104
104
  rails (>= 3)
105
105
  erubi (1.10.0)
106
- ffi (1.15.0)
106
+ ffi (1.15.3)
107
107
  globalid (0.4.2)
108
108
  activesupport (>= 4.2.0)
109
109
  i18n (1.8.9)
110
110
  concurrent-ruby (~> 1.0)
111
111
  ice_nine (0.11.2)
112
- listen (3.5.0)
112
+ listen (3.6.0)
113
113
  rb-fsevent (~> 0.10, >= 0.10.3)
114
114
  rb-inotify (~> 0.9, >= 0.9.10)
115
115
  loofah (2.9.0)
@@ -124,17 +124,17 @@ GEM
124
124
  nokogiri (~> 1)
125
125
  rake
126
126
  mini_mime (1.0.2)
127
- mini_portile2 (2.5.0)
127
+ mini_portile2 (2.5.1)
128
128
  minitest (5.14.4)
129
129
  msgpack (1.4.2)
130
130
  nio4r (2.5.7)
131
- nokogiri (1.11.2)
131
+ nokogiri (1.11.5)
132
132
  mini_portile2 (~> 2.5.0)
133
133
  racc (~> 1.4)
134
- pry (0.14.0)
134
+ pry (0.14.1)
135
135
  coderay (~> 1.1)
136
136
  method_source (~> 1.0)
137
- puma (5.2.2)
137
+ puma (5.3.2)
138
138
  nio4r (~> 2.0)
139
139
  racc (1.5.2)
140
140
  rack (2.2.3)
@@ -167,7 +167,7 @@ GEM
167
167
  rake (>= 0.8.7)
168
168
  thor (>= 0.20.3, < 2.0)
169
169
  rake (13.0.3)
170
- rb-fsevent (0.10.4)
170
+ rb-fsevent (0.11.0)
171
171
  rb-inotify (0.10.1)
172
172
  ffi (~> 1.0)
173
173
  rspec-core (3.10.1)
@@ -216,9 +216,9 @@ DEPENDENCIES
216
216
  bootsnap (>= 1.4.2)
217
217
  byebug
218
218
  dry-types-rails
219
- listen (>= 3.0.5, < 3.6)
219
+ listen (>= 3.0.5, < 3.7)
220
220
  pry
221
- puma (~> 5.2)
221
+ puma (~> 5.3)
222
222
  rails (~> 6.0.2, >= 6.0.2.2)
223
223
  rspec-rails
224
224
  sober_swag!
@@ -46,6 +46,40 @@ module SoberSwag
46
46
  super(key, parent, &block)
47
47
  end
48
48
 
49
+ ##
50
+ # Add on an attribute which only ever parses from a constant value.
51
+ # By default, this attribute will be called `type`, but you can override it with the kwarg.
52
+ # This is useful in situations where you want to emulate a sum type.
53
+ # For example, if you want to make an API endpoint that can either accept or reject proposals
54
+ #
55
+ # ```ruby
56
+ #
57
+ # ApiInputType = SoberSwag.input_object {
58
+ # identifier 'AcceptProposal'
59
+ # type_attribute 'accept'
60
+ # attribute(:message, primitive(:String))
61
+ # } | SoberSwag.input_object {
62
+ # identifier 'RejectProposal'
63
+ # type_attribute 'reject'
64
+ # attribute(:message, primitive(:String))
65
+ # }
66
+ # ```
67
+ #
68
+ # Under the hood, this basically looks like:
69
+ #
70
+ # ```ruby
71
+ # type_attribute 'archive'
72
+ # # is equivalent to
73
+ #
74
+ # attribute(:type, SoberSwag::Types::String.enum('archive'))
75
+ # ```
76
+ #
77
+ # @param value [String,Symbol] the value to parse
78
+ # @param attribute_key [Symbol] what key to use
79
+ def type_attribute(value, attribute_key: :type)
80
+ attribute(attribute_key, SoberSwag::Types::String.enum(value.to_s))
81
+ end
82
+
49
83
  ##
50
84
  # @overload attribute(key, parent = SoberSwag::InputObject, &block)
51
85
  # Defines an optional attribute by defining a sub-object inline.
@@ -19,6 +19,32 @@ module SoberSwag
19
19
 
20
20
  include FieldSyntax
21
21
 
22
+ ##
23
+ # Adds a "type key", which is basically a field that will be
24
+ # serialized out to a constant value.
25
+ #
26
+ # This is useful if you have multiple types you may want to serialize out, and want consumers of your API to distinguish between them.
27
+ #
28
+ # By default this will have the key "type" but you can set it with the keyword arg.
29
+ # ```ruby
30
+ # type_key('MyObject')
31
+ # # is equivalent to
32
+ # field :type, SoberSwag::Serializer::Primitive.new(SoberSwag::Types::String.enum('MyObject')) do |_, _|
33
+ # 'MyObject'
34
+ # end
35
+ # ```
36
+ # @param str [String, Symbol] the value to serialize (will be converted to a string)
37
+ # @param
38
+ def type_key(str, field_name: :type)
39
+ str = str.to_s
40
+ field(
41
+ field_name,
42
+ SoberSwag::Serializer::Primitive.new(
43
+ SoberSwag::Types::String.enum(str)
44
+ )
45
+ ) { |_, _| str }
46
+ end
47
+
22
48
  ##
23
49
  # Adds a new field to the fields array
24
50
  # @param field [SoberSwag::OutputObject::Field]
@@ -37,6 +37,7 @@ module SoberSwag
37
37
  # the correct thing, with the name you give it. This works for now, though.
38
38
  #
39
39
  # @return [Class] the serializer generated.
40
+ # @yieldself [SoberSwag::OutputObject::Definition]
40
41
  def self.define(&block)
41
42
  d = Definition.new.tap do |o|
42
43
  o.instance_eval(&block)
@@ -99,23 +100,12 @@ module SoberSwag
99
100
  # and {SoberSwag::Serializer::FieldList} to do the actual serialization.
100
101
  #
101
102
  # @todo: optimize view selection to use binary instead of linear search
102
- def serializer # rubocop:disable Metrics/MethodLength
103
+ def serializer
103
104
  @serializer ||=
104
105
  begin
105
- views.reduce(base_serializer) do |base, view|
106
- view_serializer = view.serializer
107
- SoberSwag::Serializer::Conditional.new(
108
- proc do |object, options|
109
- if options[:view].to_s == view.name.to_s
110
- [:left, object]
111
- else
112
- [:right, object]
113
- end
114
- end,
115
- view_serializer,
116
- base
117
- )
118
- end
106
+ view_choices = views.map { |view| [view.name.to_s, view.serializer] }.to_h
107
+ view_choices['base'] = base_serializer
108
+ SoberSwag::Serializer::Hash.new(view_choices, base, proc { |_, options| options[:view]&.to_s })
119
109
  end
120
110
  end
121
111
 
@@ -27,9 +27,11 @@ module SoberSwag
27
27
  # @param options [Hash] arbitrary options
28
28
  # @return [Hash] serialized object.
29
29
  def serialize(object, options = {})
30
- field_list.map { |field|
31
- [field.name, field.serializer.serialize(object, options)]
32
- }.to_h
30
+ {}.tap do |hash|
31
+ field_list.each do |field|
32
+ hash[field.name] = field.serializer.serialize(object, options)
33
+ end
34
+ end
33
35
  end
34
36
 
35
37
  ##
@@ -0,0 +1,53 @@
1
+ require 'set'
2
+
3
+ module SoberSwag
4
+ module Serializer
5
+ ##
6
+ # Serialize via hash lookup.
7
+ # This is used to speed up serialization of views, but it may be useful elsewhere.
8
+ #
9
+ class Hash < Base
10
+ ##
11
+ # @param choices [Hash<Object => SoberSwag::Serializer::Base>] hash of serializers
12
+ # that we might use.
13
+ # @param default [SoberSwag::Serializer::Base] default to use if key not found.
14
+ # @param key_proc [Proc<Object, Hash>] extract the key we are interested in from the proc.
15
+ # Will be called with the object to serialize and the options hash.
16
+ def initialize(choices, default, key_proc)
17
+ @choices = choices
18
+ @default = default
19
+ @key_proc = key_proc
20
+ end
21
+
22
+ attr_reader :choices, :default, :key_proc
23
+
24
+ def serialize(object, options = {})
25
+ key = key_proc.call(object, options)
26
+
27
+ choices.fetch(key) { default }.serialize(object, options)
28
+ end
29
+
30
+ ##
31
+ # @return [Set<SoberSwag::Serializer::Base>]
32
+ def possible_serializers
33
+ @possible_serializers ||= (choices.values + [default]).to_set
34
+ end
35
+
36
+ def lazy_type?
37
+ possible_serializers.any?(&:lazy_type?)
38
+ end
39
+
40
+ def finalize_lazy_type!
41
+ possible_serializers.each(&:finalize_lazy_type!)
42
+ end
43
+
44
+ def lazy_type
45
+ @lazy_type ||= possible_serializers.map(&:lazy_type).reduce(:|)
46
+ end
47
+
48
+ def type
49
+ @type ||= possible_serializers.map(&:type).reduce(:|)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -10,6 +10,7 @@ module SoberSwag
10
10
  autoload(:Mapped, 'sober_swag/serializer/mapped')
11
11
  autoload(:Optional, 'sober_swag/serializer/optional')
12
12
  autoload(:FieldList, 'sober_swag/serializer/field_list')
13
+ autoload(:Hash, 'sober_swag/serializer/hash')
13
14
  autoload(:Meta, 'sober_swag/serializer/meta')
14
15
 
15
16
  class << self
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SoberSwag
4
- VERSION = '0.20.0'
4
+ VERSION = '0.21.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sober_swag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anthony Super
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-05-17 00:00:00.000000000 Z
11
+ date: 2021-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -173,6 +173,7 @@ extra_rdoc_files: []
173
173
  files:
174
174
  - ".github/config/rubocop_linter_action.yml"
175
175
  - ".github/dependabot.yml"
176
+ - ".github/workflows/benchmark.yml"
176
177
  - ".github/workflows/lint.yml"
177
178
  - ".github/workflows/ruby.yml"
178
179
  - ".gitignore"
@@ -186,6 +187,9 @@ files:
186
187
  - LICENSE.txt
187
188
  - README.md
188
189
  - Rakefile
190
+ - bench/benchmark.rb
191
+ - bench/benchmarks/basic_field_serializer.rb
192
+ - bench/benchmarks/view_selection.rb
189
193
  - bin/console
190
194
  - bin/rspec
191
195
  - bin/setup
@@ -284,6 +288,7 @@ files:
284
288
  - lib/sober_swag/serializer/base.rb
285
289
  - lib/sober_swag/serializer/conditional.rb
286
290
  - lib/sober_swag/serializer/field_list.rb
291
+ - lib/sober_swag/serializer/hash.rb
287
292
  - lib/sober_swag/serializer/mapped.rb
288
293
  - lib/sober_swag/serializer/meta.rb
289
294
  - lib/sober_swag/serializer/optional.rb