require 'net/https' require 'uri' require 'json' require 'yaml' require 'lhj/helper/dingtalk_config' require 'lhj/helper/erb_template_helper' module Lhj class Command # generate model from yapi class Yapi < Command self.summary = '通过yapi接口生成请求' self.description = '执行`lhj api`生成接口模型' API_INTERFACE_URL = 'api/interface/get?id='.freeze API_PROJECT_URL = 'api/project/get?id='.freeze def self.options [ %w[--id api的id], %w[--model-pre 模型的前缀], %w[--model-name 模型的名称], %w[--save 保存生成文件], %w[--sync 同步到github], %w[--notify 通知到钉钉] ] end def initialize(argv) @id = argv.option('id') @model_pre_name = argv.option('model-pre') @model_result_name = argv.option('model-name') @language = argv.option('lan', 'oc') @save = argv.flag?('save', false) @sync = argv.flag?('sync', false) @notify = argv.flag?('notify', false) @debug = argv.flag?('debug', false) @sync = true if @notify @save = true if @sync @http_headers = [] @config_id = '' @config_model_pre = 'ML' @config_model_name = 'Result' @config_base_url = 'http://yapi.xx.com' @type_trans = {} super end def handle process(yml_file) if File.exist?(yml_file) end def process(config_file) load_config(config_file) res_body = get_interface_api_model unless res_body['errcode'].to_i.zero? puts '请执行 lhj yapi-init 初始化配置'.red return end return unless res_body && !res_body.empty? # get project info project_id = res_body['data']['project_id'] project_info = get_project_info(project_id) # 1.print request result res_models = print_res_body_model(res_body) # 2.print request json body req_models = print_req_body_model(res_body) if res_body['data']['req_body_is_json_schema'] # 3.print request param print_req_query(res_body['data']) unless res_body['data']['req_body_is_json_schema'] # 4.print request method service_code = print_http_method(res_body['data'], project_info) # print request mock data print_mock_request_data(req_models) if !req_models.nil? && !req_models.empty? # 5.save to file file_map = save_to_file(service_code) if @save # 6.push to git push_to_git if @sync # 7.notify robot notify_robot(file_map, res_body, project_info) if @notify end def puts_h(str) puts str.magenta @h_file_array ||= [] @h_file_array << str end def puts_h_red(str) puts str.red @h_file_array ||= [] @h_file_array << str end def puts_m(str) puts str.blue @m_file_array ||= [] @m_file_array << str end def puts_mock(str) puts str.green @req_mock_array ||= [] @req_mock_array << str end def sub_folder_name return @sub_folder_name if @sub_folder_name time = Time.now @sub_folder_name ||= time.strftime('%Y%m%d%H%M%S') @sub_folder_name end def save_to_file(service_code) name = model_name file_name = gen_model_name FileUtils.mkdir_p(File.expand_path(sub_folder_name, '.')) unless File.exist?(File.expand_path(sub_folder_name, '.')) h_file = File.join('.', sub_folder_name, "#{file_name}.h") m_file = File.join('.', sub_folder_name, "#{file_name}.m") service_file = File.join('.', sub_folder_name, "#{model_pre}#{name}Service.m") req_mock_file = File.join('.', sub_folder_name, "#{model_pre}#{name}Mock.m") File.write(h_file, @h_file_array.join("\n")) if @h_file_array.count.positive? File.write(m_file, @m_file_array.join("\n")) if @m_file_array.count.positive? File.write(service_file, service_code) if service_code File.write(req_mock_file, @req_mock_array.join("\n")) if @req_mock_array.count.positive? puts "\n\n生成文件成功!所在路径:\n#{File.expand_path(h_file)} \n#{File.expand_path(m_file)}".green if @save && !@notify { h_file: "#{sub_folder_name}/#{file_name}.h", m_file: "#{sub_folder_name}/#{file_name}.m", s_file: "#{sub_folder_name}/#{model_pre}#{name}Service.m", req_mock_file: "#{sub_folder_name}/#{model_pre}#{name}Mock.m" } end def push_to_git Actions.sh('git checkout master') Actions.sh('git add .') Actions.sh("git commit -m 'generate yapi code'") Actions.sh('git push') end def notify_robot(upload_info, interface_info, project_info) i_url = "#{project_info['data']['basepath']}#{interface_info['data']['path']}" username = interface_info['data']['username'] title = interface_info['data']['title'] p_id = project_info['data']['_id'] api_url = api_original_url(p_id) temp_vars = upload_info.merge({ api_id: api_id, title: title, username: username, interface_url: i_url, api_url: api_url }) template = Lhj::ErbTemplateHelper.load('oc_code_notify') output = Lhj::ErbTemplateHelper.render(template, temp_vars, '-') Lhj::Dingtalk.post_message_robot(robot_url, 'yapi generate', output) end def interface_url_str "#{@config_base_url}/#{API_INTERFACE_URL}#{api_id}" end def project_url_str(project_id) "#{@config_base_url}/#{API_PROJECT_URL}#{project_id}" end def api_original_url(project_id) "#{@config_base_url}/project/#{project_id}/interface/api/#{api_id}" end def yml_file File.join(Lhj::Config.instance.home_dir, 'yapi.yml') end def load_config(file) config = YAML.load_file(file) config.each do |k, v| @http_headers << "#{k}=#{v}" if k.eql?('__wpkreporterwid_') || k.eql?('_yapi_token') || k.eql?('_yapi_uid') end @config_base_url = config['base_url'] @config_id = config['id'] @config_model_pre = config['model_pre'] @config_model_suffix = config['model_suffix'] @config_model_name = config['model_name'] @config_robot_url = config['dingtalk'] @type_trans = config['type_trans'] @config_property_mapper = config['property_mapper'] end def api_id @id || @config_id.to_s end def model_pre @model_pre_name || @config_model_pre end def model_name @model_result_name || @config_model_name end def req_model_name 'RequestParam' end def model_suffix @config_model_suffix || 'Model' end def robot_url @config_robot_url || 'https://oapi.dingtalk.com/robot/send?access_token=fe879fd3e7a3b5e59d5719b2384845b7884901919be5a78fe443cbf777869807' end def property_mapper @config_property_mapper || { 'id' => 'gid' } end def get_interface_api_model uri = URI.parse(interface_url_str) req = Net::HTTP::Get.new(uri) req['Cookie'] = @http_headers.join('; ') res = Net::HTTP.start(uri.hostname, uri.port) do |http| http.request(req) end res_json = JSON.parse(res.body) puts res.body unless res_json['errcode'].to_i.zero? puts res.body if @debug res_json end def get_project_info(project_id) url = project_url_str(project_id) uri = URI.parse(url) req = Net::HTTP::Get.new(uri) req['Cookie'] = @http_headers.join('; ') res = Net::HTTP.start(uri.hostname, uri.port) do |http| http.request(req) end JSON.parse(res.body) end def print_res_body_model(res_json) res_body = fetch_res_boy(res_json) return unless res_body puts "\n<===============打印返回数据模型-Begin=====================>\n".green models = [] handle_model(res_body, :res) do |model| models << model end case @language when 'oc' print_models(models) print_models_impl(models) when 'java' print_models_for_java(models) end puts "\n<===============打印返回数据模型-End=====================>\n".green models end def print_req_body_model(res_json) req_body = fetch_req_body(res_json) return unless req_body puts "\n<===============打印请求模型-Begin=====================>\n".green models = [] handle_model(req_body, :req) do |model| models << model end case @language when 'oc' print_models(models) print_models_impl(models) when 'java' print_models_for_java(models) end puts "\n<===============打印请求模型-End=====================>\n".green models end def fetch_res_boy(res_json) return if !res_json || !res_json['data'] || !res_json['data']['res_body'] res_body = JSON.parse(res_json['data']['res_body']) result = res_body result = res_body['properties']['detailMsg'] if res_body['properties'] && res_body['properties']['detailMsg'] return unless result['type'] == 'object' || result['type'] == 'array' result['name'] = gen_model_name result end def fetch_req_body(res_json) return if !res_json || !res_json['data'] || !res_json['data']['req_body_other'] result = JSON.parse(res_json['data']['req_body_other']) result['name'] = gen_model_name(nil, :req) result end def gen_model_name(property_name = nil, type = :res) name = model_name unless property_name.nil? name = property_name.gsub(/vo|model|list/i, '').gsub(/(.*)s$/, '\1').gsub(/^\w/) { Regexp.last_match(0).upcase } name = property_name.gsub(/^\w/) { Regexp.last_match(0).upcase } if name.length <= 0 end if type == :req "#{model_pre}#{name}#{req_model_name}" else "#{model_pre}#{name}#{model_suffix}" end end def handle_model(model, type, &block) p_type = model['type'] p_name = model['name'] p_properties = model['properties'] p_model = { name: p_name } properties = [] case p_type when 'object' p_properties.each do |k, v| c_type = @type_trans[v['type']] || v['type'] c_model = { key: k, type: c_type, description: v['description'], default: '' } if v['type'].eql?('object') || v['type'].eql?('array') o = v['items'] || v o['name'] = gen_model_name(k, type) if v['type'].eql?('array') && v['items']['type'].eql?('string') c_model[:type_name] = 'NSString' else c_model[:type_name] = o['name'] handle_model(o, type, &block) end end properties << c_model end p_model[:properties] = properties block[p_model] if block_given? when 'array' t = model['items'] t['name'] = p_name handle_model(t, type, &block) end end def print_models(models) models.each do |model| model_name = model[:name] || '' model_properties = model[:properties] puts_h "@interface #{model_name} : NSObject" model_properties.each do |m| print_model(m) end puts_h "@end\n\n\n" end end def print_models_for_java(models) models.each do |model| model_name = model[:name] || '' model_properties = model[:properties] puts_h "public class #{model_name} extends BaseModel implements BusinessEvent.ProductDataCollect {" model_properties.each do |m| print_model_for_java(m) end puts_h "}\n\n\n" end end def print_models_impl(models) models.each do |model| puts_m "@implementation #{model[:name]}" str = model[:properties].filter { |p| p[:type].eql?('array') && !p[:type_name].eql?('NSString') }.map { |p| "@\"#{p[:key]}\": #{p[:type_name]}.class" }.join(', ') if str&.length&.positive? puts_m '+(NSDictionary *)modelContainerPropertyGenericClass {' puts_m " return @{#{str}};" puts_m '}' end properties = model[:properties].filter { |p| property_mapper.keys.include?(p[:key]) }.map do |p| "@\"#{property_mapper[p[:key]]}\": @\"#{p[:key]}\"" end property_mapper_str = properties.join(', ') if properties.count.positive? if property_mapper_str&.length&.positive? puts_m '+ (NSDictionary *)modelCustomPropertyMapper {' puts_m " return @{#{property_mapper_str}};" puts_m '}' end puts_m "@end\n" puts "\n\n" end end def print_model(m) key = m[:key] key = property_mapper[key] if property_mapper.keys.include?(key) type_name = m[:type_name] type = m[:type] des = m[:description] || '' des.gsub!(/\n/, ' ') default = m[:default] puts_h "///#{des} #{default}" case type when 'integer' puts_h "@property (nonatomic, assign) NSInteger #{key};" when 'string' puts_h "@property (nonatomic, copy) NSString *#{key};" when 'number' puts_h "@property (nonatomic, strong) NSNumber *#{key};" when 'float' puts_h "@property (nonatomic, assign) CGFloat #{key};" when 'double' puts_h "@property (nonatomic, assign) double #{key};" when 'boolean' puts_h "@property (nonatomic, assign) BOOL #{key};" when 'object' puts_h "@property (nonatomic, strong) #{type_name} *#{key};" when 'array' puts_h "@property (nonatomic, strong) NSArray<#{type_name} *> *#{key};" else puts_h "@property (nonatomic, copy) NSString *#{key};" end end def print_model_for_java(m) key = m[:key] type_name = m[:type_name] type = m[:type] des = m[:description] || '' des.gsub!(/\n/, ' ') default = m[:default] case type when 'integer' puts_h "int #{key};//#{des} #{default}" when 'string' puts_h "String #{key};//#{des} #{default}" when 'number' puts_h "double #{key};//#{des} #{default}" when 'float' puts_h "double #{key};//#{des} #{default}" when 'double' puts_h "double #{key};//#{des} #{default}" when 'boolean' puts_h "boolean #{key};//#{des} #{default}" when 'object' puts_h "#{type_name} #{key};//#{des} #{default}" when 'array' puts_h "#{type_name}[] #{key};//#{des} #{default}" else puts_h "String #{key};//#{des} #{default}" end end # @param [Object] data def print_req_query(data) return unless data && data['req_query'] case @language when 'oc' param_model_name = gen_model_name(nil, :req) puts_h "@interface #{param_model_name} : NSObject" data['req_query'].each do |h| des = h['desc'] puts_h "///#{des} #{h['example']}" puts_h "@property (nonatomic, copy) NSString *#{h['name']};" end puts_h '@end' puts "\n\n" when 'java' end end def print_http_method(data, project_info) return unless data path = data['path'] if path arr = path.split('/').map do |s| re = s.gsub(/[^A-Za-z0-9](.)/) { Regexp.last_match(0).upcase } re = re.gsub(/[^A-Za-z0-9]/, '') re.gsub(/^\w/) { Regexp.last_match(0).upcase } end path_name = arr.join('') end path_key = "k#{path_name}URL" display_path = "#{project_info['data']['basepath']}#{path}" display_path = display_path[1..-1] if display_path.index('/') == 0 result_model_name = gen_model_name param_model_name = gen_model_name(nil, :req) mth = data['method'] mth = 'JSON' if data['req_body_is_json_schema'] case @language when 'oc' puts "\n<===============方法调用=====================>\n".green model_temp = Lhj::ErbTemplateHelper.load('oc_code_service_inner') model_temp_result = Lhj::ErbTemplateHelper.render(model_temp, { result_model_name: result_model_name }, '-') yapi_temp = Lhj::ErbTemplateHelper.load('oc_code_service') yapi_vars = { title: data['title'], desc: data['desc'], username: data['username'], path: display_path, path_key: path_key, path_name: path_name, result_model_name: result_model_name, param_model_name: param_model_name, mth: mth, model_template: model_temp_result } yapi_temp_result = Lhj::ErbTemplateHelper.render(yapi_temp, yapi_vars, '-') puts yapi_temp_result.green return yapi_temp_result when 'java' end end def print_mock_request_data(req_models) puts "\n<===============打印Mock Data-Begin=====================>\n".green req_models.each do |model| model_name = model[:name] || '' model_properties = model[:properties] puts_mock "#{model_name} *mock = [[#{model_name} alloc] init];" model_properties.each do |m| puts_mock "/// #{m[:description]}" type = m[:type] case type when 'integer' puts_mock "mock.#{m[:key]} = 0;" when 'number' puts_mock "mock.#{m[:key]} = 0;" when 'float' puts_mock "mock.#{m[:key]} = 0;" when 'double' puts_mock "mock.#{m[:key]} = 0;" when 'object' puts_mock "mock.#{m[:key]} = @{};" when 'array' puts_mock "mock.#{m[:key]} = @[];" else puts_mock "mock.#{m[:key]} = @\"\";" end end puts_mock "\n\n" end puts "\n<===============打印Mock Data-End=====================>\n".green end end end end