May be used to render an object into a JSON output as configured by the spec.
render :json => data_inst.as_json(:spec => :name)
# OR
render :text => data_inst.to_json(:spec => :name)
May be used to load a hash generated by JSON.parse (or text to be passed into it) into a target object using the import
# Existing Instance
data_inst.from_json(text_or_hash, :spec => :name)
# Save resulting data
data_inst.save
The system may be used to generate an empty JSON schema, which lays out the key names, and JSON level data types (objects, arrays, values).
Automatically used to generate a schema for data exported to javascript.
Data.json_spec_schema(:name)
Note: In the future this feature may be expanded to provide info about value data types, size constraints, and other bits of info that may be useful in an online API editor.
The system will store all names spec at a given path. If path is not specified it will try to generate it from the collection name of the data object, or failing that the class name of the object.
This means that any specs used in objects without collection names, and without a hard-coded path will overwrite other similarly named specs.
Note, all values pass through .to_json
unless otherwise noted
json_spec :spec_name do
## Nodes
# Key-Value Nodes
node_name #=> "node_name": #{inst.node_name}
node_name :symbol #=> "node_name": #{inst.symbol)}
node_name get: proc, set: proc {|value|} #=> "node_name": #{inst.instance_eval(&proc)}
# Typed Key-Value Nodes (Exception on invalid types)
node_name type: Type, ... # Same operation as normal Key-Value nodes
# Key-Array Nodes
node_name Array #=> "node_name": [#{inst.node_name.each {|val| val}}]
node_name Array, :symbol #=> "node_name": [#{inst.node_name.each(&:symbol)}]
node_name Array, get: proc set: proc {|parsed_array|} #=> "node_name": [#{inst.instance_eval(&proc)}]
# Typed Key-Array Nodes (Exception on invalid types)
node_name Array, type: Type, ... # Same operation as normal Key-Array nodes
# Key-Object Nodes
node_name do ... end
#=> "node_name": {#{json_spec.from(&block).apply(inst.node_name)}}
# Key-Array of Object Nodes
node_name Array do ... end
#=> "node_name": [#{inst.node_name.each {|item| json_spec.from(&block).apply(item)}}]
## Operations
override! :node_name, ... # Identical functionality to above. Useful for ruby keyword names
extends! :spec_name # Extend from a different spec in the same node
extends! DataClass, :spec_name # Extend from a different spec for a different node
extends! 'collection_name', :spec_name # Sambe as above, for rails collection names
if! proc do ... end # Exted current spec if proc returns true in context of current inst
end
Raw data nodes can be named within the json_spec block
# Definition
json_spec :basic do
id
end
# Result
data_inst.to_json(spec: :basic)
#=> {"id": #{data_inst.id.to_json}}
data_inst.to_json(spec: :basic, escape: true)
#=> {\"id\": #{data_inst.id.to_json.to_json}}
# Schema Result
Data.json_spec_schema(:typed)
#=> {"id": "BasicValue"}
More fine grained control of the node value can be achieved with procs
# Definition
json_spec :basic_proc do
id get: proc {other_id}
tag :email
end
# Result
data_inst.to_json(spec: :basic_proc)
#=> {
# "id": "#{data_inst.other_id.to_json}",
# "tag": #{data_inst.email.to_json}}
Raw data nodes may specify a data type. This is used primarily for schema generation, for use with ustream editors.
However, the system will also verify the data type of the object before writing it into JSON from an export, and will attempt to parse the JSON input into this type during import.
# Definition
json_spec :typed do
id type: Integer
end
# Result
data_inst.to_json(spec: :typed)
#=> {"id": #{data_inst.id.to_json}}
# Schema Result
Data.json_spec_schema(:typed)
#=> {"id": "Integer"}
Important: If the data type does not match the generator will emit an error.
# Definition
json_spec :typed_inval do
not_a_string type: String
end
# Result
data_inst.to_json(spec: :typed_inval)
#=> Error: not_a_string fails data type contraints
The Dynamic Node generation may also supply default values
# Definition
json_spec :basic_def do
word get: proc {"hello"}
end
# Result
data_inst.to_json(spec: :basic_def)
#=> {"word": #{"hello".to_json}}
The spec system may also be used to update a data object based on an existing spec.
# Definition
json_spec :basic_set do
custom set: proc {|value| set_custom('name', value, force: true)}
end
# Result
data_inst.from_json({custom: 1}, spec: :basic_imp)
# Same effect as:
# data_inst.set_custom('name', 1, force: true)
Setter shorthands may be used to provide common functionality without using a proc.
# Definition
json_spec :extended_set do
basic set: :set_basic
complex set: :set_attribute.caller_shift(:complex)
word set: :assign.caller_push(:word)
end
# Result
data_inst.from_json({basic: 1, complex: 2, word: "hi"}, spec: :basic_imp)
# Same effect as:
# data_inst.set_basic(1)
# data_inst.set_attribute(:complex, "hi")
# data_inst.assign("hi", :word)
For typed nodes, if the typed class responds to the "from_json" signal, the value passed to the setter will be parsed through this method.
class Foo
def self.from_json(value) ... end
end
# Definition
json_spec :basic_set do
typed type: Foo, set: :set_typed
end
# Result
data_inst.from_json({typed: "raw_data"}, spec: :basic_imp)
# Same effect as:
# data_inst.set_typed(Foo.from_json("raw_data"))
Collections must be denoted as such by speficying the Array data type after the node name.
# Definition
json_spec :col do
nodes Array
end
# Result
data_inst.to_json(spec: :col) #=> {"nodes": [nodes[0].to_json, nodes[1].to_json...]}
# Schema Result
Data.json_spec_schema(:typed)
#=> {"id": [... collection_schema ...]}
If a specified value is not an array, it will be added to a single-element Array.
# Assume
data_inst.key = "asdf"
# Definition
json_spec :col_non_array do
key Array
end
# Result
data_inst.to_json(spec: :col_non_array) #=> "{"key": ["asdf".to_json]}"
Collection generation may be extended with blocks or generator functions.
# Definition
json_spec :col_block do
nodes Array, :symbol
others Array, get: proc {gen_other}
data_ids Array, get: proc{data.map(&:id)}
end
# Result
data_inst.to_json(spec: :col_block) #=> {
# "nodes": [symbol[0].to_json, symbol[1].to_json...],
# "others": [gen_other[0].to_json, gen_others[1].to_json...]}
# "data_ids": [data[0].id.to_json, data[1].id.to_json...]}
Data nodes in a collection may specify a data type. This will ensure that all members of the collection are of a given data type before generating the JSON object.
Generating a schema from a typed collection will set the element of that collection to that type name.
# Definition
json_spec :typed do
post_ids Array, type: Integer, get: proc {posts.map(&:id)}
end
# Result
#=> {"post_ids": [post1.id, post2.id...]}
# Schema Result
Data.json_spec_schema(:typed)
#=> {"post_id": ['Integer']}
The spec system does not try to impose special rules upon array imports.
The setter will execute in the context of the array parent object, and the developer will receive access to an array of raw values, which may then be parsed as necessary.
# Assume
data_inst.keys = [1, 3]
# Definition
json_spec :collection_import do
keys Array, set: proc {|values| self.keys = self.keys | values}
end
# Result
data_inst.from_json('{keys: [2, 3, 4]}', spec: :collection_import)
#=> data_inst.keys == [1, 2, 3, 4]
# Schema Result
Data.json_spec_schema(:typed)
#=> {"post_id": ['Integer']}
New objects may be embedded in an existing spec as blocks. The resulting context will determine if the data instance has a context of a given name, and will run within that context if present.
Important: Objects can not be imported. All imports must be performed on object keys.
# Assume
class Data
has_attribute :key, :real_other_key
not_an_attribute :no_key
end
# Definition
json_spec :obj do
key do
node_name_a
end
other_key get: proc {real_other_key} do
node_name_b
end
no_key do
node_name_c
end
end
# Result
data_inst.to_json(spec: :obj) #=> "{
#"key": {"node_name_a": "#{data_inst.key.node_name_a.to_json}"},
#"other_key": {"node_name_b": "#{data_inst.real_other_key.node_name_b.to_json}"},
#"no_key": {"node_name_b": "#{data_inst.node_name_c.to_json}"}
# Schema Result
Data.json_spec_schema(:typed)
#=> {"id": [... object_schema ...]}
As with norman nodes, object nodes may be placed in a collection
json_spec :obj_arr do
node_name Array do
key
end
end
# Result
data_inst.to_json(spec: :obj_arr)
#=> {"node_name": [{"key": #{node1.key.to_json}}, {"key": #{node2.key.to_json}}...]}
Occasionally you may wish to create an object key for an attribute in the current context.
# Assume
class Data
has_attribute :key_a, :node_name_a
end
# Definition
json_spec :obj_glue do
key_a get: proc {self} do
node_name_a
end
end
# Result
data_inst.to_json(spec: obj) #=> "{
#"key_a": {"node_name_a": "#{data_inst.node_name_a.to_json}"}
If another json_spec defines some or all of the behaviour needed for a node, that spec can be used as a starting basis for a new spec. The extended pec will provide provide all configuration values not otherwise overridden in the new spec.
# Definition
json_spec :obj_ovr do
default! :keep, 1
default! :replace, 2
end
json_spec :obj_ext do
extends! :obj_ovr
replace
end
# Result
data_inst.to_json(spec: :obj_ext) #=> {"keep": 1, "replace": #{data_inst.replace.to_json}}
Object extensions may also be performed of json_specs defined for different objects. This is useful when generating a complex json which requires multiple interacting models.
# Assume
class Second
has_association :first
end
# Definition
class First
json_spec :obj_ovr_path do
default! :key, 1
end
end
class Second
json_spec :obj_ext_path do
first do
extends! First, :obj_ovr_path
end
end
end
# Result
second_inst.to_json(spec: :obj_ext_path) #=> {"first": {"key": 1}}
Important: Each block may be extended only once. New extensions override the prior ones.
Sometimes a node_name will be defined in the global ruby function scope. In this situation the override method allows direct access to above features using a hard-coded function.
# Definition
json_spec :ovr do
override! :print
end
# Result
data_inst.to_json(spec: :ovr)
#=> {"print": #{data_inst.print.to_json}
Important: The node name must be a symbol, or something that may be converted to a symbol.
Occasionally some nodes may be conditionally required based on some outside criteria. For this purpose the json_spec supports operators to break directly into the execution of the spec generator.
Note: All conditionals are executed within the context of the data_inst.
# Definition
json_spec :cond do
if!(proc {some_check}) do
key
end
end
# Result
data_inst.to_json(spec: :cond)
#=> if data_inst.some_check
# {"key": #{data_inst.key.jo_json}}
#=> if not data_inst.some_check
# {}
# Schema Result
# {"key": "ConditionalBasicType"}
Redefining a node defined previously complete overrides it, even if done in the same context. If complex node behaviour is required, please break it down into a proper series of extensions.
The javascript library provides a browser level interface into the tunnel.
The context joins the communication and operational layers into one congruent system
The communication layer sends and receives data from the server.
Object sync represents a server data instance. Any changes to it can be pulled from the server, or push to the server.
Follows server provided spec. Will only allow updates to settable methods.
In the future may even support server push.
URL with reserved keys to fill in specific data. Used in conjunction with Sync Objects these form the core communication mechanism.
General data object used to export failure information from server to client.
JST based templates with contextual code.
Binds a server object type with a set of templates assigned to rendering different views into an object
Handle multiple distinct views that are part of a single operational flow