class_from_son 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 +7 -0
- data/LICENCE.txt +21 -0
- data/README.txt +43 -0
- data/build.rb +51 -0
- data/class_from_son.gemspec +15 -0
- data/lib/class_from_SON.rb +413 -0
- data/test/Address.java +38 -0
- data/test/GeneratedFromJson.java +74 -0
- data/test/PhoneNumbers.java +20 -0
- data/test/address.rb +13 -0
- data/test/generated_from_json.rb +21 -0
- data/test/phonenumbers.rb +9 -0
- data/test/test_class_from_son.rb +73 -0
- data/test/testjson.json +28 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 53b0a1472f3977c2b06e85680263efdd69178894
|
4
|
+
data.tar.gz: 70b7f897adb5ee1b4ead08b90517eb2d1d412dad
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: da824ddb560f2d64d476b3602d6e8c24c59f62ee62a367bf2b532813877d5151fdc45daae57ccc90aea77b3e9acf04054c1218e6fa5b60a4da284ba8ae9728f8
|
7
|
+
data.tar.gz: 640658471d76f8fa525f8057d6dc7a7159a56c093f498f165d1f570f9da6c0dce11f1e9c32ac09d0b016a2dcc2fd5d6a9575c58a481a3aae17a0ac9145b2173e
|
data/LICENCE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 Richard Morrisby
|
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.txt
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
This gem will attempt to generate code of a class of an object representing the contents of a Serialised-Object-Notation (SON) string (or file). E.g. it will generate code of a object's class from some JSON.
|
2
|
+
|
3
|
+
The intention with this gem is to provide assistance with creating code that can translate JSON (or other SON) into a proper Object.
|
4
|
+
|
5
|
+
If the SON looks to have a nested object structure (e.g. a Contact object looks to hold a PhoneNumber object) then multiple classes will be generated.
|
6
|
+
|
7
|
+
Limitations :
|
8
|
+
|
9
|
+
SON : will only process JSON
|
10
|
+
Code : will only generate Ruby or Java
|
11
|
+
|
12
|
+
Usage : require the gem, then invoke as follows :
|
13
|
+
|
14
|
+
ClassFromSON.generate_from_file :ruby, a_file.json
|
15
|
+
|
16
|
+
or
|
17
|
+
|
18
|
+
ClassFromSON.generate :ruby, my_json_string, :json
|
19
|
+
|
20
|
+
|
21
|
+
Method parameter explanations :
|
22
|
+
|
23
|
+
# Will generate classes from a SON file
|
24
|
+
# Regardless of whether or not files are written, this will return an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents
|
25
|
+
#
|
26
|
+
# dest_lang is symbol
|
27
|
+
# file is filename & path
|
28
|
+
# source_lang is symbol or nil (if nil, source language will be determined from the file extension)
|
29
|
+
# make_file flag defaults to true; set to false if you do not want files to be created by this method
|
30
|
+
# force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)
|
31
|
+
def ClassFromSON.generate_from_file(dest_lang, file, source_lang, make_file = true, force_file = false)
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
# Will generate classes from a SON string
|
36
|
+
# Regardless of whether or not files are written, this will return an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents
|
37
|
+
#
|
38
|
+
# dest_lang is symbol
|
39
|
+
# file is filename & path
|
40
|
+
# source_lang is symbol
|
41
|
+
# make_file flag defaults to true; set to false if you do not want files to be created by this method
|
42
|
+
# force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)
|
43
|
+
def ClassFromSON.generate(dest_lang, source, source_lang, make_file = true, force_file = false)
|
data/build.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Simple build script
|
2
|
+
# Deletes the existing gem file (if present)
|
3
|
+
# Uninstalls the gem
|
4
|
+
# Builds the gem
|
5
|
+
# Installs the new gem
|
6
|
+
|
7
|
+
# Note : when calling system (or backticks, etc.) th enew process starts at the system default, not the current working directory.
|
8
|
+
# Therefore we need to use a syntax of : system "cd #{gem_dir} && #{i_cmd}"
|
9
|
+
|
10
|
+
# Run from this directory!
|
11
|
+
|
12
|
+
gem_dir = Dir.getwd
|
13
|
+
|
14
|
+
# Delete existing .gem files in the dir
|
15
|
+
|
16
|
+
gemfiles = Dir.entries(gem_dir).collect {|q| q if q =~ /.gem$/}.compact
|
17
|
+
|
18
|
+
gemfiles.each do |q|
|
19
|
+
File.delete q
|
20
|
+
puts "Deleted #{q}"
|
21
|
+
end
|
22
|
+
|
23
|
+
gemfiles = Dir.entries(gem_dir).collect {|q| q if q =~ /.gem$/}.compact
|
24
|
+
raise "Gem has not been deleted" unless gemfiles.size == 0
|
25
|
+
|
26
|
+
# Uninstall, build, install
|
27
|
+
gemspecs = Dir.entries(gem_dir).collect {|q| q if q =~ /.gemspec$/}.compact
|
28
|
+
|
29
|
+
raise "Did not find a .gemspec in #{gem_dir}" if gemspecs.size < 1
|
30
|
+
raise "Found more than one .gemspec in #{gem_dir}" if gemspecs.size > 1
|
31
|
+
|
32
|
+
gemspec = gemspecs[0]
|
33
|
+
|
34
|
+
gemname = File.basename(gemspec, File.extname(gemspec))
|
35
|
+
|
36
|
+
u_cmd = "gem uninstall #{gemname}"
|
37
|
+
system u_cmd
|
38
|
+
|
39
|
+
b_cmd = "gem build #{gemspec}"
|
40
|
+
system "cd #{gem_dir} && #{b_cmd}"
|
41
|
+
|
42
|
+
gemfiles = Dir.entries(gem_dir).collect {|q| q if q =~ /.gem$/}.compact
|
43
|
+
raise "Gem was not built" unless gemfiles.size == 1
|
44
|
+
|
45
|
+
gemfile = gemfiles[0]
|
46
|
+
raise "Gem file is not for the expected gem, expected a #{gemname} gem but found #{gemfile}" unless gemfile =~ /^#{gemname}/
|
47
|
+
|
48
|
+
i_cmd = "gem install #{gemfile}"
|
49
|
+
system "cd #{gem_dir} && #{i_cmd}"
|
50
|
+
|
51
|
+
puts "Gem #{gemname} built & installed"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'class_from_son'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.licenses = ['MIT']
|
5
|
+
s.summary = "Generates classes from SON (e.g. JSON)"
|
6
|
+
s.description = "This gem will attempt to generate code of a class of an object representing the contents of a Serialised-Object-Notation (SON) string (or file). E.g. it will generate code of a object's class from some JSON."
|
7
|
+
s.authors = ["Richard Morrisby"]
|
8
|
+
s.email = 'rmorrisby@gmail.com'
|
9
|
+
s.files = ["lib/class_from_SON.rb"]
|
10
|
+
s.homepage = 'https://rubygems.org/gems/class_from_son'
|
11
|
+
s.required_ruby_version = '>=1.9'
|
12
|
+
s.files = Dir['**/**']
|
13
|
+
s.test_files = Dir["test/test*.rb"]
|
14
|
+
s.has_rdoc = true
|
15
|
+
end
|
@@ -0,0 +1,413 @@
|
|
1
|
+
require "json"
|
2
|
+
require "more-ruby"
|
3
|
+
|
4
|
+
# A utility to convert an input file of string-object notation, e.g. JSON, XML, YAML, and generate code that looks like a class of your desired language
|
5
|
+
|
6
|
+
class ClassFromSON
|
7
|
+
|
8
|
+
@@target_languages = [:java, :ruby]
|
9
|
+
@@input_modes = [:json]
|
10
|
+
|
11
|
+
def error(message)
|
12
|
+
puts "ERROR : #{message}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def error_and_exit(message)
|
16
|
+
puts "ERROR : #{message}"
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.error_and_exit(message)
|
21
|
+
puts "ERROR : #{message}"
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
def warn(message)
|
26
|
+
puts "WARN : #{message}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def convert_ruby_type_to_type(type, value_types = [])
|
30
|
+
# puts "#{__method__} called with type '#{type.inspect}', #{type.class}, #{value_types.inspect}"
|
31
|
+
# Because type is an instance of Class, we need to compare names, so convert to String
|
32
|
+
# Use .to_s instead of .name to cope with custom types (i.e. types that are themselves new classes to be generated)
|
33
|
+
case type.to_s
|
34
|
+
when "Fixnum", "Integer"
|
35
|
+
converted = convert_fixnum_to_type
|
36
|
+
when "Float"
|
37
|
+
converted = convert_float_to_type
|
38
|
+
when "String"
|
39
|
+
converted = convert_string_to_type
|
40
|
+
when "TrueClass", "FalseClass"
|
41
|
+
converted = convert_boolean_to_type
|
42
|
+
when "Array"
|
43
|
+
converted = convert_array_to_type(value_types)
|
44
|
+
when "Hash"
|
45
|
+
converted = convert_hash_to_type(value_types)
|
46
|
+
when "NilClass" # default nil to String
|
47
|
+
converted = convert_string_to_type
|
48
|
+
else
|
49
|
+
converted = convert_custom_class_type(type)
|
50
|
+
end
|
51
|
+
# puts "Converted '#{type.inspect}' to #{converted}"
|
52
|
+
converted
|
53
|
+
end
|
54
|
+
|
55
|
+
# Translate "Fixnum" into the desired output language
|
56
|
+
def convert_fixnum_to_type
|
57
|
+
case @language
|
58
|
+
when :java
|
59
|
+
return "int"
|
60
|
+
else
|
61
|
+
error_and_exit "Could not convert to output language #{@language}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Translate "Fixnum" into the desired output language
|
66
|
+
def convert_float_to_type
|
67
|
+
case @language
|
68
|
+
when :java
|
69
|
+
return "float"
|
70
|
+
else
|
71
|
+
error_and_exit "Could not convert to output language #{@language}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Translate "Fixnum" into the desired output language
|
76
|
+
def convert_boolean_to_type
|
77
|
+
case @language
|
78
|
+
when :java
|
79
|
+
return "boolean"
|
80
|
+
else
|
81
|
+
error_and_exit "Could not convert to output language #{@language}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Translate "String" into the desired output language
|
86
|
+
def convert_string_to_type
|
87
|
+
case @language
|
88
|
+
when :java
|
89
|
+
return "String"
|
90
|
+
else
|
91
|
+
error_and_exit "Could not convert to output language #{@language}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Translate "Array" into the desired output language
|
96
|
+
# Also needs the 'value types', i.e. what type is this array? A list of strings? A list of ints?
|
97
|
+
def convert_array_to_type(value_types)
|
98
|
+
error_and_exit "Detected an array, but could not determine the type of its children; found #{value_types.size} possibilities" unless value_types.size == 1
|
99
|
+
case @language
|
100
|
+
when :java
|
101
|
+
return "List<#{convert_ruby_type_to_type(value_types[0])}>"
|
102
|
+
else
|
103
|
+
error_and_exit "Could not convert to output language #{@language}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Translate "Hash" into the desired output language
|
108
|
+
# Also needs the 'value types', i.e. what type is this hash? A map of strings to booleans? A map of ints to strings?
|
109
|
+
def convert_hash_to_type(value_types)
|
110
|
+
error_and_exit "Detected a hash, but could not determine the type of its keys and values; found #{value_types.size} possibilities" unless value_types.size == 2
|
111
|
+
case @language
|
112
|
+
when :java
|
113
|
+
return "HashMap<#{convert_ruby_type_to_type(value_types[0])}, #{convert_ruby_type_to_type(value_types[1])}>"
|
114
|
+
else
|
115
|
+
error_and_exit "Could not convert to output language #{@language}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns code representing the start of the class
|
120
|
+
def convert_custom_class_type(type)
|
121
|
+
case @language
|
122
|
+
when :java, :ruby
|
123
|
+
return type.capitalize_first_letter_only
|
124
|
+
else
|
125
|
+
error_and_exit "Could not convert to output language #{@language}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def generate_top_level_name
|
130
|
+
case @language
|
131
|
+
when :java
|
132
|
+
return "generatedFrom#{@mode.capitalize}"
|
133
|
+
when :ruby
|
134
|
+
return "generated_from_#{@mode}"
|
135
|
+
else
|
136
|
+
error_and_exit "Could not convert to output language #{@language}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns an appropriately-formatted classname for the given name
|
141
|
+
def generate_classname(name)
|
142
|
+
case @language
|
143
|
+
when :java, :ruby
|
144
|
+
return name.capitalize_first_letter_only
|
145
|
+
else
|
146
|
+
error_and_exit "Could not convert to output language #{@language}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns an appropriately-formatted filename for the given name
|
151
|
+
def generate_filename(name)
|
152
|
+
case @language
|
153
|
+
when :java
|
154
|
+
return name.capitalize_first_letter_only + @extension
|
155
|
+
when :ruby
|
156
|
+
return name.downcase + @extension # TODO convert camelcase to underbarised case
|
157
|
+
else
|
158
|
+
error_and_exit "Could not convert to output language #{@language}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def set_file_extension_for_language
|
163
|
+
case @language
|
164
|
+
when :java
|
165
|
+
@extension = ".java"
|
166
|
+
when :ruby
|
167
|
+
@extension = ".rb"
|
168
|
+
else
|
169
|
+
error_and_exit "Could not convert to output language #{@language}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns code representing the start of the class
|
174
|
+
def generate_class_start(name)
|
175
|
+
case @language
|
176
|
+
when :java
|
177
|
+
start = "public class #{convert_custom_class_type(name)} {"
|
178
|
+
when :ruby
|
179
|
+
start = "class #{convert_custom_class_type(name)}"
|
180
|
+
else
|
181
|
+
error_and_exit "Could not convert to output language #{@language}"
|
182
|
+
end
|
183
|
+
start
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns code representing the end of the class
|
187
|
+
def generate_class_end
|
188
|
+
case @language
|
189
|
+
when :java
|
190
|
+
class_end = "}"
|
191
|
+
when :ruby
|
192
|
+
class_end = "end"
|
193
|
+
else
|
194
|
+
error_and_exit "Could not convert to output language #{@language}"
|
195
|
+
end
|
196
|
+
class_end
|
197
|
+
end
|
198
|
+
|
199
|
+
def generate_getter_and_setter(type, name)
|
200
|
+
lines = []
|
201
|
+
case @language
|
202
|
+
when :java
|
203
|
+
class_name = convert_custom_class_type(name)
|
204
|
+
lines << "\t"
|
205
|
+
lines << "\tpublic #{type} get#{class_name}() {"
|
206
|
+
lines << "\t\treturn #{name};"
|
207
|
+
lines << "\t}"
|
208
|
+
lines << "\t"
|
209
|
+
lines << "\tpublic void set#{class_name}(#{type} #{name}) {"
|
210
|
+
lines << "\t\tthis.#{name} = #{name};"
|
211
|
+
lines << "\t}"
|
212
|
+
else
|
213
|
+
error_and_exit "Could not convert to output language #{@language}"
|
214
|
+
end
|
215
|
+
lines
|
216
|
+
end
|
217
|
+
|
218
|
+
# Returns code representing each of the supplied attributes
|
219
|
+
def generate_code_from_attributes(attributes)
|
220
|
+
case @language
|
221
|
+
when :java
|
222
|
+
return generate_java_code_from_attributes(attributes)
|
223
|
+
when :ruby
|
224
|
+
return generate_ruby_code_from_attributes(attributes)
|
225
|
+
else
|
226
|
+
error_and_exit "Could not convert to output language #{@language}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Returns Java code representing each of the supplied attributes
|
231
|
+
def generate_java_code_from_attributes(attributes)
|
232
|
+
code = []
|
233
|
+
# Instance variables
|
234
|
+
attributes.each do |att|
|
235
|
+
code << "\tpublic #{convert_ruby_type_to_type(att[:type], att[:value_types])} #{att[:name]};"
|
236
|
+
end
|
237
|
+
|
238
|
+
#TODO constructor
|
239
|
+
|
240
|
+
# Getters & setters
|
241
|
+
attributes.each do |att|
|
242
|
+
code << generate_getter_and_setter(convert_ruby_type_to_type(att[:type], att[:value_types]), att[:name])
|
243
|
+
end
|
244
|
+
code
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns Ruby code representing each of the supplied attributes
|
248
|
+
def generate_ruby_code_from_attributes(attributes)
|
249
|
+
code = []
|
250
|
+
names = []
|
251
|
+
attributes.each {|att| names << att[:name]}
|
252
|
+
|
253
|
+
# Instance variables
|
254
|
+
names.each do |name|
|
255
|
+
code << "\tattr_accessor #{name.to_sym.inspect}"
|
256
|
+
end
|
257
|
+
code << "" # An empty string is enough to trigger a newline
|
258
|
+
|
259
|
+
# Constructor
|
260
|
+
code << "\tdef initialize(#{names.join(", ")})"
|
261
|
+
names.each do |name|
|
262
|
+
code << "\t\t@#{name} = #{name}"
|
263
|
+
end
|
264
|
+
code << "\tend"
|
265
|
+
|
266
|
+
code
|
267
|
+
end
|
268
|
+
|
269
|
+
# From the supplied hash, generates code representing a class in the desired language (as a string)
|
270
|
+
# Returns an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents
|
271
|
+
def generate_output_classes(hash, top_level_classname = nil)
|
272
|
+
classname = generate_classname(top_level_classname)
|
273
|
+
filename = generate_filename(classname)
|
274
|
+
files = []
|
275
|
+
this_file = {:name => classname, :name_with_ext => filename}
|
276
|
+
lines = []
|
277
|
+
lines << generate_class_start(classname)
|
278
|
+
attributes = [] # array of hashes; keys => :name, :type, :value_types # :type, :value_types ([]) are initially kept as Ruby class names
|
279
|
+
|
280
|
+
hash.each_pair do |k, v|
|
281
|
+
attribute = {:name => k}
|
282
|
+
if v.class == Array
|
283
|
+
attribute[:type] = Array
|
284
|
+
if v[0].class == Hash
|
285
|
+
new_files = generate_output_classes(v[0], k)
|
286
|
+
attribute[:value_types] = [new_files[0][:name]]
|
287
|
+
files += new_files
|
288
|
+
else
|
289
|
+
# Array only contains primitives, not objects
|
290
|
+
attribute[:value_types] = [v[0].class]
|
291
|
+
end
|
292
|
+
elsif v.class == Hash
|
293
|
+
new_files = generate_output_classes(v, k)
|
294
|
+
attribute[:type] = new_files[0][:name]
|
295
|
+
files += new_files
|
296
|
+
else
|
297
|
+
attribute[:type] = v.class
|
298
|
+
end
|
299
|
+
attributes << attribute
|
300
|
+
end
|
301
|
+
|
302
|
+
lines << generate_code_from_attributes(attributes)
|
303
|
+
lines << generate_class_end
|
304
|
+
lines.flatten!
|
305
|
+
this_file[:contents] = lines.join("\n")
|
306
|
+
files.insert(0, this_file)
|
307
|
+
files
|
308
|
+
end
|
309
|
+
|
310
|
+
###################################################################################################
|
311
|
+
# The Generator
|
312
|
+
###################################################################################################
|
313
|
+
|
314
|
+
# Will generate classes from a SON file
|
315
|
+
# Regardless of whether or not files are written, this will return an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents
|
316
|
+
#
|
317
|
+
# dest_lang is symbol
|
318
|
+
# file is filename & path
|
319
|
+
# source_lang is symbol or nil (if nil, source language will be determined from the file extension)
|
320
|
+
# make_file flag defaults to true; set to false if you do not want files to be created by this method
|
321
|
+
# force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)
|
322
|
+
def ClassFromSON.generate_from_file(dest_lang, file, source_lang = nil, make_file = true, force_file = false)
|
323
|
+
|
324
|
+
error_and_exit "Could not locate file #{file}" unless File.exists?(file)
|
325
|
+
|
326
|
+
source_lang ||= File.extname(file).gsub(".", "")
|
327
|
+
source = File.readlines(file).join
|
328
|
+
ClassFromSON.generate(dest_lang, source, source_lang, make_file, force_file)
|
329
|
+
|
330
|
+
# o = ClassFromSON.new
|
331
|
+
# o.generate(dest_lang, source, source_lang, make_file)
|
332
|
+
end
|
333
|
+
|
334
|
+
# Will generate classes from a SON string
|
335
|
+
# Regardless of whether or not files are written, this will return an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents
|
336
|
+
#
|
337
|
+
# dest_lang is symbol
|
338
|
+
# file is filename & path
|
339
|
+
# source_lang is symbol
|
340
|
+
# make_file flag defaults to true; set to false if you do not want files to be created by this method
|
341
|
+
# force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)
|
342
|
+
def ClassFromSON.generate(dest_lang, source, source_lang, make_file = true, force_file = false)
|
343
|
+
o = ClassFromSON.new
|
344
|
+
o.generate(dest_lang, source, source_lang, make_file, force_file)
|
345
|
+
end
|
346
|
+
|
347
|
+
# Will generate classes from a SON string.
|
348
|
+
# Regardless of whether or not files are written, this will return an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents
|
349
|
+
#
|
350
|
+
# dest_lang is symbol
|
351
|
+
# file is filename & path
|
352
|
+
# source_lang is symbol
|
353
|
+
# make_file flag defaults to true; set to false if you do not want files to be created by this method
|
354
|
+
# force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)
|
355
|
+
def generate(dest_lang, source, source_lang, make_file = true, force_file = false)
|
356
|
+
|
357
|
+
error_and_exit "Please supply first argument as a Symbol" unless dest_lang.class == Symbol
|
358
|
+
|
359
|
+
@language = dest_lang
|
360
|
+
if @@target_languages.include?(@language)
|
361
|
+
# may proceed
|
362
|
+
# TODO other target languages, e.g. C#, Python
|
363
|
+
else
|
364
|
+
error_and_exit "Cannot generate language #{@language}; can only generate #{@@target_languages.join(", ")}"
|
365
|
+
end
|
366
|
+
@extension = set_file_extension_for_language
|
367
|
+
|
368
|
+
@mode = source_lang.to_sym
|
369
|
+
if @@input_modes.include?(@mode)
|
370
|
+
# may proceed
|
371
|
+
else
|
372
|
+
error_and_exit "Cannot parse input language #{@mode}; can only parse #{@@input_modes.join(", ")}"
|
373
|
+
end
|
374
|
+
|
375
|
+
case @mode
|
376
|
+
when :json
|
377
|
+
hash = JSON.parse(source)
|
378
|
+
# TODO other input languages, e.g. XML, YAML
|
379
|
+
else
|
380
|
+
error_and_exit "Cannot parse mode #{@mode}"
|
381
|
+
end
|
382
|
+
|
383
|
+
# If we have read in an array instead of a hash, then take the first element of the array
|
384
|
+
# This assumes that each element in this top-level array has the same structure, which is reasonable
|
385
|
+
if hash.class == Array && hash.size > 0
|
386
|
+
hash = hash.shift
|
387
|
+
end
|
388
|
+
|
389
|
+
error_and_exit "Input file did not have a hash / map of key-value pairs; could not parse" unless hash.class == Hash
|
390
|
+
|
391
|
+
error_and_exit "Input file hash / map was empty" if hash.empty?
|
392
|
+
|
393
|
+
top_level_classname = generate_top_level_name
|
394
|
+
output_classes = generate_output_classes(hash, top_level_classname).flatten # returns an array
|
395
|
+
|
396
|
+
if make_file
|
397
|
+
output_classes.each do |out|
|
398
|
+
name = out[:name_with_ext]
|
399
|
+
contents = out[:contents]
|
400
|
+
unless force_file
|
401
|
+
error_and_exit "Want to generate output file #{name}, but that file already exists" if File.exists?(name)
|
402
|
+
end
|
403
|
+
File.open(name, "w+") do |f|
|
404
|
+
f.puts contents
|
405
|
+
end
|
406
|
+
puts "Wrote out file #{name}"
|
407
|
+
end
|
408
|
+
|
409
|
+
puts "Please inspect generated code files and adjust names and types accordingly"
|
410
|
+
end
|
411
|
+
output_classes
|
412
|
+
end
|
413
|
+
end
|
data/test/Address.java
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
public class Address {
|
2
|
+
public String streetAddress;
|
3
|
+
public String city;
|
4
|
+
public String state;
|
5
|
+
public String postalCode;
|
6
|
+
|
7
|
+
public String getStreetAddress() {
|
8
|
+
return streetAddress;
|
9
|
+
}
|
10
|
+
|
11
|
+
public void setStreetAddress(String streetAddress) {
|
12
|
+
this.streetAddress = streetAddress;
|
13
|
+
}
|
14
|
+
|
15
|
+
public String getCity() {
|
16
|
+
return city;
|
17
|
+
}
|
18
|
+
|
19
|
+
public void setCity(String city) {
|
20
|
+
this.city = city;
|
21
|
+
}
|
22
|
+
|
23
|
+
public String getState() {
|
24
|
+
return state;
|
25
|
+
}
|
26
|
+
|
27
|
+
public void setState(String state) {
|
28
|
+
this.state = state;
|
29
|
+
}
|
30
|
+
|
31
|
+
public String getPostalCode() {
|
32
|
+
return postalCode;
|
33
|
+
}
|
34
|
+
|
35
|
+
public void setPostalCode(String postalCode) {
|
36
|
+
this.postalCode = postalCode;
|
37
|
+
}
|
38
|
+
}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
public class GeneratedFromJson {
|
2
|
+
public String firstName;
|
3
|
+
public String lastName;
|
4
|
+
public boolean isAlive;
|
5
|
+
public int age;
|
6
|
+
public Address address;
|
7
|
+
public List<PhoneNumbers> phoneNumbers;
|
8
|
+
public List<String> children;
|
9
|
+
public String spouse;
|
10
|
+
|
11
|
+
public String getFirstName() {
|
12
|
+
return firstName;
|
13
|
+
}
|
14
|
+
|
15
|
+
public void setFirstName(String firstName) {
|
16
|
+
this.firstName = firstName;
|
17
|
+
}
|
18
|
+
|
19
|
+
public String getLastName() {
|
20
|
+
return lastName;
|
21
|
+
}
|
22
|
+
|
23
|
+
public void setLastName(String lastName) {
|
24
|
+
this.lastName = lastName;
|
25
|
+
}
|
26
|
+
|
27
|
+
public boolean getIsAlive() {
|
28
|
+
return isAlive;
|
29
|
+
}
|
30
|
+
|
31
|
+
public void setIsAlive(boolean isAlive) {
|
32
|
+
this.isAlive = isAlive;
|
33
|
+
}
|
34
|
+
|
35
|
+
public int getAge() {
|
36
|
+
return age;
|
37
|
+
}
|
38
|
+
|
39
|
+
public void setAge(int age) {
|
40
|
+
this.age = age;
|
41
|
+
}
|
42
|
+
|
43
|
+
public Address getAddress() {
|
44
|
+
return address;
|
45
|
+
}
|
46
|
+
|
47
|
+
public void setAddress(Address address) {
|
48
|
+
this.address = address;
|
49
|
+
}
|
50
|
+
|
51
|
+
public List<PhoneNumbers> getPhoneNumbers() {
|
52
|
+
return phoneNumbers;
|
53
|
+
}
|
54
|
+
|
55
|
+
public void setPhoneNumbers(List<PhoneNumbers> phoneNumbers) {
|
56
|
+
this.phoneNumbers = phoneNumbers;
|
57
|
+
}
|
58
|
+
|
59
|
+
public List<String> getChildren() {
|
60
|
+
return children;
|
61
|
+
}
|
62
|
+
|
63
|
+
public void setChildren(List<String> children) {
|
64
|
+
this.children = children;
|
65
|
+
}
|
66
|
+
|
67
|
+
public String getSpouse() {
|
68
|
+
return spouse;
|
69
|
+
}
|
70
|
+
|
71
|
+
public void setSpouse(String spouse) {
|
72
|
+
this.spouse = spouse;
|
73
|
+
}
|
74
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
public class PhoneNumbers {
|
2
|
+
public String type;
|
3
|
+
public String number;
|
4
|
+
|
5
|
+
public String getType() {
|
6
|
+
return type;
|
7
|
+
}
|
8
|
+
|
9
|
+
public void setType(String type) {
|
10
|
+
this.type = type;
|
11
|
+
}
|
12
|
+
|
13
|
+
public String getNumber() {
|
14
|
+
return number;
|
15
|
+
}
|
16
|
+
|
17
|
+
public void setNumber(String number) {
|
18
|
+
this.number = number;
|
19
|
+
}
|
20
|
+
}
|
data/test/address.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class Address
|
2
|
+
attr_accessor :streetAddress
|
3
|
+
attr_accessor :city
|
4
|
+
attr_accessor :state
|
5
|
+
attr_accessor :postalCode
|
6
|
+
|
7
|
+
def initialize(streetAddress, city, state, postalCode)
|
8
|
+
@streetAddress = streetAddress
|
9
|
+
@city = city
|
10
|
+
@state = state
|
11
|
+
@postalCode = postalCode
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Generated_from_json
|
2
|
+
attr_accessor :firstName
|
3
|
+
attr_accessor :lastName
|
4
|
+
attr_accessor :isAlive
|
5
|
+
attr_accessor :age
|
6
|
+
attr_accessor :address
|
7
|
+
attr_accessor :phoneNumbers
|
8
|
+
attr_accessor :children
|
9
|
+
attr_accessor :spouse
|
10
|
+
|
11
|
+
def initialize(firstName, lastName, isAlive, age, address, phoneNumbers, children, spouse)
|
12
|
+
@firstName = firstName
|
13
|
+
@lastName = lastName
|
14
|
+
@isAlive = isAlive
|
15
|
+
@age = age
|
16
|
+
@address = address
|
17
|
+
@phoneNumbers = phoneNumbers
|
18
|
+
@children = children
|
19
|
+
@spouse = spouse
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require_relative "../lib/class_from_son"
|
3
|
+
|
4
|
+
class TestEx < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def switch_dir
|
7
|
+
# Change this to more easily find the test files
|
8
|
+
@previous_wd ||= Dir.getwd # Preserve the old wd so we can switch back to it after the test
|
9
|
+
Dir.chdir File.expand_path(File.dirname(__FILE__))
|
10
|
+
end
|
11
|
+
|
12
|
+
# Switch to the same directory as this unit-test file
|
13
|
+
def setup
|
14
|
+
switch_dir
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_class_from_json_to_ruby
|
18
|
+
json = File.readlines("testjson.json").join
|
19
|
+
|
20
|
+
address_filename = "address.rb"
|
21
|
+
top_level_filename = "generated_from_json.rb"
|
22
|
+
phonenumbers_filename = "phonenumbers.rb"
|
23
|
+
|
24
|
+
expected_address = File.readlines(address_filename).join.chomp # trim off any trailing whitespace (irrelevent for this test)
|
25
|
+
expected_example = File.readlines(top_level_filename).join.chomp # trim off any trailing whitespace (irrelevent for this test)
|
26
|
+
expected_phonenumbers = File.readlines(phonenumbers_filename).join.chomp # trim off any trailing whitespace (irrelevent for this test)
|
27
|
+
|
28
|
+
generated = nil
|
29
|
+
assert_nothing_raised("ClassFromSON.generate exited with an error") { generated = ClassFromSON.generate(:ruby, json, :json, false) }
|
30
|
+
|
31
|
+
assert_equal(3, generated.size, "Failed to correctly generate all Ruby classes from JSON")
|
32
|
+
|
33
|
+
assert_equal(top_level_filename, generated[0][:name_with_ext], "Failed to correctly generate top-level generated_from_json example filename")
|
34
|
+
assert_equal(address_filename, generated[1][:name_with_ext], "Failed to correctly generate address example filename")
|
35
|
+
assert_equal(phonenumbers_filename, generated[2][:name_with_ext], "Failed to correctly generate phonenumbers example filename")
|
36
|
+
|
37
|
+
assert_equal(expected_example, generated[0][:contents], "Failed to correctly generate top-level GeneratedFromJson example Ruby class from JSON")
|
38
|
+
assert_equal(expected_address, generated[1][:contents], "Failed to correctly generate Address example Ruby class from JSON")
|
39
|
+
assert_equal(expected_phonenumbers, generated[2][:contents], "Failed to correctly generate PhoneNumbers example Ruby class from JSON")
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def test_class_from_json_to_java
|
44
|
+
json = File.readlines("testjson.json").join
|
45
|
+
|
46
|
+
address_filename = "Address.java"
|
47
|
+
top_level_filename = "GeneratedFromJson.java"
|
48
|
+
phonenumbers_filename = "PhoneNumbers.java"
|
49
|
+
|
50
|
+
expected_address = File.readlines(address_filename).join.chomp # trim off any trailing whitespace (irrelevent for this test)
|
51
|
+
expected_example = File.readlines(top_level_filename).join.chomp # trim off any trailing whitespace (irrelevent for this test)
|
52
|
+
expected_phonenumbers = File.readlines(phonenumbers_filename).join.chomp # trim off any trailing whitespace (irrelevent for this test)
|
53
|
+
|
54
|
+
generated = nil
|
55
|
+
assert_nothing_raised("ClassFromSON.generate exited with an error") { generated = ClassFromSON.generate(:java, json, :json, false) }
|
56
|
+
|
57
|
+
assert_equal(3, generated.size, "Failed to correctly generate all Java classes from JSON")
|
58
|
+
|
59
|
+
assert_equal(top_level_filename, generated[0][:name_with_ext], "Failed to correctly generate top-level GeneratedFromJson example filename")
|
60
|
+
assert_equal(address_filename, generated[1][:name_with_ext], "Failed to correctly generate Address example filename")
|
61
|
+
assert_equal(phonenumbers_filename, generated[2][:name_with_ext], "Failed to correctly generate PhoneNumbers example filename")
|
62
|
+
|
63
|
+
assert_equal(expected_example, generated[0][:contents], "Failed to correctly generate top-level GeneratedFromJson example Java class from JSON")
|
64
|
+
assert_equal(expected_address, generated[1][:contents], "Failed to correctly generate Address example Java class from JSON")
|
65
|
+
assert_equal(expected_phonenumbers, generated[2][:contents], "Failed to correctly generate PhoneNumbers example Java class from JSON")
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# Restore the working directory to what it was previously
|
70
|
+
def teardown
|
71
|
+
Dir.chdir @previous_wd # restore the working directory to what it was previously
|
72
|
+
end
|
73
|
+
end
|
data/test/testjson.json
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
{
|
2
|
+
"firstName": "John",
|
3
|
+
"lastName": "Smith",
|
4
|
+
"isAlive": true,
|
5
|
+
"age": 25,
|
6
|
+
"address": {
|
7
|
+
"streetAddress": "21 2nd Street",
|
8
|
+
"city": "New York",
|
9
|
+
"state": "NY",
|
10
|
+
"postalCode": "10021-3100"
|
11
|
+
},
|
12
|
+
"phoneNumbers": [
|
13
|
+
{
|
14
|
+
"type": "home",
|
15
|
+
"number": "212 555-1234"
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"type": "office",
|
19
|
+
"number": "646 555-4567"
|
20
|
+
},
|
21
|
+
{
|
22
|
+
"type": "mobile",
|
23
|
+
"number": "123 456-7890"
|
24
|
+
}
|
25
|
+
],
|
26
|
+
"children": [],
|
27
|
+
"spouse": null
|
28
|
+
}
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: class_from_son
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Richard Morrisby
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-03 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: This gem will attempt to generate code of a class of an object representing
|
14
|
+
the contents of a Serialised-Object-Notation (SON) string (or file). E.g. it will
|
15
|
+
generate code of a object's class from some JSON.
|
16
|
+
email: rmorrisby@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- LICENCE.txt
|
22
|
+
- README.txt
|
23
|
+
- build.rb
|
24
|
+
- class_from_son.gemspec
|
25
|
+
- lib/class_from_SON.rb
|
26
|
+
- test/Address.java
|
27
|
+
- test/GeneratedFromJson.java
|
28
|
+
- test/PhoneNumbers.java
|
29
|
+
- test/address.rb
|
30
|
+
- test/generated_from_json.rb
|
31
|
+
- test/phonenumbers.rb
|
32
|
+
- test/test_class_from_son.rb
|
33
|
+
- test/testjson.json
|
34
|
+
homepage: https://rubygems.org/gems/class_from_son
|
35
|
+
licenses:
|
36
|
+
- MIT
|
37
|
+
metadata: {}
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.9'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 2.6.7
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: Generates classes from SON (e.g. JSON)
|
58
|
+
test_files:
|
59
|
+
- test/test_class_from_son.rb
|