json_schema_tools 0.0.7 → 0.0.8
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.
- data/CHANGELOG.md +4 -1
- data/README.md +12 -12
- data/json_schema_tools.gemspec +1 -0
- data/lib/schema_tools/klass_factory.rb +29 -3
- data/lib/schema_tools/modules/validations.rb +78 -0
- data/lib/schema_tools/version.rb +1 -1
- data/lib/schema_tools.rb +1 -0
- data/spec/fixtures/client.json +3 -1
- data/spec/fixtures/lead.json +2 -2
- data/spec/schema_tools/klass_factory_spec.rb +37 -1
- metadata +21 -4
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -97,7 +97,7 @@ For example in a client controller
|
|
97
97
|
|
98
98
|
## Object attributes from Schema
|
99
99
|
|
100
|
-
|
100
|
+
Add methods, defined in schema properties, to an existing class.
|
101
101
|
Very usefull if you are building a API client and don't want to manually add
|
102
102
|
methods to you local classes .. like people NOT using JSON schema
|
103
103
|
|
@@ -111,31 +111,31 @@ methods to you local classes .. like people NOT using JSON schema
|
|
111
111
|
# raw access
|
112
112
|
contact.schema_attrs
|
113
113
|
|
114
|
-
##
|
114
|
+
## Classes from Schema - KlassFactory
|
115
115
|
|
116
116
|
Use the KlassFactory to directly create classes, with all attributes from a
|
117
|
-
schema.
|
117
|
+
schema. Instead of adding attributes to an existing class like in above example.
|
118
|
+
The classes are named after each schema's [name] (in global path).
|
118
119
|
So lets assume you have a 'client.json' schema with a name attribute in it, for
|
119
120
|
the following examples:
|
120
121
|
|
121
122
|
SchemaTools::KlassFactory.build
|
122
|
-
client = Client.new
|
123
|
-
client.name = '
|
123
|
+
client = Client.new first_name: 'Heinz'
|
124
|
+
client.name = 'Schultz'
|
125
|
+
client.valid?
|
126
|
+
client.errors.full_messages
|
124
127
|
|
125
128
|
|
126
|
-
Rather like a namespace? Good idea, but the class or module must
|
129
|
+
Rather like a namespace? Good idea, but don't forget the class or module must
|
130
|
+
be defined.
|
127
131
|
|
128
|
-
module SalesKing; end
|
129
132
|
SchemaTools::KlassFactory.build namespace: SalesKing
|
130
133
|
client = SalesKing::Client.new
|
131
134
|
|
132
|
-
Add a custom schema reader
|
135
|
+
Add a custom schema reader most likely usefull in conjunction with a custom path
|
133
136
|
|
134
137
|
reader = SchemaTools::Reader.new
|
135
|
-
reader
|
136
|
-
SchemaTools::KlassFactory.build reader: reader
|
137
|
-
|
138
|
-
|
138
|
+
SchemaTools::KlassFactory.build reader: reader, path: HappyPdf::Schema.path
|
139
139
|
|
140
140
|
## Real world examples
|
141
141
|
|
data/json_schema_tools.gemspec
CHANGED
@@ -6,11 +6,24 @@ module SchemaTools
|
|
6
6
|
class << self
|
7
7
|
|
8
8
|
# Build classes from schema inside the given namespace. Uses all classes
|
9
|
-
# found in schema path
|
9
|
+
# found in schema path:
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# First set a global schema path:
|
13
|
+
# SchemaTools.schema_path = File.expand_path('../fixtures', __FILE__)
|
14
|
+
#
|
15
|
+
# Bake classes from all schema.json found
|
16
|
+
# SchemaTools::KlassFactory.build
|
17
|
+
#
|
18
|
+
# Go use it
|
19
|
+
# client = Client.new organisation: 'SalesKing'
|
20
|
+
# client.valid?
|
21
|
+
# client.errors.should be_blank
|
10
22
|
#
|
11
|
-
# @param [String|Symbol|Module] namespace of the new classes e.g. MyCustomNamespace::MySchemaClass
|
12
23
|
# @param [Hash] opts
|
13
24
|
# @options opts [SchemaTools::Reader] :reader to use instead of global one
|
25
|
+
# @options opts [SchemaTools::Reader] :path to schema files instead of global one
|
26
|
+
# @options opts [SchemaTools::Reader] :namespace of the new classes e.g. MyCustomNamespace::MySchemaClass
|
14
27
|
def build(opts={})
|
15
28
|
reader = opts[:reader] || SchemaTools::Reader
|
16
29
|
schemata = reader.read_all( opts[:path] || SchemaTools.schema_path )
|
@@ -18,14 +31,27 @@ module SchemaTools
|
|
18
31
|
if namespace.is_a?(String) || namespace.is_a?(Symbol)
|
19
32
|
namespace = "#{namespace}".constantize
|
20
33
|
end
|
21
|
-
|
34
|
+
# bake classes
|
22
35
|
schemata.each do |schema|
|
23
36
|
klass_name = schema['name'].classify
|
24
37
|
next if namespace.const_defined?(klass_name, false)
|
25
38
|
klass = namespace.const_set(klass_name, Class.new)
|
26
39
|
klass.class_eval do
|
27
40
|
include SchemaTools::Modules::Attributes
|
41
|
+
include ActiveModel::Conversion
|
42
|
+
include SchemaTools::Modules::Validations # +naming + transl + conversion
|
28
43
|
has_schema_attrs schema['name'], reader: reader
|
44
|
+
validate_with schema['name'], reader:reader
|
45
|
+
getter_names = schema['properties'].select{|name,prop| !prop['readonly'] }.keys.map { |name| name.to_sym}
|
46
|
+
attr_accessor *getter_names
|
47
|
+
|
48
|
+
def initialize(attributes = {})
|
49
|
+
attributes.each do |name, value|
|
50
|
+
send("#{name}=", value)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def persisted?; false end
|
29
55
|
end
|
30
56
|
end
|
31
57
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_model/validations'
|
3
|
+
require 'active_model/naming'
|
4
|
+
require 'active_model/translation'
|
5
|
+
require 'active_model/conversion'
|
6
|
+
module SchemaTools
|
7
|
+
module Modules
|
8
|
+
# Add schema properties to a class by including this module and defining from
|
9
|
+
# which schema to inherit attributes.
|
10
|
+
module Validations
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
include ActiveModel::Conversion
|
13
|
+
include ActiveModel::Validations
|
14
|
+
|
15
|
+
#included do
|
16
|
+
# validate_with_schema :schema_name
|
17
|
+
#end
|
18
|
+
# Runs all the validations within the specified context. Returns true if no errors are found,
|
19
|
+
# false otherwise.
|
20
|
+
#
|
21
|
+
# If the argument is false (default is +nil+), the context is set to <tt>:create</tt> if
|
22
|
+
# <tt>new_record?</tt> is true, and to <tt>:update</tt> if it is not.
|
23
|
+
#
|
24
|
+
# Validations with no <tt>:on</tt> option will run no matter the context. Validations with
|
25
|
+
# some <tt>:on</tt> option will only run in the specified context.
|
26
|
+
def valid?(context = nil)
|
27
|
+
#context ||= (new_record? ? :create : :update)
|
28
|
+
output = super(context)
|
29
|
+
errors.empty? && output
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
|
34
|
+
# @param [Symbol|String] schema name
|
35
|
+
# @param [Hash<Symbol|String>] opts
|
36
|
+
# @options opts [String] :path schema path
|
37
|
+
# @options opts [SchemaTools::Reader] :reader instance, instead of global reader/registry
|
38
|
+
def validate_with(schema, opts={})
|
39
|
+
reader = opts[:reader] || SchemaTools::Reader
|
40
|
+
schema = reader.read(schema, opts[:path])
|
41
|
+
# make getter / setter
|
42
|
+
schema[:properties].each do |key, val|
|
43
|
+
validates_length_of key, validate_length_opts(val) if val['maxLength'] || val['minLength']
|
44
|
+
validates_presence_of key if val['required'] || val['required'] == 'true'
|
45
|
+
validates_numericality_of key, validate_number_opts(val) if val['type'] == 'number'
|
46
|
+
#TODO array minItems, max unique, null, string
|
47
|
+
# format: date-time, regex color style, email,uri, ..
|
48
|
+
validates_numericality_of key, validate_number_opts(val) if val['type'] == 'number'
|
49
|
+
#end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def validate_length_opts(attr)
|
54
|
+
opts = {}
|
55
|
+
opts[:within] = attr['minLength']..attr['maxLength'] if attr['minLength'] && attr['maxLength']
|
56
|
+
opts[:maximum] = attr['maxLength'] if attr['maxLength'] && !attr['minLength']
|
57
|
+
opts[:minimum] = attr['minLength'] if attr['minLength'] && !attr['maxLength']
|
58
|
+
opts[:allow_blank] = true if !attr['required']
|
59
|
+
opts
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param [Hash<String>] attr property values
|
63
|
+
def validate_number_opts(attr)
|
64
|
+
opts = {}
|
65
|
+
opts[:allow_blank] = true
|
66
|
+
# those vals should not be set both in one property
|
67
|
+
opts[:greater_than_or_equal_to] = attr['minimum'] if attr['minimum'].present?
|
68
|
+
opts[:less_than_or_equal_to] = attr['maximum'] if attr['maximum'].present?
|
69
|
+
opts[:less_than] = attr['exclusiveMinimum'] if attr['exclusiveMinimum'].present?
|
70
|
+
opts[:greater_than] = attr['exclusiveMaximum'] if attr['exclusiveMaximum'].present?
|
71
|
+
opts
|
72
|
+
end
|
73
|
+
|
74
|
+
end # ClassMethods
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/schema_tools/version.rb
CHANGED
data/lib/schema_tools.rb
CHANGED
@@ -3,6 +3,7 @@ require 'schema_tools/version'
|
|
3
3
|
require 'schema_tools/modules/read'
|
4
4
|
require 'schema_tools/modules/hash'
|
5
5
|
require 'schema_tools/modules/attributes'
|
6
|
+
require 'schema_tools/modules/validations'
|
6
7
|
require 'schema_tools/reader'
|
7
8
|
require 'schema_tools/cleaner'
|
8
9
|
require 'schema_tools/hash'
|
data/spec/fixtures/client.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{"type":"object",
|
2
2
|
"title": "client",
|
3
3
|
"name": "client",
|
4
|
-
"description": "A client
|
4
|
+
"description": "A client in SalesKing",
|
5
5
|
"properties":{
|
6
6
|
"id":{
|
7
7
|
"description":"Unique identifier - UUID",
|
@@ -164,6 +164,8 @@
|
|
164
164
|
},
|
165
165
|
"cash_discount":{
|
166
166
|
"description": "Default cash discount for new invoices.",
|
167
|
+
"maximum": 100,
|
168
|
+
"minimum": 0,
|
167
169
|
"type":"number"
|
168
170
|
},
|
169
171
|
"due_days":{
|
data/spec/fixtures/lead.json
CHANGED
@@ -10,14 +10,14 @@
|
|
10
10
|
"maxLength": 255
|
11
11
|
},
|
12
12
|
"links_clicked":{
|
13
|
-
"description": "Timestamps of clicks.
|
13
|
+
"description": "Timestamps of clicks. Test nested array values without object reference",
|
14
14
|
"type":"array",
|
15
15
|
"properties":{
|
16
16
|
"type": "string"
|
17
17
|
}
|
18
18
|
},
|
19
19
|
"conversion":{
|
20
|
-
"description": "
|
20
|
+
"description": "Test nested object value without object reference",
|
21
21
|
"type":"object",
|
22
22
|
"properties":{
|
23
23
|
"from": {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
class TestNamespaceKlass
|
4
|
-
# for schema classes
|
4
|
+
# namepsace for schema classes
|
5
5
|
end
|
6
6
|
|
7
7
|
module TestNamespaceModule
|
@@ -28,6 +28,42 @@ describe SchemaTools::KlassFactory do
|
|
28
28
|
|
29
29
|
end
|
30
30
|
|
31
|
+
context 'with validations' do
|
32
|
+
it 'should add validations' do
|
33
|
+
SchemaTools::KlassFactory.build
|
34
|
+
client = Client.new
|
35
|
+
client.valid?
|
36
|
+
client.errors[:organisation][0].should include 'blank'
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should build with params' do
|
40
|
+
SchemaTools::KlassFactory.build
|
41
|
+
client = Client.new organisation: 'SalesKing'
|
42
|
+
client.valid?
|
43
|
+
client.errors.should be_blank
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should validate number maximum and minimum' do
|
47
|
+
SchemaTools::KlassFactory.build
|
48
|
+
client = Client.new cash_discount: 100, organisation: 'SalesKing'
|
49
|
+
client.should be_valid
|
50
|
+
#to big
|
51
|
+
client.cash_discount = 101
|
52
|
+
client.valid?
|
53
|
+
client.errors.full_messages[0].should include('less_than_or_equal_to')
|
54
|
+
# to small
|
55
|
+
client.cash_discount = -1
|
56
|
+
client.valid?
|
57
|
+
client.errors.full_messages[0].should include('greater_than_or_equal_to')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should raise with invalid params' do
|
61
|
+
SchemaTools::KlassFactory.build
|
62
|
+
expect { Client.new id: 'SalesKing' }.to raise_error NoMethodError
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
31
67
|
context 'class building with namespace' do
|
32
68
|
|
33
69
|
after :each do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_schema_tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-02-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: activemodel
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
46
62
|
- !ruby/object:Gem::Dependency
|
47
63
|
name: rake
|
48
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,6 +131,7 @@ files:
|
|
115
131
|
- lib/schema_tools/modules/attributes.rb
|
116
132
|
- lib/schema_tools/modules/hash.rb
|
117
133
|
- lib/schema_tools/modules/read.rb
|
134
|
+
- lib/schema_tools/modules/validations.rb
|
118
135
|
- lib/schema_tools/reader.rb
|
119
136
|
- lib/schema_tools/version.rb
|
120
137
|
- spec/fixtures/address.json
|
@@ -142,7 +159,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
159
|
version: '0'
|
143
160
|
segments:
|
144
161
|
- 0
|
145
|
-
hash:
|
162
|
+
hash: 4078782522717715484
|
146
163
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
164
|
none: false
|
148
165
|
requirements:
|
@@ -151,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
168
|
version: '0'
|
152
169
|
segments:
|
153
170
|
- 0
|
154
|
-
hash:
|
171
|
+
hash: 4078782522717715484
|
155
172
|
requirements: []
|
156
173
|
rubyforge_project:
|
157
174
|
rubygems_version: 1.8.24
|