json_schema_tools 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +5 -1
- data/README.md +66 -19
- data/lib/schema_tools.rb +1 -0
- data/lib/schema_tools/klass_factory.rb +12 -8
- data/lib/schema_tools/modules/as_schema.rb +40 -0
- data/lib/schema_tools/modules/attributes.rb +7 -4
- data/lib/schema_tools/modules/hash.rb +18 -9
- data/lib/schema_tools/modules/read.rb +4 -4
- data/lib/schema_tools/version.rb +1 -1
- data/spec/fixtures/client.json +6 -194
- data/spec/schema_tools/cleaner_spec.rb +7 -1
- data/spec/schema_tools/hash_spec.rb +71 -16
- data/spec/schema_tools/klass_factory_spec.rb +2 -4
- data/spec/schema_tools/modules/as_schema_spec.rb +85 -0
- data/spec/schema_tools/modules/attributes_spec.rb +4 -0
- metadata +6 -4
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# Changelog JSON Schema Tools
|
2
2
|
|
3
3
|
|
4
|
+
2013-10
|
5
|
+
|
6
|
+
* add option to exclude_root in to_schema hash method
|
7
|
+
|
4
8
|
2013-06
|
5
9
|
|
6
10
|
* *has_schema_attr :schema=>{schema as ruby hash}* allow pass a schema as hash
|
@@ -8,10 +12,10 @@
|
|
8
12
|
* rails 4 (ActiveModel) compatibility
|
9
13
|
* Testing with different ActiveModel Versions
|
10
14
|
|
11
|
-
|
12
15
|
2013-02
|
13
16
|
|
14
17
|
* add validations for classes generated by KlassFactory
|
15
18
|
|
16
19
|
2012-12
|
20
|
+
|
17
21
|
* initial version with reader, hasher, params cleaner, attributes module
|
data/README.md
CHANGED
@@ -17,9 +17,12 @@ Hook the gem into your app
|
|
17
17
|
|
18
18
|
## Read Schema
|
19
19
|
|
20
|
-
Before the fun begins, with any of the tools, one or multiple JSON schema
|
21
|
-
must be available
|
22
|
-
|
20
|
+
Before the fun begins, with any of the tools, one or multiple JSON schema(files)
|
21
|
+
must be available. A schema is converted into a ruby hash and for convenience is
|
22
|
+
cached into a registry (global or local). So you only need to initialize them
|
23
|
+
once e.g on program start.
|
24
|
+
|
25
|
+
Provide a base path where the schema json files are located.
|
23
26
|
|
24
27
|
```ruby
|
25
28
|
SchemaTools.schema_path = '/path/to/schema-json-files'
|
@@ -64,29 +67,58 @@ reader.read :client, 'from/path'
|
|
64
67
|
reader.registry
|
65
68
|
```
|
66
69
|
|
67
|
-
## Object to Schema
|
70
|
+
## Object to JSON - from Schema
|
71
|
+
|
72
|
+
As you probably know such is done e.g in rails via object.to_json. While using
|
73
|
+
this might be simple, it has a damn big drawback: There is no transparent
|
74
|
+
contract about the data-structure, as rails simply uses all fields defined in the
|
75
|
+
database(ActiveRecord model). One side-effect: With each migration you are f***ed
|
68
76
|
|
69
|
-
A schema provides a
|
70
|
-
internal object is converted to it's schema version on delivery(API access).
|
77
|
+
A schema provides a public contract about an object definition. Therefore an
|
78
|
+
internal object is converted to it's public(schema) version on delivery(API access).
|
71
79
|
First the object is converted to a hash containing only the properties(keys)
|
72
80
|
from its schema definition. Afterwards it is a breeze to convert this hash into
|
73
81
|
JSON, with your favorite generator.
|
74
82
|
|
75
|
-
Following uses client.json schema
|
76
|
-
schema_path and adds properties to the clients_hash simply calling
|
83
|
+
Following uses client.json schema, detected from peter.class name.underscore => "client",
|
84
|
+
inside the global schema_path and adds properties to the clients_hash by simply calling
|
77
85
|
client.send('property-name'):
|
78
86
|
|
79
87
|
```ruby
|
88
|
+
class Client < ActiveRecord::Base
|
89
|
+
include SchemaTools::Modules::AsSchema
|
90
|
+
end
|
91
|
+
|
80
92
|
peter = Client.new name: 'Peter'
|
81
|
-
|
82
|
-
#=> "client"
|
93
|
+
peter.as_schema_json
|
94
|
+
#=> "client":{"id":12, "name": "Peter", "email":"",..}
|
95
|
+
|
96
|
+
peter.as_schema_hash
|
97
|
+
#=> "client"=>{"id"=>12, "name"=> "Peter", "email"=>"",..}
|
98
|
+
```
|
99
|
+
|
100
|
+
The AsSchema module is a tiny wrapper for following low level method:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
paul = Contact.new name: 'Peter'
|
104
|
+
contact_hash = SchemaTools::Hash.from_schema(paul)
|
105
|
+
#=> "contact"=>{"id"=>12, "name"=> "Peter", "email"=>"",..}
|
83
106
|
# to_json is up to you .. or your rails controller
|
84
107
|
```
|
85
108
|
|
86
|
-
### Customise
|
109
|
+
### Customise Output JSON / Hash
|
110
|
+
|
111
|
+
Following examples show options to customize the resulting json or hash. Of
|
112
|
+
course they can be combined.
|
87
113
|
|
88
114
|
Only use some fields e.g. to save bandwidth
|
89
115
|
|
116
|
+
```ruby
|
117
|
+
peter.as_schema_json(fields:['id', 'name'])
|
118
|
+
#=> "client":{"id":12, "name": "Peter"}
|
119
|
+
```
|
120
|
+
Of course the low level hash method also supports all of these options:
|
121
|
+
|
90
122
|
```ruby
|
91
123
|
client_hash = SchemaTools::Hash.from_schema(peter, fields:['id', 'name'])
|
92
124
|
#=> "client"=>{"id"=>12, "name"=> "Peter"}
|
@@ -96,15 +128,29 @@ Use a custom schema name e.g. to represent a client as contact. Assumes you also
|
|
96
128
|
have a schema named contact.json
|
97
129
|
|
98
130
|
```ruby
|
99
|
-
|
100
|
-
#=> "contact"=>{"id"=>12, "name"=> "Peter"}
|
131
|
+
peter.as_schema_json(class_name: 'contact')
|
101
132
|
```
|
102
133
|
|
103
|
-
|
134
|
+
Set a custom schema path
|
104
135
|
|
105
136
|
```ruby
|
106
|
-
|
107
|
-
|
137
|
+
peter.as_schema_json( path: 'path-to/json-files/')
|
138
|
+
```
|
139
|
+
|
140
|
+
By default the object hash has the class name (client) and the link-section on
|
141
|
+
root level. This divides the data from the available methods and makes a clear
|
142
|
+
statement about the object type(it's class).
|
143
|
+
If you don't want to traverse that one extra level you can exclude the root
|
144
|
+
and move the data one level up. See how class name and links are available
|
145
|
+
inline:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
|
149
|
+
peter.as_schema_json( exclude_root: true )
|
150
|
+
|
151
|
+
client_hash = SchemaTools::Hash.from_schema(peter, exclude_root: true)
|
152
|
+
#=> {"id"=>12, "name"=> "Peter",
|
153
|
+
# "_class_name":"client", "_links":[ .. are inlined .. ]}
|
108
154
|
```
|
109
155
|
|
110
156
|
## Parameter cleaning
|
@@ -125,7 +171,7 @@ end
|
|
125
171
|
## Object attributes from Schema
|
126
172
|
|
127
173
|
Add methods, defined in schema properties, to an existing class.
|
128
|
-
Very
|
174
|
+
Very useful if you are building a API client and don't want to manually add
|
129
175
|
methods to you local classes .. like people NOT using JSON schema
|
130
176
|
|
131
177
|
```ruby
|
@@ -138,6 +184,8 @@ contact = Client.new
|
|
138
184
|
contact.last_name = 'Rambo'
|
139
185
|
# raw access
|
140
186
|
contact.schema_attrs
|
187
|
+
# to json
|
188
|
+
contact.as_schema_json
|
141
189
|
```
|
142
190
|
|
143
191
|
## Classes from Schema - KlassFactory
|
@@ -194,8 +242,7 @@ Testing with different ActiveModel / ActiveSupport Versions:
|
|
194
242
|
RAILS_VERSION=4 rake spec
|
195
243
|
|
196
244
|
The RAILS_VERSION switch sets the version of the gems in the Gemfile and is only
|
197
|
-
|
198
|
-
|
245
|
+
useful in test env.
|
199
246
|
|
200
247
|
# Credits
|
201
248
|
|
data/lib/schema_tools.rb
CHANGED
@@ -2,6 +2,7 @@ require 'json'
|
|
2
2
|
require 'schema_tools/version'
|
3
3
|
require 'schema_tools/modules/read'
|
4
4
|
require 'schema_tools/modules/hash'
|
5
|
+
require 'schema_tools/modules/as_schema'
|
5
6
|
require 'schema_tools/modules/attributes'
|
6
7
|
require 'schema_tools/modules/validations'
|
7
8
|
require 'schema_tools/reader'
|
@@ -5,25 +5,29 @@ module SchemaTools
|
|
5
5
|
|
6
6
|
class << self
|
7
7
|
|
8
|
-
# Build classes from schema
|
9
|
-
# found in
|
8
|
+
# Build ruby classes from a schema with getter/setter methods for all
|
9
|
+
# properties and validation. Uses all classes(json files) found in global
|
10
|
+
# schema path or you can add a custom path.
|
11
|
+
# A namespace can be given under which the classes will be created.
|
10
12
|
# @example
|
11
13
|
#
|
12
|
-
# First set a
|
14
|
+
# First set a(global) schema path:
|
13
15
|
# SchemaTools.schema_path = File.expand_path('../fixtures', __FILE__)
|
14
16
|
#
|
15
|
-
#
|
17
|
+
# Build classes from all json files in schema path
|
16
18
|
# SchemaTools::KlassFactory.build
|
17
19
|
#
|
18
|
-
# Go use
|
20
|
+
# Go use them
|
19
21
|
# client = Client.new organisation: 'SalesKing'
|
20
22
|
# client.valid?
|
21
23
|
# client.errors.should be_blank
|
22
|
-
#
|
24
|
+
# With custom options:
|
25
|
+
# SchemaTools::KlassFactory.build namespace: MyModule,
|
26
|
+
# path: 'custom/path/to_json_schema'
|
23
27
|
# @param [Hash] opts
|
24
28
|
# @options opts [SchemaTools::Reader] :reader to use instead of global one
|
25
|
-
# @options opts [
|
26
|
-
# @options opts [
|
29
|
+
# @options opts [String] :path to schema files instead of global one
|
30
|
+
# @options opts [String|Symbol|Module] :namespace of the new classes e.g. MyCustomNamespace::MySchemaClass
|
27
31
|
def build(opts={})
|
28
32
|
reader = opts[:reader] || SchemaTools::Reader
|
29
33
|
schemata = reader.read_all( opts[:path] || SchemaTools.schema_path )
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
module SchemaTools
|
3
|
+
module Modules
|
4
|
+
# Extend a class so it can be rendered as json from a named schema
|
5
|
+
module AsSchema
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
# convert this class to a schema markup.
|
9
|
+
# @param [Hash{Symbol=>Mixed}] opts passed on to #SchemaTools::Hash.from_schema
|
10
|
+
# @return [String] json
|
11
|
+
def as_schema_json(opts={})
|
12
|
+
ActiveSupport::JSON.encode(as_schema_hash(opts))
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get the object as hash with fields detected from its schema.
|
16
|
+
# The schema is derived from
|
17
|
+
# * from options[:class_name]
|
18
|
+
# * has_schema_attrs definition(if used)
|
19
|
+
# * the underscored class name
|
20
|
+
# @param [Hash{Symbol=>Mixed}] opts passed on to #SchemaTools::Hash.from_schema
|
21
|
+
# @return [Hash]
|
22
|
+
def as_schema_hash(opts={})
|
23
|
+
# detect schema name from class method, else class name or opts is used.
|
24
|
+
if self.class.schema_name
|
25
|
+
opts[:class_name] ||= self.class.schema_name
|
26
|
+
end
|
27
|
+
SchemaTools::Hash.from_schema(self, opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
# Get or set the schema name used
|
32
|
+
def schema_name(name=nil)
|
33
|
+
@schema_name = name if name
|
34
|
+
@schema_name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
require 'active_support/concern'
|
2
2
|
module SchemaTools
|
3
3
|
module Modules
|
4
|
-
# Add schema properties to a class by
|
4
|
+
# Add schema properties to a class by using has_schema_attrs to define from
|
5
5
|
# which schema to inherit attributes.
|
6
6
|
module Attributes
|
7
7
|
extend ActiveSupport::Concern
|
8
|
+
include SchemaTools::Modules::AsSchema
|
8
9
|
|
9
10
|
def schema_attrs
|
10
11
|
@schema_attrs ||= {}
|
@@ -16,10 +17,12 @@ module SchemaTools
|
|
16
17
|
# @param [Hash<Symbol|String>] opts
|
17
18
|
# @options opts [String] :path schema path
|
18
19
|
# @options opts [SchemaTools::Reader] :reader instance, instead of global reader/registry
|
19
|
-
def has_schema_attrs(
|
20
|
+
def has_schema_attrs(schema_name, opts={})
|
20
21
|
reader = opts[:reader] || SchemaTools::Reader
|
21
22
|
schema_location = opts[:path] || opts[:schema]
|
22
|
-
schema = reader.read(
|
23
|
+
schema = reader.read(schema_name, schema_location)
|
24
|
+
# remember name on class level
|
25
|
+
self.schema_name(schema_name)
|
23
26
|
# make getter / setter
|
24
27
|
schema[:properties].each do |key, val|
|
25
28
|
# getter
|
@@ -36,7 +39,7 @@ module SchemaTools
|
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
39
|
-
end
|
42
|
+
end
|
40
43
|
|
41
44
|
end
|
42
45
|
end
|
@@ -24,7 +24,7 @@ module SchemaTools
|
|
24
24
|
# obj_hash = Schema.to_hash_from_schema(obj, class_name: :document)
|
25
25
|
# => { 'document' =>{'title'=>'hello world' } }
|
26
26
|
#
|
27
|
-
# @param [Object] obj
|
27
|
+
# @param [Object] obj returned as hash
|
28
28
|
# @param [Hash{Symbol=>Mixed}] opts additional options
|
29
29
|
# @options opts [String|Symbol] :class_name used as hash key. Should be
|
30
30
|
# a lowercase underscored name and it MUST have an existing schema file.
|
@@ -32,6 +32,8 @@ module SchemaTools
|
|
32
32
|
# @options opts [Array<String>] :fields to return. If not set all schema
|
33
33
|
# properties are used.
|
34
34
|
# @options opts [String] :path of the schema files overriding global one
|
35
|
+
# @options opts [Boolean] :exclude_root if set objects are not nested under
|
36
|
+
# their class name and the object hash gets _links and _class_name inline.
|
35
37
|
#
|
36
38
|
# @return [Hash{String=>{String=>Mixed}}] The object as hash:
|
37
39
|
# { 'invoice' => {'title'=>'hello world', 'number'=>'4711' } }
|
@@ -56,10 +58,17 @@ module SchemaTools
|
|
56
58
|
data[field] = obj.send(field) if obj.respond_to?(field)
|
57
59
|
end
|
58
60
|
end
|
59
|
-
|
60
|
-
#add links if present
|
61
|
+
#get links if present
|
61
62
|
links = parse_links(obj, schema)
|
62
|
-
|
63
|
+
|
64
|
+
if opts[:exclude_root]
|
65
|
+
hsh = data
|
66
|
+
hsh['_class_name'] = "#{class_name}"
|
67
|
+
links && hsh['_links'] = links
|
68
|
+
else
|
69
|
+
hsh = { "#{class_name}" => data }
|
70
|
+
links && hsh['links'] = links
|
71
|
+
end
|
63
72
|
hsh
|
64
73
|
end
|
65
74
|
|
@@ -88,11 +97,11 @@ module SchemaTools
|
|
88
97
|
if obj.respond_to?( field ) && rel_objects = obj.send( field )
|
89
98
|
rel_objects.each do |rel_obj|
|
90
99
|
res << if prop['properties'] && prop['properties']['$ref']
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
100
|
+
#got schema describing the objects
|
101
|
+
from_schema(rel_obj, opts)
|
102
|
+
else
|
103
|
+
rel_obj
|
104
|
+
end
|
96
105
|
end
|
97
106
|
end
|
98
107
|
res
|
@@ -25,8 +25,8 @@ module SchemaTools
|
|
25
25
|
|
26
26
|
# Read a schema and return it as hash. You can supply a path or the
|
27
27
|
# global path defined in #SchemaTools.schema_path is used.
|
28
|
-
# Schemata returned from cache
|
29
|
-
# round-trips. The cache can be
|
28
|
+
# Schemata are returned from cache(#registry) if present to prevent
|
29
|
+
# filesystem round-trips. The cache can be reset with #registry_reset
|
30
30
|
#
|
31
31
|
# @param [String|Symbol] schema name to be read from schema path directory
|
32
32
|
# @param [String|Hash] either the path to retrieve schema_name from,
|
@@ -36,14 +36,14 @@ module SchemaTools
|
|
36
36
|
schema_name = schema_name.to_sym
|
37
37
|
return registry[schema_name] if registry[schema_name]
|
38
38
|
|
39
|
-
if path_or_schema.is_a?
|
39
|
+
if path_or_schema.is_a?(::Hash)
|
40
40
|
path = nil
|
41
41
|
plain_data = path_or_schema.to_json
|
42
42
|
elsif path_or_schema.is_a?(::String) || path_or_schema.nil?
|
43
43
|
path = path_or_schema
|
44
44
|
file_path = File.join(path || SchemaTools.schema_path, "#{schema_name}.json")
|
45
45
|
else
|
46
|
-
raise ArgumentError,
|
46
|
+
raise ArgumentError, 'Second parameter must be a path or a schema!'
|
47
47
|
end
|
48
48
|
|
49
49
|
plain_data ||= File.open(file_path, 'r'){|f| f.read}
|
data/lib/schema_tools/version.rb
CHANGED
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 example client.",
|
5
5
|
"properties":{
|
6
6
|
"id":{
|
7
7
|
"description":"Unique identifier - UUID",
|
@@ -12,7 +12,7 @@
|
|
12
12
|
"minLength":22
|
13
13
|
},
|
14
14
|
"number":{
|
15
|
-
"description": "Unique number, auto-created
|
15
|
+
"description": "Unique number, auto-created for new client without number.",
|
16
16
|
"type":"string",
|
17
17
|
"maxLength": 50
|
18
18
|
},
|
@@ -32,54 +32,11 @@
|
|
32
32
|
"type":"string",
|
33
33
|
"maxLength": 50
|
34
34
|
},
|
35
|
-
"gender":{
|
36
|
-
"description": "Can be empty for a company. Is used in salutation",
|
37
|
-
"enum":["male", "female"],
|
38
|
-
"type":"string"
|
39
|
-
},
|
40
|
-
"notes":{
|
41
|
-
"description": "Notes for a contact. For day to day information you should use comments instead.",
|
42
|
-
"type":"string",
|
43
|
-
"format": "text"
|
44
|
-
},
|
45
|
-
"position":{
|
46
|
-
"description": "Position of a person in a company.",
|
47
|
-
"type":"string",
|
48
|
-
"maxLength": 50
|
49
|
-
},
|
50
|
-
"title":{
|
51
|
-
"description": "Academical title of a person e.g. Dr., Prof",
|
52
|
-
"type":"string",
|
53
|
-
"maxLength": 50
|
54
|
-
},
|
55
|
-
"tax_number":{
|
56
|
-
"description": "Tax number, normally applies to a private person",
|
57
|
-
"type":"string",
|
58
|
-
"maxLength": 30
|
59
|
-
},
|
60
|
-
"vat_number":{
|
61
|
-
"description": "VAT number, for a company or person paying value added taxes.",
|
62
|
-
"type":"string",
|
63
|
-
"maxLength": 30
|
64
|
-
},
|
65
35
|
"email":{
|
66
36
|
"description": "Email address of the contact.",
|
67
37
|
"type":"string",
|
68
38
|
"maxLength": 100
|
69
39
|
},
|
70
|
-
"url":{
|
71
|
-
"description": "An url associated with the person, e.g its company website.",
|
72
|
-
"type":"string",
|
73
|
-
"maxLength": 255
|
74
|
-
},
|
75
|
-
"birthday":{
|
76
|
-
"format":"date",
|
77
|
-
"type":"string"
|
78
|
-
},
|
79
|
-
"tag_list":{
|
80
|
-
"description": "Space separated list of tags. Are split and saved as Tag objects on create, update.",
|
81
|
-
"type":"string"
|
82
|
-
},
|
83
40
|
"created_at":{
|
84
41
|
"description": "Date the record was created in SK. Never changes afterwards.",
|
85
42
|
"format":"date-time",
|
@@ -92,101 +49,26 @@
|
|
92
49
|
"readonly":true,
|
93
50
|
"type":"string"
|
94
51
|
},
|
95
|
-
"language":{
|
96
|
-
"description": "Should be a valid language short-code: de-DE, fr, en-GB; like defined in your account language menu. When the client is emailed, a localized version of a multi-language template(email, pdf) will be used if available. The language will also be set for new documents.",
|
97
|
-
"type":"string",
|
98
|
-
"maxLength": 10
|
99
|
-
},
|
100
|
-
"currency":{
|
101
|
-
"description": "Currency code as defined by the ISO 4217 standard(3-letter UPCASE: EUR, USD). If set the currency is taken for new documents.",
|
102
|
-
"type":"string",
|
103
|
-
"maxLength": 3,
|
104
|
-
"minLength": 3
|
105
|
-
},
|
106
|
-
"payment_method":{
|
107
|
-
"description": "Default payment method for used for new documemts",
|
108
|
-
"enum":["cash","bank_transfer","credit_card","paypal","direct_debit","cheque", "moneybookers", "premium_sms"],
|
109
|
-
"type":"string"
|
110
|
-
},
|
111
|
-
"bank_name":{
|
112
|
-
"description": "Bank name",
|
113
|
-
"type":"string",
|
114
|
-
"maxLength": 70
|
115
|
-
},
|
116
|
-
"bank_number":{
|
117
|
-
"description": "Bank number",
|
118
|
-
"type":"string",
|
119
|
-
"maxLength": 35
|
120
|
-
},
|
121
|
-
"bank_account_number":{
|
122
|
-
"description": "Bank account number.",
|
123
|
-
"type":"string",
|
124
|
-
"maxLength": 35
|
125
|
-
},
|
126
|
-
"bank_iban":{
|
127
|
-
"description": "IBAN Number of the bank account. Is validated",
|
128
|
-
"type":"string",
|
129
|
-
"maxLength": 35
|
130
|
-
},
|
131
|
-
"bank_swift":{
|
132
|
-
"description": "SWIFT BIC- Bank Identifier Code",
|
133
|
-
"type":"string",
|
134
|
-
"maxLength": 11
|
135
|
-
},
|
136
|
-
"bank_owner":{
|
137
|
-
"description": "Bank account owner",
|
138
|
-
"type":"string",
|
139
|
-
"maxLength": 70
|
140
|
-
},
|
141
|
-
"phone_fax":{
|
142
|
-
"description": "Fax number",
|
143
|
-
"type":"string",
|
144
|
-
"maxLength": 30
|
145
|
-
},
|
146
|
-
"phone_office":{
|
147
|
-
"description": "Office phone number",
|
148
|
-
"type":"string",
|
149
|
-
"maxLength": 30
|
150
|
-
},
|
151
|
-
"phone_home":{
|
152
|
-
"description": "Private phone number",
|
153
|
-
"type":"string",
|
154
|
-
"maxLength": 30
|
155
|
-
},
|
156
52
|
"phone_mobile":{
|
157
53
|
"description": "Mobile phone number",
|
158
54
|
"type":"string",
|
159
55
|
"maxLength": 30
|
160
56
|
},
|
161
|
-
"lock_version":{
|
162
|
-
"description": "Increased on every edit, so SK can detect/prevent a concurrent edit by another user. First save wins.",
|
163
|
-
"type":"integer"
|
164
|
-
},
|
165
57
|
"cash_discount":{
|
166
58
|
"description": "Default cash discount for new invoices.",
|
167
59
|
"maximum": 100,
|
168
60
|
"minimum": 0,
|
169
61
|
"type":"number"
|
170
62
|
},
|
171
|
-
"due_days":{
|
172
|
-
"description": "Default due days for new invoices.",
|
173
|
-
"type":"integer"
|
174
|
-
},
|
175
|
-
"address_field":{
|
176
|
-
"description": "Returns the address field used on new docs. Consist of Organisation name and default(first) address",
|
177
|
-
"readonly":true,
|
178
|
-
"type":"string"
|
179
|
-
},
|
180
63
|
"addresses":{
|
181
64
|
"description": "A client can have many addresses, sorted by date descending(new first). Default address is the most recent one.",
|
182
65
|
"type":"array",
|
183
66
|
"properties" : {"$ref":"./address.json#properties"}
|
184
67
|
},
|
185
|
-
"
|
186
|
-
"description": "
|
187
|
-
"type":"
|
188
|
-
"
|
189
|
-
"minLength":22
|
68
|
+
"work_address":{
|
69
|
+
"description": "The work address as an example for a single nested/related object",
|
70
|
+
"type":"object",
|
71
|
+
"properties" : {"$ref":"./address.json#properties"}
|
190
72
|
}
|
191
73
|
},
|
192
74
|
"links":[
|
@@ -211,55 +93,6 @@
|
|
211
93
|
"description": "Wildcard search in first, last_name, organisation, email, number",
|
212
94
|
"type":"string"
|
213
95
|
},
|
214
|
-
"filter[tags]":{
|
215
|
-
"title" : "Tags",
|
216
|
-
"description": "Filter by a space delimited list of tags",
|
217
|
-
"type":"string"
|
218
|
-
},
|
219
|
-
"filter[ids]":{
|
220
|
-
"title" : "Clients",
|
221
|
-
"description": "A single or a list of client uuids, comma separated",
|
222
|
-
"type" : "string"
|
223
|
-
},
|
224
|
-
"filter[created_at_from]":{
|
225
|
-
"title" : "From date",
|
226
|
-
"description": "Objects with a creation date after the date, including given datetime. ISO 8601 format YYY-MM-DDThh:mm:ss+z",
|
227
|
-
"format" : "date-time",
|
228
|
-
"type" : "string"
|
229
|
-
},
|
230
|
-
"filter[created_at_to]":{
|
231
|
-
"title" : "To date",
|
232
|
-
"description": "Objects with a creation date before the date, including given datetime. ISO 8601 format YYY-MM-DDThh:mm:ss+z",
|
233
|
-
"format" : "date-time",
|
234
|
-
"type" : "string"
|
235
|
-
},
|
236
|
-
"filter[birthday_from]":{
|
237
|
-
"title" : "From birthday date",
|
238
|
-
"description": "Contacts with a birthday after and on the date. Leave the birthday-to date blank to only search on this day.",
|
239
|
-
"format" : "date",
|
240
|
-
"type" : "string"
|
241
|
-
},
|
242
|
-
"filter[birthday_to]":{
|
243
|
-
"title" : "To birthday date",
|
244
|
-
"description": "Contacts with a birthday date before and on the date.",
|
245
|
-
"format" : "date",
|
246
|
-
"type" : "string"
|
247
|
-
},
|
248
|
-
"filter[creator_ids]":{
|
249
|
-
"title" : "Creator",
|
250
|
-
"description": "Objects created by the given users uuids, comma separated",
|
251
|
-
"type" : "string"
|
252
|
-
},
|
253
|
-
"filter[number]":{
|
254
|
-
"title" : "By number",
|
255
|
-
"description": "Search by number where the number is matched from the start: number%",
|
256
|
-
"type" : "string"
|
257
|
-
},
|
258
|
-
"filter[languages]":{
|
259
|
-
"title" : "Languages",
|
260
|
-
"description": "A single or a list of language codes, comma separated",
|
261
|
-
"type" : "string"
|
262
|
-
},
|
263
96
|
"sort_by":{
|
264
97
|
"title" : "Sort by",
|
265
98
|
"description": "Sort the results by the given field => number",
|
@@ -293,27 +126,6 @@
|
|
293
126
|
},
|
294
127
|
{ "rel": "invoices",
|
295
128
|
"href": "clients/{id}/invoices"
|
296
|
-
},
|
297
|
-
{ "rel": "estimates",
|
298
|
-
"href": "clients/{id}/estimates"
|
299
|
-
},
|
300
|
-
{ "rel": "orders",
|
301
|
-
"href": "clients/{id}/orders"
|
302
|
-
},
|
303
|
-
{ "rel": "credit_notes",
|
304
|
-
"href": "clients/{id}/credit_notes"
|
305
|
-
},
|
306
|
-
{ "rel": "recurrings",
|
307
|
-
"href": "clients/{id}/recurrings"
|
308
|
-
},
|
309
|
-
{ "rel": "payment_reminders",
|
310
|
-
"href": "clients/{id}/payment_reminders"
|
311
|
-
},
|
312
|
-
{ "rel": "comments",
|
313
|
-
"href": "clients/{id}/comments"
|
314
|
-
},
|
315
|
-
{ "rel": "emails",
|
316
|
-
"href": "clients/{id}/emails"
|
317
129
|
}
|
318
130
|
]
|
319
131
|
}
|
@@ -6,7 +6,8 @@ describe SchemaTools::Cleaner do
|
|
6
6
|
let(:params){
|
7
7
|
{ id: 'some id',
|
8
8
|
last_name: 'Clean',
|
9
|
-
first_name: 'Paul'
|
9
|
+
first_name: 'Paul',
|
10
|
+
phone_mobile: 110
|
10
11
|
}
|
11
12
|
}
|
12
13
|
|
@@ -19,6 +20,11 @@ describe SchemaTools::Cleaner do
|
|
19
20
|
params[:last_name].should == 'Clean'
|
20
21
|
params[:id].should be_nil
|
21
22
|
end
|
23
|
+
|
24
|
+
it 'should convert values for string fields' do
|
25
|
+
SchemaTools::Cleaner.clean_params!(:client, params)
|
26
|
+
params[:phone_mobile].should == '110'
|
27
|
+
end
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
@@ -1,12 +1,34 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
################################################################################
|
4
|
+
# classes used in tests their naming is important because the respecting
|
5
|
+
# json schema is derived from it
|
6
|
+
################################################################################
|
7
|
+
class Client
|
8
|
+
attr_accessor :first_name, :id, :addresses, :work_address
|
9
|
+
end
|
10
|
+
class Address
|
11
|
+
attr_accessor :city, :zip
|
12
|
+
end
|
13
|
+
|
3
14
|
class Contact
|
4
15
|
attr_accessor :first_name, :last_name, :addresses, :id
|
5
16
|
end
|
6
17
|
|
18
|
+
# see fixtures/lead.json
|
19
|
+
class Lead < Contact
|
20
|
+
attr_accessor :links_clicked, :conversion
|
21
|
+
end
|
22
|
+
|
23
|
+
class Conversion
|
24
|
+
attr_accessor :from, :to
|
25
|
+
end
|
26
|
+
|
27
|
+
|
7
28
|
describe SchemaTools::Hash do
|
8
29
|
|
9
|
-
context 'from_schema' do
|
30
|
+
context 'from_schema to hash conversion' do
|
31
|
+
|
10
32
|
let(:contact){Contact.new}
|
11
33
|
before :each do
|
12
34
|
contact.first_name = 'Peter'
|
@@ -40,23 +62,38 @@ describe SchemaTools::Hash do
|
|
40
62
|
hash['contact']['id'].should == contact.id
|
41
63
|
hash['contact']['first_name'].should be_nil
|
42
64
|
end
|
43
|
-
end
|
44
65
|
|
45
|
-
|
46
|
-
|
47
|
-
|
66
|
+
it 'should exclude root' do
|
67
|
+
hash = SchemaTools::Hash.from_schema(contact, exclude_root: true)
|
68
|
+
hash['last_name'].should == 'Paul'
|
69
|
+
hash['_class_name'].should == 'contact'
|
48
70
|
end
|
49
|
-
|
50
|
-
|
71
|
+
|
72
|
+
it 'should have _links on object if exclude root' do
|
73
|
+
hash = SchemaTools::Hash.from_schema(contact, exclude_root: true, class_name: :client)
|
74
|
+
hash['_links'].length.should == 8
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should have _class_name on object if exclude root' do
|
78
|
+
hash = SchemaTools::Hash.from_schema(contact, exclude_root: true, class_name: :client)
|
79
|
+
hash['_class_name'].should == 'client'
|
51
80
|
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'with nested values referencing a schema' do
|
52
84
|
|
53
85
|
let(:client){Client.new}
|
54
86
|
|
55
|
-
it 'should have empty
|
87
|
+
it 'should have an empty array if values are missing' do
|
56
88
|
hash = SchemaTools::Hash.from_schema(client)
|
57
89
|
hash['client']['addresses'].should == []
|
58
90
|
end
|
59
91
|
|
92
|
+
it 'should have nil if nested object is missing' do
|
93
|
+
hash = SchemaTools::Hash.from_schema(client)
|
94
|
+
hash['client']['work_address'].should be_nil
|
95
|
+
end
|
96
|
+
|
60
97
|
it 'should have nested array values' do
|
61
98
|
a1 = Address.new
|
62
99
|
a1.city = 'Cologne'
|
@@ -66,19 +103,37 @@ describe SchemaTools::Hash do
|
|
66
103
|
hash['client']['addresses'].should == [{"address"=>{"city"=>"Cologne", "zip"=>50733}}]
|
67
104
|
end
|
68
105
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
106
|
+
it 'should have nested array values without root' do
|
107
|
+
a1 = Address.new
|
108
|
+
a1.city = 'Cologne'
|
109
|
+
a1.zip = 50733
|
110
|
+
client.addresses = [a1]
|
111
|
+
hash = SchemaTools::Hash.from_schema(client, exclude_root: true)
|
112
|
+
hash['addresses'].should == [{"city"=>"Cologne", "zip"=>50733, "_class_name"=>"address"}]
|
113
|
+
end
|
73
114
|
|
74
|
-
|
75
|
-
|
115
|
+
it 'should have nested object value' do
|
116
|
+
a1 = Address.new
|
117
|
+
a1.city = 'Cologne'
|
118
|
+
a1.zip = 50733
|
119
|
+
client.work_address = a1
|
120
|
+
hash = SchemaTools::Hash.from_schema(client)
|
121
|
+
hash['client']['work_address'].should == {"address"=>{"city"=>"Cologne", "zip"=>50733}}
|
76
122
|
end
|
77
123
|
|
78
|
-
|
79
|
-
|
124
|
+
it 'should have nested object value without root' do
|
125
|
+
a1 = Address.new
|
126
|
+
a1.city = 'Cologne'
|
127
|
+
a1.zip = 50733
|
128
|
+
client.work_address = a1
|
129
|
+
hash = SchemaTools::Hash.from_schema(client, exclude_root: true)
|
130
|
+
hash['work_address'].should == {"city"=>"Cologne", "zip"=>50733, "_class_name"=>"address"}
|
80
131
|
end
|
81
132
|
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'with plain nested values' do
|
136
|
+
|
82
137
|
let(:lead){Lead.new}
|
83
138
|
before :each do
|
84
139
|
lead.links_clicked = ['2012-12-12', '2012-12-15', '2012-12-16']
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ClassWithSchemaAttrs
|
4
|
+
include SchemaTools::Modules::Attributes
|
5
|
+
has_schema_attrs :client
|
6
|
+
end
|
7
|
+
|
8
|
+
class ClassWithSchemaName
|
9
|
+
include SchemaTools::Modules::AsSchema
|
10
|
+
schema_name :lead
|
11
|
+
end
|
12
|
+
|
13
|
+
# namespaced to not interfere with classes used in other tests
|
14
|
+
module Test
|
15
|
+
class Address
|
16
|
+
include SchemaTools::Modules::AsSchema
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
describe SchemaTools::Modules::AsSchema do
|
22
|
+
|
23
|
+
describe 'included' do
|
24
|
+
subject { ClassWithSchemaAttrs.new }
|
25
|
+
|
26
|
+
it 'should add as_schema_hash method' do
|
27
|
+
subject.should respond_to(:as_schema_hash)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should add as_schema_json method' do
|
31
|
+
subject.should respond_to(:as_schema_json)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should return hash' do
|
35
|
+
subject.last_name = 'Hogan'
|
36
|
+
hsh = subject.as_schema_hash
|
37
|
+
hsh['client']['last_name'].should == 'Hogan'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should return json' do
|
41
|
+
subject.last_name = 'Hogan'
|
42
|
+
json_str = subject.as_schema_json
|
43
|
+
hsh = ActiveSupport::JSON.decode(json_str)
|
44
|
+
hsh['client']['last_name'].should == 'Hogan'
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'schema name detection' do
|
50
|
+
|
51
|
+
it 'should use name from has_schema_attrs' do
|
52
|
+
ClassWithSchemaAttrs.new.as_schema_hash.keys.should include('client', 'links')
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should use schema_name defined in class' do
|
56
|
+
ClassWithSchemaName.new.as_schema_hash.keys.should include('lead')
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should use class name ' do
|
60
|
+
Test::Address.new.as_schema_hash.keys.should include('address')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'schema options' do
|
65
|
+
subject { ClassWithSchemaAttrs.new }
|
66
|
+
|
67
|
+
it 'should override schema name from' do
|
68
|
+
subject.as_schema_hash(class_name:'contact').keys.should include('contact')
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should use fields' do
|
72
|
+
subject.as_schema_hash(fields:['id'])['client'].keys.should == ['id']
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'should exclude root' do
|
76
|
+
subject.as_schema_hash(exclude_root: true).keys.should include('id')
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should use class name ' do
|
80
|
+
Test::Address.new.as_schema_hash.keys.should include('address')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
@@ -27,6 +27,10 @@ describe SchemaTools::Modules::Attributes do
|
|
27
27
|
subject.should_not respond_to('id=')
|
28
28
|
subject.should_not respond_to('created_at=')
|
29
29
|
end
|
30
|
+
|
31
|
+
it 'should add schema_name to class' do
|
32
|
+
subject.class.schema_name.should == :client
|
33
|
+
end
|
30
34
|
end
|
31
35
|
|
32
36
|
context 'attributes from dynamic schema' 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.
|
4
|
+
version: 0.2.0
|
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-10-
|
12
|
+
date: 2013-10-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
@@ -96,6 +96,7 @@ files:
|
|
96
96
|
- lib/schema_tools/cleaner.rb
|
97
97
|
- lib/schema_tools/hash.rb
|
98
98
|
- lib/schema_tools/klass_factory.rb
|
99
|
+
- lib/schema_tools/modules/as_schema.rb
|
99
100
|
- lib/schema_tools/modules/attributes.rb
|
100
101
|
- lib/schema_tools/modules/hash.rb
|
101
102
|
- lib/schema_tools/modules/read.rb
|
@@ -110,6 +111,7 @@ files:
|
|
110
111
|
- spec/schema_tools/cleaner_spec.rb
|
111
112
|
- spec/schema_tools/hash_spec.rb
|
112
113
|
- spec/schema_tools/klass_factory_spec.rb
|
114
|
+
- spec/schema_tools/modules/as_schema_spec.rb
|
113
115
|
- spec/schema_tools/modules/attributes_spec.rb
|
114
116
|
- spec/schema_tools/reader_spec.rb
|
115
117
|
- spec/spec_helper.rb
|
@@ -128,7 +130,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
130
|
version: '0'
|
129
131
|
segments:
|
130
132
|
- 0
|
131
|
-
hash:
|
133
|
+
hash: -1056530391654598731
|
132
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
135
|
none: false
|
134
136
|
requirements:
|
@@ -137,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
139
|
version: '0'
|
138
140
|
segments:
|
139
141
|
- 0
|
140
|
-
hash:
|
142
|
+
hash: -1056530391654598731
|
141
143
|
requirements: []
|
142
144
|
rubyforge_project:
|
143
145
|
rubygems_version: 1.8.24
|