Class: ClassFromSON

Inherits:
Object
  • Object
show all
Defined in:
lib/class_from_SON.rb

Overview

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

Constant Summary collapse

@@target_languages =
[:java, :java_lombok, :ruby]
@@input_modes =
[:json]

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.error_and_exit(message) ⇒ Object



20
21
22
23
# File 'lib/class_from_SON.rb', line 20

def self.error_and_exit(message)
	puts "ERROR : #{message}"
	exit
end

.generate(dest_lang, source, source_lang, make_file = true, force_file = false) ⇒ Object

Will generate classes from a SON string 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

dest_lang is symbol file is filename & path source_lang is symbol make_file flag defaults to true; set to false if you do not want files to be created by this method force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)



379
380
381
382
# File 'lib/class_from_SON.rb', line 379

def ClassFromSON.generate(dest_lang, source, source_lang, make_file = true, force_file = false)
	o = ClassFromSON.new
	o.generate(dest_lang, source, source_lang, make_file, force_file)
end

.generate_from_file(dest_lang, file, source_lang = nil, make_file = true, force_file = false) ⇒ Object

Will generate classes from a SON file 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

dest_lang is symbol file is filename & path source_lang is symbol or nil (if nil, source language will be determined from the file extension) make_file flag defaults to true; set to false if you do not want files to be created by this method force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)



359
360
361
362
363
364
365
366
367
368
369
# File 'lib/class_from_SON.rb', line 359

def ClassFromSON.generate_from_file(dest_lang, file, source_lang = nil, make_file = true, force_file = false)

	error_and_exit "Could not locate file #{file}" unless File.exists?(file)

	source_lang ||= File.extname(file).gsub(".", "")
	source = File.readlines(file).join
	ClassFromSON.generate(dest_lang, source, source_lang, make_file, force_file)

	# o = ClassFromSON.new
	# o.generate(dest_lang, source, source_lang, make_file)
end

Instance Method Details

#convert_array_to_type(value_types) ⇒ Object

Translate “Array” into the desired output language Also needs the ‘value types’, i.e. what type is this array? A list of strings? A list of ints?



97
98
99
100
101
102
103
104
105
# File 'lib/class_from_SON.rb', line 97

def convert_array_to_type(value_types)
	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
	case @language
	when :java, :java_lombok
		return "List<#{convert_ruby_type_to_type(value_types[0])}>"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#convert_boolean_to_typeObject

Translate “Fixnum” into the desired output language



76
77
78
79
80
81
82
83
# File 'lib/class_from_SON.rb', line 76

def convert_boolean_to_type
	case @language
	when :java, :java_lombok
		return "boolean"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#convert_custom_class_type(type) ⇒ Object

Returns code representing the start of the class



120
121
122
123
124
125
126
127
# File 'lib/class_from_SON.rb', line 120

def convert_custom_class_type(type)
	case @language
	when :java, :java_lombok, :ruby
		return type.capitalize_first_letter_only
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#convert_fixnum_to_typeObject

Translate “Fixnum” into the desired output language



56
57
58
59
60
61
62
63
# File 'lib/class_from_SON.rb', line 56

def convert_fixnum_to_type
	case @language
	when :java, :java_lombok
		return "int"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#convert_float_to_typeObject

Translate “Fixnum” into the desired output language



66
67
68
69
70
71
72
73
# File 'lib/class_from_SON.rb', line 66

def convert_float_to_type
	case @language
	when :java, :java_lombok
		return "float"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#convert_hash_to_type(value_types) ⇒ Object

Translate “Hash” into the desired output language 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
110
111
112
113
114
115
116
117
# File 'lib/class_from_SON.rb', line 109

def convert_hash_to_type(value_types)
	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
	case @language
	when :java, :java_lombok
		return "HashMap<#{convert_ruby_type_to_type(value_types[0])}, #{convert_ruby_type_to_type(value_types[1])}>"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#convert_ruby_type_to_type(type, value_types = []) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/class_from_SON.rb', line 29

def convert_ruby_type_to_type(type, value_types = [])
	# puts "#{__method__} called with type '#{type.inspect}', #{type.class}, #{value_types.inspect}"
	# Because type is an instance of Class, we need to compare names, so convert to String
	# Use .to_s instead of .name to cope with custom types (i.e. types that are themselves new classes to be generated)
	case type.to_s
	when "Fixnum", "Integer"
		converted = convert_fixnum_to_type
	when "Float"
		converted = convert_float_to_type
	when "String"
		converted = convert_string_to_type
	when "TrueClass", "FalseClass"
		converted = convert_boolean_to_type
	when "Array"
		converted = convert_array_to_type(value_types)
	when "Hash"
		converted = convert_hash_to_type(value_types)
	when "NilClass" # default nil to String
		converted = convert_string_to_type
	else
		converted = convert_custom_class_type(type)
	end
	# puts "Converted '#{type.inspect}' to #{converted}"
	converted
end

#convert_string_to_typeObject

Translate “String” into the desired output language



86
87
88
89
90
91
92
93
# File 'lib/class_from_SON.rb', line 86

def convert_string_to_type
	case @language
	when :java, :java_lombok
		return "String"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#error(message) ⇒ Object



11
12
13
# File 'lib/class_from_SON.rb', line 11

def error(message)
	puts "ERROR : #{message}"
end

#error_and_exit(message) ⇒ Object



15
16
17
18
# File 'lib/class_from_SON.rb', line 15

def error_and_exit(message)
	puts "ERROR : #{message}"
	exit
end

#generate(dest_lang, source, source_lang, make_file = true, force_file = false) ⇒ Object

Will generate classes from a SON string. 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

dest_lang is symbol file is filename & path source_lang is symbol make_file flag defaults to true; set to false if you do not want files to be created by this method force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/class_from_SON.rb', line 392

def generate(dest_lang, source, source_lang, make_file = true, force_file = false)

	error_and_exit "Please supply first argument as a Symbol" unless dest_lang.class == Symbol

	@language = dest_lang
	if @@target_languages.include?(@language)
		# may proceed
		# TODO other target languages, e.g. C#, Python
	else
		error_and_exit "Cannot generate language #{@language}; can only generate #{@@target_languages.join(", ")}"
	end
	@extension = set_file_extension_for_language

	@mode = source_lang.to_sym
	if @@input_modes.include?(@mode)
		# may proceed
	else
		error_and_exit "Cannot parse input language #{@mode}; can only parse #{@@input_modes.join(", ")}"
	end

	# TODO other input languages, e.g. XML, YAML
	case @mode
	when :json
		begin
			hash = JSON.parse(source)
		rescue JSON::ParserError => e
			error_and_exit "Could not parse supplied string as JSON. Error message : #{e.message}"
		end
	else
		error_and_exit "Cannot parse mode #{@mode}"
	end

	# If we have read in an array instead of a hash, then take the first element of the array
	# This assumes that each element in this top-level array has the same structure, which is reasonable
	if hash.class == Array && hash.size > 0
		hash = hash.shift
	end

	error_and_exit "Input file did not have a hash / map of key-value pairs; could not parse" unless hash.class == Hash

	error_and_exit "Input file hash / map was empty" if hash.empty?

	top_level_classname = generate_top_level_name
	output_classes = generate_output_classes(hash, top_level_classname).flatten # returns an array

	if make_file
		output_classes.each do |out|
			name = out[:name_with_ext]
			contents = out[:contents]
			unless force_file
				error_and_exit "Want to generate output file #{name}, but that file already exists" if File.exists?(name)
			end
			File.open(name, "w+") do |f|
				f.puts contents
			end
			puts "Wrote out file #{name}"
		end

		puts "Please inspect generated code files and adjust names and types accordingly"
	end
	output_classes
end

#generate_class_endObject

Returns code representing the end of the class



210
211
212
213
214
215
216
217
218
219
220
# File 'lib/class_from_SON.rb', line 210

def generate_class_end
	case @language
	when :java, :java_lombok
		class_end = "}"
	when :ruby 
		class_end = "end"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
	class_end
end

#generate_class_start(name) ⇒ Object

Returns code representing the start of the class



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/class_from_SON.rb', line 174

def generate_class_start(name)
	case @language
	when :java
		start = <<-START

import com.fasterxml.jackson.annotation.JsonProperty;

public class #{convert_custom_class_type(name)} {
START
	when :java_lombok
		start = <<-START

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@JsonInclude(Include.NON_NULL)
@NoArgsConstructor // Need @NoArgsConstructor for JSON deserialisation
@AllArgsConstructor // Need @AllArgsConstructor for @Builder
@Builder
@Getter
@Setter
public class #{convert_custom_class_type(name)} {
START
	when :ruby
		start = "class #{convert_custom_class_type(name)}"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
	start
end

#generate_classname(name) ⇒ Object

Returns an appropriately-formatted classname for the given name



141
142
143
144
145
146
147
148
# File 'lib/class_from_SON.rb', line 141

def generate_classname(name)
	case @language
	when :java, :java_lombok, :ruby
		return name.capitalize_first_letter_only
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#generate_code_from_attributes(attributes) ⇒ Object

Returns code representing each of the supplied attributes



248
249
250
251
252
253
254
255
256
257
# File 'lib/class_from_SON.rb', line 248

def generate_code_from_attributes(attributes)
	case @language
	when :java, :java_lombok
		return generate_java_code_from_attributes(attributes)
	when :ruby
		return generate_ruby_code_from_attributes(attributes)
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#generate_filename(name) ⇒ Object

Returns an appropriately-formatted filename for the given name



151
152
153
154
155
156
157
158
159
160
# File 'lib/class_from_SON.rb', line 151

def generate_filename(name)
	case @language
	when :java, :java_lombok
		return name.capitalize_first_letter_only + @extension
	when :ruby
		return name.snakecase + @extension
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#generate_getter_and_setter(type, name) ⇒ Object



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/class_from_SON.rb', line 222

def generate_getter_and_setter(type, name)
	lines = []
	case @language
	when :java_lombok
		# do nothing - Lombok's raison d'etre is to avoid getters & setters
	when :java
		# This is safe even if the name is already in snakecase
		field_name_for_getter = name.snakecase.pascalcase

		name = name.camelcase if name.include? "_"
			
		lines << "\t"
		lines << "\tpublic #{type} get#{field_name_for_getter}() {"
		lines << "\t\treturn #{name};"
		lines << "\t}"
		lines << "\t"
		lines << "\tpublic void set#{field_name_for_getter}(#{type} #{name}) {"		
		lines << "\t\tthis.#{name} = #{name};"
		lines << "\t}"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
	lines
end

#generate_java_code_from_attributes(attributes) ⇒ Object

Returns Java code representing each of the supplied attributes



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/class_from_SON.rb', line 260

def generate_java_code_from_attributes(attributes)
	code = []
	# Instance variables
	attributes.each do |att|
		if att[:name].include? "_"
			snakecase_name = att[:name]
			camelcase_name = att[:name].camelcase
			code << "\t@JsonProperty(\"#{snakecase_name}\")"
			code << "\tprivate #{convert_ruby_type_to_type(att[:type], att[:value_types])} #{camelcase_name};"
			code << "" # add a new line so that fields are separated & easier to read
		else 
			code << "\tprivate #{convert_ruby_type_to_type(att[:type], att[:value_types])} #{att[:name]};"
		end
	end

	#TODO constructor

	# Getters & setters
	attributes.each do |att|
		code << generate_getter_and_setter(convert_ruby_type_to_type(att[:type], att[:value_types]), att[:name])
	end
	code
end

#generate_output_classes(hash, top_level_classname = nil) ⇒ Object

From the supplied hash, generates code representing a class in the desired language (as a string) Returns an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/class_from_SON.rb', line 308

def generate_output_classes(hash, top_level_classname = nil)
	classname = generate_classname(top_level_classname)
	filename = generate_filename(classname)
	files = []
	this_file = {:name => classname, :name_with_ext => filename}
	lines = []
	lines << generate_class_start(classname)
	attributes = [] # array of hashes; keys => :name, :type, :value_types # :type, :value_types ([]) are initially kept as Ruby class names

	hash.each_pair do |k, v|
		attribute = {:name => k}
		if v.class == Array
			attribute[:type] = Array
			if v[0].class == Hash
				new_files = generate_output_classes(v[0], k)
				attribute[:value_types] = [new_files[0][:name]]
				files += new_files
			else
				# Array only contains primitives, not objects				
				attribute[:value_types] = [v[0].class]
			end			
		elsif v.class == Hash
			new_files = generate_output_classes(v, k)
			attribute[:type] = new_files[0][:name]
			files += new_files
		else
			attribute[:type] = v.class
		end
		attributes << attribute
	end

	lines << generate_code_from_attributes(attributes)
	lines << generate_class_end
	lines.flatten!
	this_file[:contents] = lines.join("\n")
	files.insert(0, this_file)
	files
end

#generate_ruby_code_from_attributes(attributes) ⇒ Object

Returns Ruby code representing each of the supplied attributes



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/class_from_SON.rb', line 285

def generate_ruby_code_from_attributes(attributes)
	code = []
	names = []
	attributes.each {|att| names << att[:name]}

	# Instance variables
	names.each do |name|
		code << "\tattr_accessor #{name.to_sym.inspect}"
	end
	code << "" # An empty string is enough to trigger a newline

	# Constructor
	code << "\tdef initialize(#{names.join(", ")})"
	names.each do |name|
		code << "\t\t@#{name} = #{name}"
	end
	code << "\tend"

	code
end

#generate_top_level_nameObject



129
130
131
132
133
134
135
136
137
138
# File 'lib/class_from_SON.rb', line 129

def generate_top_level_name
	case @language
	when :java, :java_lombok
		return "generatedFrom#{@mode.capitalize}"
	when :ruby
		return "generated_from_#{@mode}"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#set_file_extension_for_languageObject



162
163
164
165
166
167
168
169
170
171
# File 'lib/class_from_SON.rb', line 162

def set_file_extension_for_language
	case @language
	when :java, :java_lombok
		@extension = ".java"
	when :ruby
		@extension = ".rb"
	else 
		error_and_exit "Could not convert to output language #{@language}"
	end
end

#warn(message) ⇒ Object



25
26
27
# File 'lib/class_from_SON.rb', line 25

def warn(message)
	puts "WARN : #{message}"
end