katachi 0.0.0.1 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad0165ffcbc6b7a1e90e916df99564cf28dafdf658951145744a946612309fa4
4
- data.tar.gz: 161a85a62de47928e8f4ac5c83b2dfdf583c077c43817c87c7e648b7ca4c3c33
3
+ metadata.gz: eae61d46a9a766e14abce3b55525a693129e315439ed4984312b081ebf188ec7
4
+ data.tar.gz: fddee880cf7f3aeb4911b47b5143242dd5f38e1fd0308498b1e745f032b71898
5
5
  SHA512:
6
- metadata.gz: 45d3934a9606aaf37f90b4d3253f5c4ec6d9a8023bfd24db1307b71ed4781a7272c8e412f901015df1a6a0dcf8637b94e78dae5b53f75b4288b55dd9508353ac
7
- data.tar.gz: a4fd2cc5b74d36a8da5e882afba202bc37ceec3a801ec190085b8e7912dfe84685b5b33d6ab18b7b484ff8f0c37e4a3a6a12f9154fdf1ed1174c8517c287cdb2
6
+ metadata.gz: 12e516977d0ad4fbb33b52920737b76bfc5fd218ce934fdd78691e16d38cd90ce62247fa89b6adc226bd0ae04cc6ece47c651c2ebd5c93e3fe0bef980fea2b9e
7
+ data.tar.gz: 76d6350536445901723c42df05b5cb2ed7bb15f781ee16896b03d88bc6cdae55a9e01bbf91a204776d820e4a3e9472eb7b0eaf5f88c34b3052ecdf7cd1fd25b1
data/.rubocop.yml CHANGED
@@ -1,8 +1,42 @@
1
+ plugins:
2
+ - rubocop-performance
3
+ - rubocop-rake
4
+ - rubocop-rspec
5
+
1
6
  AllCops:
2
- TargetRubyVersion: 3.1
7
+ TargetRubyVersion: 3.2
8
+ NewCops: enable
9
+
10
+ Layout/ClassStructure:
11
+ Enabled: true
12
+
13
+ Metrics/MethodLength:
14
+ CountAsOne: &count_as_one
15
+ - array
16
+ - hash
17
+ - heredoc
18
+ - method_call
19
+
20
+ RSpec/ExampleLength:
21
+ CountAsOne: *count_as_one
22
+
23
+ Style/ClassAndModuleChildren:
24
+ EnforcedStyle: compact
25
+
26
+ Style/EndlessMethod:
27
+ EnforcedStyle: require_single_line
28
+
29
+ Style/HashSyntax:
30
+ EnforcedShorthandSyntax: always
3
31
 
4
32
  Style/StringLiterals:
5
33
  EnforcedStyle: double_quotes
6
34
 
7
35
  Style/StringLiteralsInInterpolation:
8
36
  EnforcedStyle: double_quotes
37
+
38
+ Style/TrailingCommaInHashLiteral:
39
+ EnforcedStyleForMultiline: diff_comma
40
+
41
+ Style/TrailingCommaInArguments:
42
+ EnforcedStyleForMultiline: consistent_comma
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.1.6 3.2.4 3.3.3
1
+ ruby 3.2.7 3.3.7 3.4.2
data/Guardfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # More info at https://github.com/guard/guard#readme
4
+ guard :rspec, cmd: "bundle exec rspec" do
5
+ require "guard/rspec/dsl"
6
+ dsl = Guard::RSpec::Dsl.new(self)
7
+
8
+ # RSpec files
9
+ rspec = dsl.rspec
10
+ watch(rspec.spec_helper) { rspec.spec_dir }
11
+ watch(rspec.spec_support) { rspec.spec_dir }
12
+ watch(rspec.spec_files)
13
+
14
+ # Ruby files
15
+ ruby = dsl.ruby
16
+ dsl.watch_spec_files_for(ruby.lib_files)
17
+ end
data/README.md CHANGED
@@ -1,52 +1,320 @@
1
- # katachi
1
+ ![Gem Version](https://img.shields.io/gem/v/katachi)
2
2
 
3
- An RSpec plugin for testing APIs
3
+ # Katachi
4
4
 
5
- TODO: Delete this and the text below, and describe your gem
5
+ A tool for describing and validating objects as intuitively as possible.
6
6
 
7
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/katachi`. To experiment with that code, run `bin/console` for an interactive prompt.
7
+ ```ruby
8
+ Katachi.compare(
9
+ value: {name: 'John', age: 30},
10
+ shape: {name: String, age: Integer}
11
+ ).match? # => true
12
+ ```
8
13
 
9
- ## Installation
14
+ ## What's with the name?
10
15
 
11
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
16
+ > The word “katachi” is a composite of “kata” (pattern) and “chi” (magical power), thus it includes meanings such as “complete form” or “form telling an attractive story.” It can reveal the relationship between shape, function and meaning.
17
+ >
18
+ > https://symmetry-us.com/about_the_site/what-is-katachi/
12
19
 
13
- Install the gem and add to the application's Gemfile by executing:
20
+ This tool is all about defining the shape of your data. The usual words of schema, definition, or validator all felt too formal. Since Ruby originated in Japan, I looked up the Japanese word for shape. It came back as 形 (katachi), and the above quote was the first thing I saw when checking for prior usage. It felt like a perfect fit.
14
21
 
15
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
22
+ ## Features
16
23
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
24
+ ### Basic Shape Matching
18
25
 
19
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
26
+ A comparison system built on the power of the Ruby `===` operator.
20
27
 
21
- ## Usage
28
+ ```ruby
29
+ Kt = Katachi
30
+ Kt.compare(value: 'hello', shape: 'hello').match? # => true
31
+ Kt.compare(value: 'hello', shape: 'world').match? # => false
32
+ Kt.compare(value: 'hello', shape: String).match? # => true
33
+ Kt.compare(value: 'hello', shape: /ell/).match? # => true
34
+ Kt.compare(value: 4, shape: 1..10).match? # => true
35
+ Kt.compare(value: 4, shape: ->(v) { v > 3 }).match? # => true
36
+ ```
22
37
 
23
- TODO: Write usage instructions here
38
+ If you're dealing with more variable data, there's`any_of` to allow multiple types.
39
+ This is especially useful for optional values, since we treat `nil` just like any other value.
24
40
 
25
- ## Development
41
+ ```ruby
42
+ value = user.preferred_name
43
+ shape = Kt.any_of(String, nil)
44
+ Kt.compare(value:, shape:).match? # => true
45
+ ```
26
46
 
27
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
47
+ ### An Easy-To-Use Shape Library
28
48
 
29
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
49
+ We provide some common shapes that can be accessed by `:${name}`.
30
50
 
31
- ## Contributing
51
+ ```ruby
52
+ Kt.compare(
53
+ value: "123e4567-e89b-12d3-a456-426614174000",
54
+ shape: :$uuid
55
+ ).match? # => true
56
+ ```
32
57
 
33
- Bug reports and pull requests are welcome on GitHub at https://github.com/jtannas/katachi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/jtannas/katachi/blob/main/CODE_OF_CONDUCT.md).
58
+ You can also add your own shapes to fit your needs.
34
59
 
35
- ## License
60
+ ```ruby
61
+ Kt.add_shape(:$even, ->(v) { v.even? })
62
+ Kt.compare(value: 4, shape: :$even).match? # => true
63
+ ```
36
64
 
37
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
65
+ The full list of included shapes can be found in the [predefined_shapes.rb](./lib/katachi/predefined_shapes.rb) file.
66
+ If you think there's a shape everyone should have, feel free to open an issue! Or better yet, a PR!
67
+
68
+ ### Array Comparison
69
+
70
+ Arrays are checked to ensure their contents also match the shape.
71
+
72
+ ```ruby
73
+ Kt.compare(value: [1], shape: [Integer]).match? # => true
74
+ ```
75
+
76
+ Since arrays aren't usually a fixed length, we don't compare the length
77
+ of the value and shape arrays. Instead, we treat the contents of the shape
78
+ array like `any_of`.
79
+ `[String, Integer]` is effectively shorthand for `[Kt.any_of(String, Integer)]`.
80
+
81
+ ```ruby
82
+ # pseudo-code for how arrays are compared
83
+ array_matches = value.all? do |element|
84
+ shape.any? do |shape_element|
85
+ Kt.compare(value: element, shape: shape_element).match?
86
+ end
87
+ end
88
+ ```
89
+
90
+ Seeing a few examples is probably the best way to understand how this works.
91
+
92
+ ```ruby
93
+ Kt.compare(value: [1, 2, 3, 4, 5], shape: [Integer]).match? # => true
94
+ Kt.compare(value: ['a', 'b', 'c'], shape: [Integer]).match? # => false
95
+ Kt.compare(value: [1, 2, 'c'], shape: [Integer]).match? # => false
96
+ Kt.compare(value: ['a', 2, 'c', 4], shape: [Integer, String]).match? # => true
97
+ ```
98
+
99
+ We said arrays aren't _usually_ a fixed length but it does happen.
100
+
101
+ For this situation, the Ruby `in` operator is your friend.
102
+
103
+ Here's how you can check for an array of exactly 5 elements without a lot of typing.
104
+
105
+ ```ruby
106
+ value = [1, 2, 3, 4, 5]
107
+ shape = ->(v) { v in ([Integer] * 5) }
108
+ Kt.compare(value:, shape:).match? # => true
109
+ ```
110
+
111
+ It also works for when you want to check for specific values at specific indexes.
112
+
113
+ ```ruby
114
+ value = [1, 'a', 2]
115
+ shape = ->(v) { v in [Integer, String, Integer] }
116
+ Kt.compare(value:, shape:).match? # => true
117
+ ```
118
+
119
+ Checks are recursive, so you can nest arrays as deep as you like.
120
+
121
+ ```ruby
122
+ value = [1, [2, [3, 4]]]
123
+ shape = [Integer, [Integer, [Integer]]]
124
+ Kt.compare(value:, shape:).match? # => true
125
+ ```
126
+
127
+ ### Hash Comparison
128
+
129
+ Hashes are checked to ensure their keys and values match the shape.
130
+
131
+ ```ruby
132
+ value = {a: 1}
133
+ shape = {a: Integer}
134
+ Kt.compare(value:, shape:).match? # => true
135
+ ```
136
+
137
+ By default, no extra or missing hash keys are allowed.
138
+
139
+ ```ruby
140
+ # This will fail because `:b` is not in the shape
141
+ value = {a: 1, b: 2}
142
+ shape = {a: Integer}
143
+ Kt.compare(value:, shape:).match? # => false
144
+
145
+ # This will fail because `:b` is missing from the value
146
+ value = {a: 1}
147
+ shape = {a: Integer, b: String}
148
+ Kt.compare(value:, shape:).match? # => false
149
+ ```
150
+
151
+ If you want to allow extra keys, no special syntax is needed.
152
+ Ruby comes to the rescue!
153
+ Ruby accepts more than just strings and symbols as hash keys.
154
+ We take advantage of this by applying the same comparison logic to the keys as we do to the values.
155
+
156
+ ```ruby
157
+ value = {a: 1, b: 2, c: 3}
158
+ shape = {a: Integer, Symbol => Integer}
159
+ Kt.compare(value:, shape:).match? # => true
160
+ ```
161
+
162
+ This means you can use any shape you like for the keys, though it's usually best to stick to simple shapes.
163
+
164
+ ```ruby
165
+ value = { "123e4567-e89b-12d3-a456-426614174000" => "My Id" }
166
+ shape = { :$uuid => String}
167
+ Kt.compare(value:, shape:).match? # => true
168
+ ```
169
+
170
+ We've made sure that if you go through the trouble of describing an exact key, it will override more generic matches.
171
+ We consider an exact key to be one that doesn't contain a Class, a Range, a Proc, or a Regexp.
172
+
173
+ ```ruby
174
+ value = {a: 'a', b: 'b', c: 'c'}
175
+ shape = {a: 'foo', Symbol => String}
176
+ Kt.compare(value:, shape:).match? # => false
177
+ ```
178
+
179
+ For making keys optional, we provide a special `:$undefined` shape.
180
+
181
+ ```ruby
182
+ value = {a: 1}
183
+ shape = {a: Integer, b: Kt.any_of(Integer, :$undefined)}
184
+ Kt.compare(value:, shape:).match? # => true
185
+ ```
38
186
 
39
- ## Code of Conduct
187
+ As with arrays, hashes can be nested as deep as you like.
40
188
 
41
- Everyone interacting in the Katachi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jtannas/katachi/blob/main/CODE_OF_CONDUCT.md).
189
+ ```ruby
190
+ value = {a: {b: {c: 1}}}
191
+ shape = {a: {b: {c: Integer}}}
192
+ Kt.compare(value:, shape:).match? # => true
193
+ ```
42
194
 
43
- ## Versioning
195
+ ### Custom Comparisons
44
196
 
45
- This gem uses [Epoch Semantic Versioning](https://antfu.me/posts/epoch-semver).
197
+ Need something more complex? Just add a `kt_compare` class method to whatever you'd like to compare.
198
+ As long as it returns a `Katachi::Result`, you're good to go!
46
199
 
47
- The format is: `EPOCH.MAJOR.MINOR.PATCH`
200
+ ```ruby
201
+ class CanRideThisRollerCoaster
202
+ def self.kt_compare(value:)
203
+ age_check = Kt.compare(value: value.age, shape: 14..)
204
+ height_check = Kt.compare(value: value.height, shape: 42..123)
205
+ has_parent_check = Kt.compare(value: value.has_parent, shape: true)
206
+ is_allowed = height_check.match? && (age_check.match? || has_parent_check.match?)
207
+ Kt::Result.new(
208
+ value:,
209
+ shape: self,
210
+ code: is_allowed ? :match : :mismatch,
211
+ child_results: {age_check:, height_check:, has_parent_check:}
212
+ )
213
+ end
214
+ end
215
+ ```
48
216
 
49
- - EPOCH: Increment when you make significant or groundbreaking changes.
50
- - MAJOR: Increment when you make minor incompatible API changes.
51
- - MINOR: Increment when you add functionality in a backwards-compatible manner.
52
- - PATCH: Increment when you make backwards-compatible bug fixes.
217
+ ### RSpec Integration
218
+
219
+ When using Rspec, the way it turns question mark methods in to `be_` methods is a perfect fit for our `match?` method.
220
+
221
+ ```ruby
222
+ # The following two lines are equivalent
223
+ expect(Kt.compare('abc', 'abc').match?).to be true
224
+ expect(Kt.compare('abc', 'abc')).to be_match
225
+ ```
226
+
227
+ For when you don't want a match, RSpec has a helpful utility for defining the opposite of a matcher.
228
+
229
+ ```ruby
230
+ RSpec::Matchers.define_negated_matcher :be_mismatch, :be_match
231
+ expect(Kt.compare('abc', 123)).to be_mismatch
232
+ ```
233
+
234
+ We've also added RSpec matchers to make testing your shapes even easier.
235
+
236
+ ```ruby
237
+ require 'katachi/rspec'
238
+
239
+ expect(Kt.compare('abc', 123)).to have_compare_code(:mismatch)
240
+ expect('abc').to have_shape(String)
241
+ expect('abc').to have_shape('abc').with_code(:exact_match)
242
+ ```
243
+
244
+ ### Detailed Diagnostics
245
+
246
+ All comparisons return a `Katachi::Result` object that contains detailed information about the comparison.
247
+
248
+ ```ruby
249
+ value = {a: 1, foo: :bar}
250
+ shape = { a: Integer, foo: String }
251
+ result = Kt.compare(value:, shape:)
252
+ result.match? # => false
253
+ result.code # => :hash_is_mismatch
254
+ result.child_results # contains the recursive results of interior comparisons
255
+ result.to_s == <<~RESULT.chomp
256
+ :hash_is_mismatch <-- compare(value: {a: 1, foo: :bar}, shape: {a: Integer, foo: String})
257
+ :hash_has_no_missing_keys <-- compare(value: {a: 1, foo: :bar}, shape: {a: Integer, foo: String}); child_label: :$required_keys
258
+ :hash_key_exact_match <-- compare(value: :a, shape: :a); child_label: :a
259
+ :hash_key_exact_match <-- compare(value: :foo, shape: :foo); child_label: :foo
260
+ :hash_has_no_extra_keys <-- compare(value: {a: 1, foo: :bar}, shape: {a: Integer, foo: String}); child_label: :$extra_keys
261
+ :hash_key_exactly_allowed <-- compare(value: :a, shape: :a); child_label: :a
262
+ :hash_key_exactly_allowed <-- compare(value: :foo, shape: :foo); child_label: :foo
263
+ :hash_values_are_mismatch <-- compare(value: {a: 1, foo: :bar}, shape: {a: Integer, foo: String}); child_label: :$values
264
+ :kv_specific_match <-- compare(value: {a: 1}, shape: {a: Integer}); child_label: [:a, 1]
265
+ :match <-- compare(value: 1, shape: Integer); child_label: Integer
266
+ :kv_specific_mismatch <-- compare(value: {foo: :bar}, shape: {foo: String}); child_label: [:foo, :bar]
267
+ :mismatch <-- compare(value: :bar, shape: String); child_label: String
268
+ RESULT
269
+ ```
270
+
271
+ ## Future Features Under Consideration
272
+
273
+ - [ ] More shapes (e.g. `:$email`, `:$url`, `:$iso_8601`)
274
+ - [ ] More "matching modifiers" (e.g. `all_of`, `one_of`, `none_of`)
275
+ - [ ] Docusaurus github pages for documentation
276
+ - [ ] More output formats (e.g. `to_json`, `to_hash`, etc...)
277
+ - [ ] Custom shape codes (e.g. `:email_is_invalid`)
278
+ - [ ] Minitest integration
279
+ - [ ] Rails integration (e.g. `validates_shape_of`)
280
+ - [ ] Shape-to-TypeScript conversion
281
+ - [ ] Shape-to-Zod conversion
282
+ - [ ] Shape-to-OpenAPI conversion
283
+ - [ ] Recursive shape definitions (e.g. `:$user => {name: String, spouse: Kt.any_of(:$user, nil)}`)
284
+ - [ ] `katachi-rspec-api` for testing+documenting APIs in a way inspired [RSwag](https://github.com/rswag/rswag)
285
+
286
+ ## Installation
287
+
288
+ Install the gem and add to the application's Gemfile by executing:
289
+
290
+ ```bash
291
+ $ bundle add katachi
292
+ ```
293
+
294
+ If bundler is not being used to manage dependencies, install the gem by executing:
295
+
296
+ ```bash
297
+ $ gem install katachi
298
+ ```
299
+
300
+ ## Inspiration
301
+
302
+ This is inspired by my experiences testing using [RSwag](https://github.com/rswag/rswag) and from my small part in helping maintain it. I wasn't happy with how often I had to look up the OpenAPI spec to be able to follow it.
303
+
304
+ A lot of this came down to OpenAPI itself being complex and making significant changes over the years (e.g. `x-nullable: true` → `nullable: true` → `type: ["string", "null"]`). A bigger part is they're limited to valid JSON, so they have very few tools to work with.
305
+
306
+ I started wondering if I could tweak RSwag to smooth over some of these rough edges. Is there a way to make it easier to write and harder to mess up?
307
+
308
+ It started as consolidating a few helper functions together, before a bigger question hit me:
309
+
310
+ **“What if I ditched writing OpenAPI entirely?”**
311
+
312
+ Rather than drag all of their maintainers and users along with my crackpot schemes, I decided it was time to set off on a new project: `Katachi`
313
+
314
+ ## Development and Contributing
315
+
316
+ See [CONTRIBUTING.md](./docs/CONTRIBUTING.md) for information on how to contribute to Katachi.
317
+
318
+ ## License
319
+
320
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -10,3 +10,17 @@ require "rubocop/rake_task"
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[spec rubocop]
13
+
14
+ desc "Opens a console with Katachi loaded for easier experimentation"
15
+ task :console do
16
+ require "bundler/setup"
17
+ require "irb"
18
+ require "katachi"
19
+ ARGV.clear
20
+ IRB.start(__FILE__)
21
+ end
22
+
23
+ desc "All the steps necessary to get the project ready for development"
24
+ task :setup do
25
+ system "bundle install"
26
+ end
@@ -0,0 +1,19 @@
1
+ $schema: https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json
2
+ version: "0.2"
3
+ words:
4
+ - bindir
5
+ - diffable
6
+ - kata
7
+ - Katachi
8
+ - kwargs
9
+ - PEBKAC
10
+ - pipefail
11
+ - popen
12
+ - precompare
13
+ - procs
14
+ - Rakefile
15
+ - readlines
16
+ - rubocop
17
+ - rubygems
18
+ - shapeables
19
+ - Tannas
@@ -0,0 +1,35 @@
1
+ # Contributing
2
+
3
+ ## Philosophy
4
+
5
+ To guide development and explain "why was it done this way?" we have a [Philosophy](./PHILOSOPHY.md) document.
6
+ Contributions should aim to follow the principles outlined there.
7
+
8
+ ## Development
9
+
10
+ After checking out the repo, run `rake setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `rake console` for an interactive prompt that will allow you to experiment.
11
+
12
+ In general, we want the `Rakefile` to be the source of truth for all tasks. If you find yourself manually running a task more than once, consider adding it to the `Rakefile`.
13
+
14
+ We don't have a release process yet, but it's coming soon!
15
+
16
+ ## Contributing
17
+
18
+ Bug reports and pull requests are welcome. Feature requests are welcome, but please open an issue first to discuss what you would like to change.
19
+
20
+ Everyone interacting in the Katachi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](./CODE_OF_CONDUCT.md).
21
+
22
+ # Versioning
23
+
24
+ This gem uses [Epoch Semantic Versioning](https://antfu.me/posts/epoch-semver).
25
+
26
+ The format is: `EPOCH.MAJOR.MINOR.PATCH`
27
+
28
+ > - EPOCH: Increment when you make significant or groundbreaking changes.
29
+ > - MAJOR: Increment when you make minor incompatible API changes.
30
+ > - MINOR: Increment when you add functionality in a backwards-compatible manner.
31
+ > - PATCH: Increment when you make backwards-compatible bug fixes.
32
+
33
+ Until we reach EPOCH 1, we will be in a state of rapid development.
34
+ Breaking changes will still be communicated via major versions, but
35
+ they may be fairly large in scope and number.