diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -39,6 +39,7 @@ 'rsrc/css/application/base/phabricator-application-launch-view.css' => '16ca323f', 'rsrc/css/application/base/standard-page-view.css' => 'd3e1abe9', 'rsrc/css/application/chatlog/chatlog.css' => '852140ff', + 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '7fedf08b', 'rsrc/css/application/config/config-template.css' => '8e6c6fcd', 'rsrc/css/application/config/config-welcome.css' => '6abd79be', @@ -345,7 +346,6 @@ 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', - 'rsrc/js/application/calendar/event-all-day.js' => '712540b4', 'rsrc/js/application/config/behavior-reorder-fields.js' => '14a827de', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '9e507b59', 'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a', @@ -508,6 +508,7 @@ 'aphront-typeahead-control-css' => '0e403212', 'auth-css' => '1e655982', 'changeset-view-manager' => '58562350', + 'conduit-api-css' => '7bc725c4', 'config-options-css' => '7fedf08b', 'config-welcome-css' => '6abd79be', 'conpherence-durable-column-view' => '2e68a92f', @@ -584,7 +585,6 @@ 'javelin-behavior-doorkeeper-tag' => 'e5822781', 'javelin-behavior-durable-column' => '657c2b50', 'javelin-behavior-error-log' => '6882e80a', - 'javelin-behavior-event-all-day' => '712540b4', 'javelin-behavior-fancy-datepicker' => '5c0f680f', 'javelin-behavior-global-drag-and-drop' => 'c8e57404', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', diff --git a/src/applications/conduit/call/ConduitCall.php b/src/applications/conduit/call/ConduitCall.php --- a/src/applications/conduit/call/ConduitCall.php +++ b/src/applications/conduit/call/ConduitCall.php @@ -150,5 +150,9 @@ return $method; } + public function getMethodImplementation() { + return $this->handler; + } + } diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -21,6 +21,7 @@ $method = $this->method; $api_request = null; + $method_implementation = null; $log = new PhabricatorConduitMethodCallLog(); $log->setMethod($method); @@ -36,6 +37,7 @@ list($metadata, $params) = $this->decodeConduitParams($request, $method); $call = new ConduitCall($method, $params); + $method_implementation = $call->getMethodImplementation(); $result = null; @@ -151,7 +153,8 @@ return $this->buildHumanReadableResponse( $method, $api_request, - $response->toDictionary()); + $response->toDictionary(), + $method_implementation); case 'json': default: return id(new AphrontJSONResponse()) @@ -525,7 +528,8 @@ private function buildHumanReadableResponse( $method, ConduitAPIRequest $request = null, - $result = null) { + $result = null, + ConduitAPIMethod $method_implementation = null) { $param_rows = array(); $param_rows[] = array('Method', $this->renderAPIValue($method)); @@ -574,11 +578,20 @@ ->addTextCrumb($method, $method_uri) ->addTextCrumb(pht('Call')); + $example_panel = null; + if ($request && $method_implementation) { + $params = $request->getAllParameters(); + $example_panel = $this->renderExampleBox( + $method_implementation, + $params); + } + return $this->buildApplicationPage( array( $crumbs, $param_panel, $result_panel, + $example_panel, ), array( 'title' => pht('Method Call Result'), diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php --- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php +++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php @@ -3,31 +3,23 @@ final class PhabricatorConduitConsoleController extends PhabricatorConduitController { - private $method; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->method = $data['method']; - } - - public function processRequest() { - - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $method_name = $request->getURIData('method'); $method = id(new PhabricatorConduitMethodQuery()) ->setViewer($viewer) - ->withMethods(array($this->method)) + ->withMethods(array($method_name)) ->executeOne(); - if (!$method) { return new Aphront404Response(); } - $can_call_method = false; + $call_uri = '/api/'.$method->getAPIMethodName(); $status = $method->getMethodStatus(); $reason = $method->getMethodStatusDescription(); @@ -48,37 +40,13 @@ break; } - $error_types = $method->getErrorTypes(); - $error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.'); - $error_description = array(); - foreach ($error_types as $error => $meaning) { - $error_description[] = hsprintf( - '
  • %s: %s
  • ', - $error, - $meaning); - } - $error_description = phutil_tag('ul', array(), $error_description); - - $form = new AphrontFormView(); - $form + $form = id(new AphrontFormView()) + ->setAction($call_uri) ->setUser($request->getUser()) - ->setAction('/api/'.$this->method) - ->appendChild( - id(new AphrontFormStaticControl()) - ->setLabel('Description') - ->setValue($method->getMethodDescription())) - ->appendChild( - id(new AphrontFormStaticControl()) - ->setLabel('Returns') - ->setValue($method->getReturnType())) - ->appendChild( - id(new AphrontFormMarkupControl()) - ->setLabel('Errors') - ->setValue($error_description)) - ->appendChild(hsprintf( - '

    Enter parameters using '. - 'JSON. For instance, to enter a list, type: '. - '["apple", "banana", "cherry"]')); + ->appendRemarkupInstructions( + pht( + 'Enter parameters using **JSON**. For instance, to enter a '. + 'list, type: `["apple", "banana", "cherry"]`')); $params = $method->getParamTypes(); foreach ($params as $param => $desc) { @@ -117,12 +85,22 @@ ->setHeader($method->getAPIMethodName()); $form_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setFormErrors($errors) + ->setHeaderText(pht('Call Method')) ->appendChild($form); $content = array(); + $properties = $this->buildMethodProperties($method); + + $info_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('API Method: %s', $method->getAPIMethodName())) + ->setFormErrors($errors) + ->appendChild($properties); + + $content[] = $info_box; + $content[] = $form_box; + $content[] = $this->renderExampleBox($method, null); + $query = $method->newQueryObject(); if ($query) { $orders = $query->getBuiltinOrders(); @@ -185,7 +163,6 @@ return $this->buildApplicationPage( array( $crumbs, - $form_box, $content, ), array( @@ -193,4 +170,41 @@ )); } + private function buildMethodProperties(ConduitAPIMethod $method) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()); + + $view->addProperty( + pht('Returns'), + $method->getReturnType()); + + $error_types = $method->getErrorTypes(); + $error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.'); + $error_description = array(); + foreach ($error_types as $error => $meaning) { + $error_description[] = hsprintf( + '

  • %s: %s
  • ', + $error, + $meaning); + } + $error_description = phutil_tag('ul', array(), $error_description); + + $view->addProperty( + pht('Errors'), + $error_description); + + + $description = $method->getMethodDescription(); + $description = PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff())->setContent($description), + 'default', + $viewer); + $view->addSectionHeader(pht('Description')); + $view->addTextContent($description); + + return $view; + } + + } diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php --- a/src/applications/conduit/controller/PhabricatorConduitController.php +++ b/src/applications/conduit/controller/PhabricatorConduitController.php @@ -24,4 +24,250 @@ return $this->buildSideNavView()->getMenu(); } + protected function renderExampleBox(ConduitAPIMethod $method, $params) { + $arc_example = id(new PHUIPropertyListView()) + ->addRawContent($this->renderExample($method, 'arc', $params)); + + $curl_example = id(new PHUIPropertyListView()) + ->addRawContent($this->renderExample($method, 'curl', $params)); + + $php_example = id(new PHUIPropertyListView()) + ->addRawContent($this->renderExample($method, 'php', $params)); + + $panel_link = phutil_tag( + 'a', + array( + 'href' => '/settings/panel/apitokens/', + ), + pht('Conduit API Tokens')); + + $panel_link = phutil_tag('strong', array(), $panel_link); + + $messages = array( + pht( + 'Use the %s panel in Settings to generate or manage API tokens.', + $panel_link), + ); + + $info_view = id(new PHUIInfoView()) + ->setErrors($messages) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Examples')) + ->setInfoView($info_view) + ->addPropertyList($arc_example, pht('arc call-conduit')) + ->addPropertyList($curl_example, pht('cURL')) + ->addPropertyList($php_example, pht('PHP')); + } + + private function renderExample( + ConduitAPIMethod $method, + $kind, + $params) { + + switch ($kind) { + case 'arc': + $example = $this->buildArcanistExample($method, $params); + break; + case 'php': + $example = $this->buildPHPExample($method, $params); + break; + case 'curl': + $example = $this->buildCURLExample($method, $params); + break; + default: + throw new Exception(pht('Conduit client "%s" is not known.', $kind)); + } + + return $example; + } + + private function buildArcanistExample( + ConduitAPIMethod $method, + $params) { + + $parts = array(); + + $parts[] = '$ echo '; + if ($params === null) { + $parts[] = phutil_tag('strong', array(), ''); + } else { + $params = $this->simplifyParams($params); + $params = id(new PhutilJSON())->encodeFormatted($params); + $params = trim($params); + $params = csprintf('%s', $params); + $parts[] = phutil_tag('strong', array('class' => 'real'), $params); + } + + $parts[] = ' | '; + $parts[] = 'arc call-conduit '; + + $parts[] = '--conduit-uri '; + $parts[] = phutil_tag( + 'strong', + array('class' => 'real'), + PhabricatorEnv::getURI('/')); + $parts[] = ' '; + + $parts[] = '--conduit-token '; + $parts[] = phutil_tag('strong', array(), ''); + $parts[] = ' '; + + $parts[] = $method->getAPIMethodName(); + + return $this->renderExampleCode($parts); + } + + private function buildPHPExample( + ConduitAPIMethod $method, + $params) { + + $parts = array(); + + $libphutil_path = 'path/to/libphutil/src/__phutil_library_init__.php'; + + $parts[] = '')); + $parts[] = "\";\n"; + + $parts[] = '$api_parameters = '; + if ($params === null) { + $parts[] = 'array('; + $parts[] = phutil_tag('strong', array(), pht('')); + $parts[] = ');'; + } else { + $params = $this->simplifyParams($params); + $params = phutil_var_export($params, true); + $parts[] = phutil_tag('strong', array('class' => 'real'), $params); + $parts[] = ';'; + } + $parts[] = "\n\n"; + + $parts[] = '$client = new ConduitClient('; + $parts[] = phutil_tag( + 'strong', + array('class' => 'real'), + phutil_var_export(PhabricatorEnv::getURI('/'), true)); + $parts[] = ");\n"; + + $parts[] = '$client->setConduitToken($api_token);'; + $parts[] = "\n\n"; + + $parts[] = '$result = $client->callMethodSynchronous('; + $parts[] = phutil_tag( + 'strong', + array('class' => 'real'), + phutil_var_export($method->getAPIMethodName(), true)); + $parts[] = ', '; + $parts[] = '$api_parameters'; + $parts[] = ");\n"; + + $parts[] = 'print_r($result);'; + + return $this->renderExampleCode($parts); + } + + private function buildCURLExample( + ConduitAPIMethod $method, + $params) { + + $call_uri = '/api/'.$method->getAPIMethodName(); + + $parts = array(); + + $linebreak = array('\\', phutil_tag('br'), ' '); + + $parts[] = '$ curl '; + $parts[] = phutil_tag( + 'strong', + array('class' => 'real'), + csprintf('%R', PhabricatorEnv::getURI($call_uri))); + $parts[] = ' '; + $parts[] = $linebreak; + + $parts[] = '-d api.token='; + $parts[] = phutil_tag('strong', array(), 'api-token'); + $parts[] = ' '; + $parts[] = $linebreak; + + if ($params === null) { + $parts[] = '-d '; + $parts[] = phutil_tag('strong', array(), 'param'); + $parts[] = '='; + $parts[] = phutil_tag('strong', array(), 'value'); + $parts[] = ' '; + $parts[] = $linebreak; + $parts[] = phutil_tag('strong', array(), '...'); + } else { + $lines = array(); + $params = $this->simplifyParams($params); + + foreach ($params as $key => $value) { + $pieces = $this->getQueryStringParts(null, $key, $value); + foreach ($pieces as $piece) { + $lines[] = array( + '-d ', + phutil_tag('strong', array('class' => 'real'), $piece), + ); + } + } + + $parts[] = phutil_implode_html(array(' ', $linebreak), $lines); + } + + return $this->renderExampleCode($parts); + } + + private function renderExampleCode($example) { + require_celerity_resource('conduit-api-css'); + + return phutil_tag( + 'div', + array( + 'class' => 'PhabricatorMonospaced conduit-api-example-code', + ), + $example); + } + + private function simplifyParams(array $params) { + foreach ($params as $key => $value) { + if ($value === null) { + unset($params[$key]); + } + } + return $params; + } + + private function getQueryStringParts($prefix, $key, $value) { + if ($prefix === null) { + $head = phutil_escape_uri($key); + } else { + $head = $prefix.'['.phutil_escape_uri($key).']'; + } + + if (!is_array($value)) { + return array( + $head.'='.phutil_escape_uri($value), + ); + } + + $results = array(); + foreach ($value as $subkey => $subvalue) { + $subparts = $this->getQueryStringParts($head, $subkey, $subvalue); + foreach ($subparts as $subpart) { + $results[] = $subpart; + } + } + + return $results; + } + } diff --git a/webroot/rsrc/css/application/conduit/conduit-api.css b/webroot/rsrc/css/application/conduit/conduit-api.css new file mode 100644 --- /dev/null +++ b/webroot/rsrc/css/application/conduit/conduit-api.css @@ -0,0 +1,16 @@ +/** + * @provides conduit-api-css + */ +.conduit-api-example-code { + margin: 16px; + white-space: pre; + color: {$darkgreytext}; +} + +.conduit-api-example-code strong { + color: {$red}; +} + +.conduit-api-example-code strong.real { + color: {$blue}; +}