sober_swag 0.20.0 → 0.21.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 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