json_schematize 0.5.1 → 0.6.1

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: f7c2935672acaea1a2d0dd6cb77478ba381b3b59af9f6e67006e950a11a31d31
4
- data.tar.gz: aa3af45a457f4c0e842c4d39366158f4ec833f123ef85fddb137706918a20359
3
+ metadata.gz: d0b80648f60c06f94a7530ba812d49891e57d28549cd63f1ba42e7adfd74cb03
4
+ data.tar.gz: 0b97d3b638941ce37b2907640840759fc9462df4447d04f3828847bcb580aa8d
5
5
  SHA512:
6
- metadata.gz: 7a02efdb0960ba3bbfa9369a1e6a4d0b987bd414d676f1b701c5f2890e59153312dd490751f802fb0f27fbd51686c86fe00e6c6a5ab6ed83cfee2477aaf689fb
7
- data.tar.gz: 4c0b7cae2ffed160d64c925c60ac00004bc9a688476021615ccd8889ecc45839d225648d581b5a258644869b7f528a215855e7db250de1b3dcc4054ba42e913d
6
+ metadata.gz: cb8ac2f474f45303d6c9f17924ba1a92de71cb6cd645382ec12579e2d69274039ba8bc67c528c08f3fad0f2103265ac2792c8c88f12aec37b754b47a61f71691
7
+ data.tar.gz: 3f49f060c7fbcefd0705256e23853eb4220c5d63636c75be1e15f721b3fa2d0d631965424101e69cee1c4108d1976191a4717d1643ff15250a746b48d0794002
data/.circleci/config.yml CHANGED
@@ -8,6 +8,7 @@ orbs:
8
8
  common_envs: &common_envs
9
9
  environment:
10
10
  APP_BUNDLER_VERSION: "2.3.8"
11
+ REDIS_URL: "redis://127.0.0.1"
11
12
 
12
13
  executors:
13
14
  gem:
@@ -17,6 +18,8 @@ executors:
17
18
  type: string
18
19
  docker:
19
20
  - image: cimg/ruby:<< parameters.ruby-version >>
21
+ - image: circleci/redis:6.0
22
+
20
23
  <<: *common_envs
21
24
 
22
25
  commands:
@@ -146,6 +149,16 @@ jobs:
146
149
  command: |
147
150
  curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
148
151
  chmod +x ./cc-test-reporter
152
+ - run:
153
+ name: Waiting for Redis to be ready
154
+ command: |
155
+ for i in `seq 1 10`;
156
+ do
157
+ nc -z localhost 6379 && echo Success && exit 0
158
+ echo -n .
159
+ sleep 1
160
+ done
161
+ echo Failed waiting for Redis to be available && exit 1
149
162
  - run:
150
163
  name: Run and Report tests
151
164
  command: |
data/CHANGELOG.md CHANGED
@@ -7,10 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.1.0] - YYYY-MM-DD
10
+ ## [0.6.0] - 2021-04-01
11
11
 
12
12
  ### Added
13
- - New feature 1
13
+ - Caching Layer
14
14
  - New feature 2
15
15
 
16
16
  ### Changed
@@ -24,3 +24,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
24
24
  ### Fixed
25
25
  - Bug fix 1
26
26
  - Bug fix 2
27
+
28
+ [//]: # (Added, Changed, Removed, Fixed are the possible headers for each changlog version)
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ gem 'redis'
7
8
  gem 'faker'
8
9
  gem 'pry'
9
10
  gem 'rspec', '~> 3.0'
data/Gemfile.lock CHANGED
@@ -1,14 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- json_schematize (0.5.1)
4
+ json_schematize (0.6.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  byebug (11.1.3)
10
10
  coderay (1.1.3)
11
- concurrent-ruby (1.1.9)
11
+ concurrent-ruby (1.1.10)
12
12
  diff-lcs (1.5.0)
13
13
  docile (1.4.0)
14
14
  faker (2.20.0)
@@ -23,6 +23,7 @@ GEM
23
23
  byebug (~> 11.0)
24
24
  pry (~> 0.13.0)
25
25
  rake (12.3.3)
26
+ redis (4.6.0)
26
27
  rspec (3.11.0)
27
28
  rspec-core (~> 3.11.0)
28
29
  rspec-expectations (~> 3.11.0)
@@ -32,7 +33,7 @@ GEM
32
33
  rspec-expectations (3.11.0)
33
34
  diff-lcs (>= 1.2.0, < 2.0)
34
35
  rspec-support (~> 3.11.0)
35
- rspec-mocks (3.11.0)
36
+ rspec-mocks (3.11.1)
36
37
  diff-lcs (>= 1.2.0, < 2.0)
37
38
  rspec-support (~> 3.11.0)
38
39
  rspec-support (3.11.0)
@@ -54,6 +55,7 @@ DEPENDENCIES
54
55
  pry
55
56
  pry-byebug
56
57
  rake (~> 12.0)
58
+ redis
57
59
  rspec (~> 3.0)
58
60
  rspec_junit_formatter
59
61
  simplecov
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Matt Taylor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -110,6 +110,87 @@ class CustomClasses < JsonSchematize::Generator
110
110
  end
111
111
  ```
112
112
 
113
+ ### Caching Adapter
114
+
115
+ JsonSchematize is built to be schema for API results. But what happens when you dont expect the result to change? Introducing the caching layer. This layer lets you cache a `JsonSchematize` object that can be queried from later
116
+
117
+ **Note: This requires redis**
118
+
119
+ ```ruby
120
+ class CachedClass < JsonSchematize::Generator
121
+ include JsonSchematize::Cache
122
+ cache_options key: ->(instance_of_cached_class, cache_key_from_initialization) { "#{instance_of_cached_class.id}:#{cache_key_from_initialization}" }
123
+ cache_options ttl: 7.days.to_i
124
+
125
+ schema_default option: :dig_type, value: :string
126
+
127
+ add_field name: :id, type: Integer
128
+ end
129
+
130
+ params = { id: 1 }
131
+ CachedClass.new(cache_key: User.first.id, **params)
132
+ ###
133
+ params = { "id" => 1 }
134
+ CachedClass.new(params, cache_key: User.first.id)
135
+ ```
136
+
137
+ #### Instance methods for Cache
138
+ ```ruby
139
+ # optional cache_key added on initialization: Can be used to customize the cache entry for the instance
140
+ item = CachedClass.new(cache_key: User.first.id, **params)
141
+
142
+ # Update the cached item -- Note: This will overwrite the previous cached item IFF the `__cache_key__` remains the same. This is not gaurenteed
143
+ # Optional Param: with_delete, Default true -- Will attempt to delete the original object
144
+ item.__update_cache_item__
145
+
146
+ # Manually delete the cached entry
147
+ item.__clear_entry__!
148
+
149
+ # Cache key for the item
150
+ item.__cache_key__
151
+ ```
152
+
153
+ #### Class methods for Cache
154
+ ```ruby
155
+ # Retrieve all cached items for the class. Returns an array of CachedClass objects. Only objects that have not expired via TTL
156
+ # Optional: key_includes: "string_expected_in_cache", default is nil and return everthing
157
+ CachedClass.cached_items
158
+
159
+ # Retrieves all valid object keys from the cache
160
+ CachedClass.cached_keys
161
+
162
+ # Clears all cached items for the given class
163
+ CachedClass.clear_cache!
164
+
165
+ # manually clear objects that have expired
166
+ CachedClass.clear_unscored_items!
167
+ ```
168
+ ### Cache options
169
+ ```ruby
170
+ # [Required] false; [Expect] Proc; [Return] String to be used as instance key; [Default] key will be the hash of the object
171
+ cache_options key: ->(instance_of_class, custom_key) { }
172
+
173
+ # [Required] false; [Expect] String; [Default] ENV["CACHE_LAYER_REDIS_URL"] || ENV["REDIS_URL"]
174
+ cache_options redis_url: _redis_url_value
175
+
176
+ # [Required] false; [Expect] Redis Object or Proc that returns value of Redis Client; [Default] Redis.new(url: redis_url)
177
+ cache_options redis_client: redis_client
178
+
179
+ # [Required] false; [Expect] Object that plays with to_s and has no spaces; [Default] Full class name downcased
180
+ cache_options cache_namespace: cache_namespace
181
+
182
+ # [Required] false; [Expect] Integer in seconds; [Default] 1 day
183
+ cache_options ttl: (60 * 60)
184
+
185
+ # [Required] false; [Expect] Boolean; [Default] true
186
+ # Update the cache when a value has been changed manually
187
+ cache_options update_on_change: true
188
+
189
+ # [Required] false; [Expect] Float; [Default] 0.8
190
+ # Expected value to be between 0 and 1. The sample rate that the class will clear oldcache values on retreival
191
+ cache_options stochastic_cache_bust: 0.8
192
+ ```
193
+
113
194
  ## Development
114
195
 
115
196
  This gem can be developed against local machine or while using docker. Simpleified Docker commands can be found in the `Makefile` or execute `make help`
data/docker-compose.yml CHANGED
@@ -10,6 +10,13 @@ services:
10
10
  - .:/gem
11
11
  - ..:/local
12
12
  - bundle-cache:/usr/local/bundle:delegated
13
-
13
+ environment:
14
+ REDIS_URL: redis://redis
15
+ links:
16
+ - redis
17
+ redis:
18
+ image: redis
19
+ expose:
20
+ - 6379
14
21
  volumes:
15
22
  bundle-cache:
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class JsonSchematize::Base
4
- def self.acceptable_types
5
- raise NoMethodError, "Expected acceptable_values to be defined in parent class"
3
+ module JsonSchematize
4
+ class Base
5
+ def self.acceptable_types
6
+ raise NoMethodError, "Expected acceptable_values to be defined in parent class"
7
+ end
6
8
  end
7
9
  end
@@ -2,18 +2,20 @@
2
2
 
3
3
  require "json_schematize/base"
4
4
 
5
- class JsonSchematize::Boolean < JsonSchematize::Base
6
- FALSE_VALUES = ["false", "f", "0", false]
7
- TRUE_VALUES = ["true", "t", "1", true]
5
+ module JsonSchematize
6
+ class Boolean < JsonSchematize::Base
7
+ FALSE_VALUES = ["false", "f", "0", false]
8
+ TRUE_VALUES = ["true", "t", "1", true]
8
9
 
9
- def self.new(val)
10
- return false if FALSE_VALUES.include?(val)
11
- return true if TRUE_VALUES.include?(val)
10
+ def self.new(val)
11
+ return false if FALSE_VALUES.include?(val)
12
+ return true if TRUE_VALUES.include?(val)
12
13
 
13
- raise JsonSchematize::UndefinedBoolean, "#{val} is not a valid #{self.class}"
14
- end
14
+ raise JsonSchematize::UndefinedBoolean, "#{val} is not a valid #{self.class}"
15
+ end
15
16
 
16
- def self.acceptable_types
17
- [TrueClass, FalseClass]
17
+ def self.acceptable_types
18
+ [TrueClass, FalseClass]
19
+ end
18
20
  end
19
21
  end
@@ -0,0 +1,81 @@
1
+ module JsonSchematize
2
+ module Cache
3
+ module ClassMethods
4
+ DEFAULT_ONE_MIN = 60 * 60
5
+ DEFAULT_ONE_HOUR = DEFAULT_ONE_MIN * 60
6
+ DEFAULT_ONE_DAY = DEFAULT_ONE_HOUR * 24
7
+ DEFAULT_URL = ENV["CACHE_LAYER_REDIS_URL"] || ENV["REDIS_URL"]
8
+ DEFAULTS = {
9
+ redis_url: DEFAULT_URL,
10
+ ttl: DEFAULT_ONE_DAY,
11
+ key: ->(val, _custom_key) { val.hash },
12
+ update_on_change: true,
13
+ redis_client: ->() { ::Redis.new(url: DEFAULT_URL) },
14
+ stochastic_cache_bust: 0.8,
15
+ }
16
+
17
+ def cache_options(key: nil, redis_url: nil, redis_client: nil, cache_namespace: nil, ttl: nil, update_on_change: nil, stochastic_cache_bust: nil)
18
+ cache_configuration[:key] = key if key
19
+ cache_configuration[:ttl] = ttl if ttl
20
+ cache_configuration[:stochastic_cache_bust] = stochastic_cache_bust if stochastic_cache_bust
21
+ cache_configuration[:update_on_change] = update_on_change if update_on_change
22
+ cache_namespace = cache_configuration[:cache_namespace] = cache_namespace if cache_namespace
23
+
24
+ self.redis_client = cache_configuration[:redis_client] = redis_client if redis_client
25
+ self.redis_url = cache_configuration[:redis_url] = redis_url if redis_url
26
+ end
27
+
28
+ def cache_namespace
29
+ cache_configuration[:cache_namespace] ||= "jss:#{self.name.downcase}"
30
+ end
31
+
32
+ def cache_namespace=(namespace)
33
+ cache_configuration[:cache_namespace] = namespace
34
+ end
35
+
36
+ def cache_configuration
37
+ @cache_configuration ||= DEFAULTS.clone
38
+ end
39
+
40
+ def redis_client=(client)
41
+ cache_configuration[:redis_client] = client
42
+ end
43
+
44
+ def redis_url=(url)
45
+ cache_configuration[:redis_url] = url
46
+ cache_configuration[:redis_client] = ::Redis.new(url: url)
47
+ end
48
+
49
+ def redis_client
50
+ cache_configuration[:redis_client].is_a?(Proc) ? cache_configuration[:redis_client].call : cache_configuration[:redis_client]
51
+ end
52
+
53
+ def cached_keys
54
+ max_length = Time.now.to_i + cache_configuration[:ttl].to_i + 10
55
+ redis_client.zrangebyscore(cache_namespace, Time.now.to_i, "+inf")
56
+ end
57
+
58
+ def cached_items(key_includes: nil)
59
+ clear_unscored_items! if rand > cache_configuration[:stochastic_cache_bust]
60
+
61
+ cached_keys.map do |key|
62
+ if key_includes
63
+ next unless key.include?(key_includes)
64
+ end
65
+
66
+ serialized_string = redis_client.get(key)
67
+ Marshal.load(serialized_string)
68
+ end
69
+ end
70
+
71
+ def clear_cache!
72
+ redis_client.unlink(*cached_keys) if cached_keys.length > 0
73
+ redis_client.unlink(cache_namespace)
74
+ end
75
+
76
+ def clear_unscored_items!
77
+ redis_client.zremrangebyscore(cache_namespace, "-inf", Time.now.to_i)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,36 @@
1
+ module JsonSchematize
2
+ module Cache
3
+ module InstanceMethods
4
+ def initialize(stringified_params = nil, cache_key: nil, skip_cache_update: false, raise_on_error: true, **params)
5
+ super(stringified_params, raise_on_error: raise_on_error, **params)
6
+
7
+ @__incoming_cache_key__ = cache_key
8
+ if @values_assigned
9
+ __update_cache_item__(with_delete: false) unless skip_cache_update
10
+ end
11
+ end
12
+
13
+ def __update_cache_item__(with_delete: true)
14
+ __clear_entry__! if with_delete # needs to get done first in the event the cache_key changes
15
+ client = self.class.redis_client
16
+ ttl = self.class.cache_configuration[:ttl].to_i
17
+ score = Time.now.to_i + ttl
18
+ client.zadd(__cache_namespace__, score, __cache_key__)
19
+ client.set(__cache_key__, Marshal.dump(self), ex: ttl)
20
+ end
21
+
22
+ def __clear_entry__!
23
+ self.class.redis_client.zrem(__cache_namespace__, __cache_key__)
24
+ self.class.redis_client.unlink(__cache_key__)
25
+ end
26
+
27
+ def __cache_key__
28
+ "#{__cache_namespace__}:#{self.class.cache_configuration[:key].call(self, @__incoming_cache_key__)}"
29
+ end
30
+
31
+ def __cache_namespace__
32
+ self.class.cache_namespace
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json_schematize/cache/instance_methods"
4
+ require "json_schematize/cache/class_methods"
5
+
6
+ module JsonSchematize
7
+ module Cache
8
+ def self.included(base)
9
+ raise StandardError, "Yikes! JsonSchematize::Cache Needs Redis to work. Include it as a gem" unless defined?(Redis)
10
+
11
+ base.include(JsonSchematize::Cache::InstanceMethods)
12
+ base.extend(JsonSchematize::Cache::ClassMethods)
13
+ end
14
+ end
15
+ end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class JsonSchematize::EmptyValue
3
+ require "json_schematize/generator"
4
+
5
+ class JsonSchematize::EmptyValue < ::JsonSchematize::Generator
4
6
  def initialize(*)
7
+ super
5
8
  end
6
9
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json_schematize/empty_value"
3
+ require "json_schematize/cache"
4
4
  require "json_schematize/field"
5
5
  require "json_schematize/introspect"
6
6
 
@@ -59,6 +59,12 @@ class JsonSchematize::Generator
59
59
  @optional_fields ||= []
60
60
  end
61
61
 
62
+ # def self.update_block(&block)
63
+ # @skip_individual_updates = true
64
+ # yield(cloned)
65
+ # @skip_individual_updates = false
66
+ # end
67
+
62
68
  def self.convenience_methods(field:)
63
69
  unless self.instance_methods.include?(:"#{field.name}=")
64
70
  define_method(:"#{field.name}=") do |value|
@@ -71,6 +77,7 @@ class JsonSchematize::Generator
71
77
  return false unless validate_value(**validate_params)
72
78
 
73
79
  instance_variable_set(:"@#{field.name}", value)
80
+ __update_cache__!
74
81
  return true
75
82
  end
76
83
  end
@@ -100,6 +107,13 @@ class JsonSchematize::Generator
100
107
 
101
108
  private
102
109
 
110
+ def __update_cache__!
111
+ return unless self.class.include?(JsonSchematize::Cache)
112
+ return unless self.class.cache_configuration[:update_on_change]
113
+
114
+ __update_cache_item__
115
+ end
116
+
103
117
  def assign_values!
104
118
  self.class.fields.each do |field|
105
119
  value = field.value_from_field(@__params)[:transformed_value]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonSchematize
4
- VERSION = "0.5.1"
4
+ VERSION = "0.6.1"
5
5
  end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json_schematize/version"
4
+
3
5
  require "json_schematize/base"
4
6
  require "json_schematize/boolean"
5
7
  require "json_schematize/generator"
8
+ require "json_schematize/empty_value"
6
9
  require "json_schematize/version"
7
10
 
8
11
  module JsonSchematize
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_schematize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Taylor
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-13 00:00:00.000000000 Z
11
+ date: 2022-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry-byebug
@@ -82,6 +82,7 @@ files:
82
82
  - Dockerfile
83
83
  - Gemfile
84
84
  - Gemfile.lock
85
+ - LICENSE
85
86
  - Makefile
86
87
  - README.md
87
88
  - bin/console
@@ -90,6 +91,9 @@ files:
90
91
  - lib/json_schematize.rb
91
92
  - lib/json_schematize/base.rb
92
93
  - lib/json_schematize/boolean.rb
94
+ - lib/json_schematize/cache.rb
95
+ - lib/json_schematize/cache/class_methods.rb
96
+ - lib/json_schematize/cache/instance_methods.rb
93
97
  - lib/json_schematize/empty_value.rb
94
98
  - lib/json_schematize/field.rb
95
99
  - lib/json_schematize/field_transformations.rb